From 239214ef92ad765bd425710240a076b99decc112 Mon Sep 17 00:00:00 2001 From: Geometrically <18202329+Geometrically@users.noreply.github.com> Date: Fri, 7 Jul 2023 12:20:16 -0700 Subject: [PATCH] Initial Auth Impl + More Caching (#647) * Port redis to staging * redis cache on staging * add back legacy auth callback * Begin work on new auth flows * Finish all auth flows * Finish base session authentication * run prep + fix clippy * make compilation work --- .env | 31 +- Cargo.lock | 1480 +++++----- Cargo.toml | 55 +- docker-compose.yml | 8 + migrations/20230628180115_kill-ory.sql | 48 + sqlx-data.json | 3449 +++++++++--------------- src/auth/checks.rs | 194 ++ src/auth/flows.rs | 889 ++++++ src/auth/mod.rs | 81 + src/{util => auth}/pat.rs | 21 +- src/auth/session.rs | 80 + src/auth/validate.rs | 108 + src/database/models/ids.rs | 39 +- src/database/models/mod.rs | 13 +- src/database/models/project_item.rs | 884 +++--- src/database/models/session_item.rs | 312 +++ src/database/models/team_item.rs | 179 +- src/database/models/user_item.rs | 868 +++--- src/database/models/version_item.rs | 656 +++-- src/health/mod.rs | 8 - src/health/status.rs | 14 - src/main.rs | 23 +- src/models/ids.rs | 2 + src/models/mod.rs | 1 + src/models/projects.rs | 17 +- src/models/reports.rs | 33 +- src/models/sessions.rs | 29 + src/models/threads.rs | 41 + src/models/users.rs | 22 +- src/routes/health.rs | 31 - src/routes/maven.rs | 50 +- src/routes/mod.rs | 8 +- src/routes/updates.rs | 32 +- src/routes/v2/admin.rs | 120 +- src/routes/v2/auth.rs | 214 -- src/routes/v2/midas.rs | 325 --- src/routes/v2/mod.rs | 12 +- src/routes/v2/moderation.rs | 7 +- src/routes/v2/notifications.rs | 20 +- src/routes/v2/pats.rs | 25 +- src/routes/v2/project_creation.rs | 7 +- src/routes/v2/projects.rs | 288 +- src/routes/v2/reports.rs | 59 +- src/routes/v2/teams.rs | 110 +- src/routes/v2/threads.rs | 75 +- src/routes/v2/users.rs | 157 +- src/routes/v2/version_creation.rs | 23 +- src/routes/v2/version_file.rs | 696 ++--- src/routes/v2/versions.rs | 151 +- src/util/auth.rs | 588 ---- src/util/ext.rs | 11 + src/util/mod.rs | 2 - src/util/routes.rs | 13 - 53 files changed, 6250 insertions(+), 6359 deletions(-) create mode 100644 migrations/20230628180115_kill-ory.sql create mode 100644 src/auth/checks.rs create mode 100644 src/auth/flows.rs create mode 100644 src/auth/mod.rs rename src/{util => auth}/pat.rs (90%) create mode 100644 src/auth/session.rs create mode 100644 src/auth/validate.rs create mode 100644 src/database/models/session_item.rs delete mode 100644 src/health/mod.rs delete mode 100644 src/health/status.rs create mode 100644 src/models/sessions.rs delete mode 100644 src/routes/health.rs delete mode 100644 src/routes/v2/auth.rs delete mode 100644 src/routes/v2/midas.rs delete mode 100644 src/util/auth.rs diff --git a/.env b/.env index cf8dbcf82..765897317 100644 --- a/.env +++ b/.env @@ -15,17 +15,16 @@ CLOUDFLARE_INTEGRATION=false DATABASE_URL=postgresql://labrinth:labrinth@localhost/labrinth DATABASE_MIN_CONNECTIONS=0 DATABASE_MAX_CONNECTIONS=16 + +REDIS_URL=redis://localhost + MEILISEARCH_ADDR=http://localhost:7700 MEILISEARCH_KEY=modrinth BIND_ADDR=127.0.0.1:8000 -SELF_ADDR=http://127.0.0.1:8000 +SELF_ADDR=http://localhost:8000 MOCK_FILE_PATH=/tmp/modrinth -MINOS_URL=http://127.0.0.1:4000 -KRATOS_URL=http://127.0.0.1:4433 -ORY_AUTH_BEARER=none - STORAGE_BACKEND=local BACKBLAZE_KEY_ID=none @@ -43,8 +42,6 @@ LOCAL_INDEX_INTERVAL=3600 # 30 minutes VERSION_INDEX_INTERVAL=1800 -GITHUB_CLIENT_ID=none - RATE_LIMIT_IGNORE_IPS='["127.0.0.1"]' WHITELISTED_MODPACK_DOMAINS='["cdn.modrinth.com", "edge.forgecdn.net", "github.com", "raw.githubusercontent.com"]' @@ -54,9 +51,23 @@ ALLOWED_CALLBACK_URLS='["localhost", ".modrinth.com", "127.0.0.1"]' ARIADNE_ADMIN_KEY=feedbeef ARIADNE_URL=https://staging-ariadne.modrinth.com/v1/ -STRIPE_TOKEN=none -STRIPE_WEBHOOK_SECRET=none - PAYPAL_API_URL=https://api-m.sandbox.paypal.com/v1/ PAYPAL_CLIENT_ID=none PAYPAL_CLIENT_SECRET=none + +GITHUB_CLIENT_ID=none +GITHUB_CLIENT_SECRET=none + +GITLAB_CLIENT_ID=none +GITLAB_CLIENT_SECRET=none + +DISCORD_CLIENT_ID=none +DISCORD_CLIENT_SECRET=none + +MICROSOFT_CLIENT_ID=none +MICROSOFT_CLIENT_SECRET=none + +GOOGLE_CLIENT_ID=none +GOOGLE_CLIENT_SECRET=none + +STEAM_API_KEY=none diff --git a/Cargo.lock b/Cargo.lock index fb708f226..af789b1b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5" dependencies = [ "actix-rt", "actix_derive", - "bitflags", + "bitflags 1.3.2", "bytes", "crossbeam-channel", "futures-core", @@ -28,19 +28,19 @@ dependencies = [ [[package]] name = "actix-codec" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" +checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" dependencies = [ - "bitflags", + "bitflags 1.3.2", "bytes", "futures-core", "futures-sink", - "log", "memchr", "pin-project-lite", "tokio", "tokio-util", + "tracing", ] [[package]] @@ -69,8 +69,8 @@ dependencies = [ "actix-service", "actix-utils", "ahash 0.8.3", - "base64 0.21.0", - "bitflags", + "base64 0.21.2", + "bitflags 1.3.2", "brotli", "bytes", "bytestring", @@ -104,7 +104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" dependencies = [ "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -138,11 +138,11 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ec592f234db8a253cf80531246a4407c8a70530423eea80688a6c5a44a110e7" dependencies = [ - "darling", + "darling 0.14.4", "parse-size", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -245,7 +245,7 @@ dependencies = [ "serde_urlencoded", "smallvec", "socket2", - "time 0.3.20", + "time 0.3.22", "url", ] @@ -258,7 +258,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -269,14 +269,14 @@ checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "addr2line" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" dependencies = [ "gimli", ] @@ -289,14 +289,13 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aes" -version = "0.7.5" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if 1.0.0", "cipher", "cpufeatures", - "opaque-debug", ] [[package]] @@ -324,9 +323,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] @@ -346,6 +345,18 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56fc6cf8dc8c4158eed8649f9b8b0ea1518eb62b544fe9490d66fa0b349eafe9" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -363,9 +374,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "arrayvec" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "as-slice" @@ -375,7 +386,7 @@ checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0" dependencies = [ "generic-array 0.12.4", "generic-array 0.13.3", - "generic-array 0.14.6", + "generic-array 0.14.7", "stable_deref_trait", ] @@ -392,13 +403,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.66" +version = "0.1.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84f9ebcc6c1f5b8cb160f6990096a5c127f423fcb6e1ccc46c370cbdfb75dfc" +checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.23", ] [[package]] @@ -412,18 +423,16 @@ dependencies = [ [[package]] name = "attohttpc" -version = "0.19.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262c3f7f5d61249d8c00e5546e2685cd15ebeeb1bc0f3cc5449350a1cb07319e" +checksum = "1fcf00bc6d5abb29b5f97e3c61a90b6d3caa12f3faf897d4a3e3607c050a35a7" dependencies = [ "http", "log", "native-tls", - "openssl", "serde", "serde_json", "url", - "wildmatch", ] [[package]] @@ -434,34 +443,35 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "aws-creds" -version = "0.30.0" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeeee1a5defa63cba39097a510dfe63ef53658fc8995202a610f6a8a4d03639" +checksum = "3776743bb68d4ad02ba30ba8f64373f1be4e082fe47651767171ce75bb2f6cf5" dependencies = [ "attohttpc", "dirs", + "log", + "quick-xml 0.26.0", "rust-ini", "serde", - "serde-xml-rs", "thiserror", - "time 0.3.20", + "time 0.3.22", "url", ] [[package]] name = "aws-region" -version = "0.25.1" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f92a8af5850d0ea0916ca3e015ab86951ded0bf4b70fd27896e81ae1dfb0af37" +checksum = "056557a61427d0e5ba29dd931031c8ffed4ee7a550e7cd55692a9d8deb0a9dba" dependencies = [ "thiserror", ] [[package]] name = "backtrace" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" dependencies = [ "addr2line", "cc", @@ -480,9 +490,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" [[package]] name = "base64ct" @@ -511,13 +521,31 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.7", ] [[package]] @@ -526,14 +554,14 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.7", ] [[package]] name = "borsh" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40f9ca3698b2e4cb7c15571db0abc5551dca417a21ae8140460b50309bb2cc62" +checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", "hashbrown 0.13.2", @@ -541,37 +569,37 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598b3eacc6db9c3ee57b22707ad8f6a8d2f6d442bfe24ffeb8cbb70ca59e6a35" +checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" dependencies = [ "borsh-derive-internal", "borsh-schema-derive-internal", "proc-macro-crate", "proc-macro2", - "syn", + "syn 1.0.109", ] [[package]] name = "borsh-derive-internal" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186b734fa1c9f6743e90c95d7233c9faab6360d1a96d4ffa19d9cfd1e9350f8a" +checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "borsh-schema-derive-internal" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b7ff1008316626f485991b960ade129253d4034014616b94f309a15366cc49" +checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -610,15 +638,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bytecheck" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13fe11640a23eb24562225322cd3e452b93a3d4091d62fab69c70542fcd17d1f" +checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" dependencies = [ "bytecheck_derive", "ptr_meta", @@ -627,13 +655,13 @@ dependencies = [ [[package]] name = "bytecheck_derive" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31225543cb46f81a7e224762764f4a6a0f097b1db0b175f69e8065efaa42de5" +checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -722,13 +750,13 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", "serde", "time 0.1.45", @@ -738,21 +766,12 @@ dependencies = [ [[package]] name = "cipher" -version = "0.3.0" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "generic-array 0.14.6", -] - -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", + "crypto-common", + "inout", ] [[package]] @@ -771,10 +790,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] -name = "concurrent-queue" -version = "2.1.0" +name = "combine" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" dependencies = [ "crossbeam-utils", ] @@ -807,7 +840,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ "percent-encoding", - "time 0.3.20", + "time 0.3.22", "version_check", ] @@ -823,15 +856,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] @@ -862,9 +895,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -883,9 +916,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.14" +version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", "cfg-if 1.0.0", @@ -906,9 +939,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if 1.0.0", ] @@ -925,7 +958,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.7", "typenum", ] @@ -935,7 +968,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.7", "subtle", ] @@ -956,9 +989,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.60+curl-7.88.1" +version = "0.4.63+curl-8.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "717abe2cb465a5da6ce06617388a3980c9a2844196734bec8ccb8e575250f13f" +checksum = "aeb0fef7046022a1e2ad67a004978f0e3cacb9e3123dc62ce768f92197b771dc" dependencies = [ "cc", "libc", @@ -970,58 +1003,24 @@ dependencies = [ "winapi", ] -[[package]] -name = "cxx" -version = "1.0.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a140f260e6f3f79013b8bfc65e7ce630c9ab4388c6a89c71e07226f49487b72" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da6383f459341ea689374bf0a42979739dc421874f112ff26f829b8040b8e613" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90201c1a650e95ccff1c8c0bb5a343213bdd317c6e600a93075bca2eff54ec97" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b75aed41bb2e6367cae39e6326ef817a851db13c13e4f3263714ca3cfb8de56" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "darling" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944" +dependencies = [ + "darling_core 0.20.1", + "darling_macro 0.20.1", ] [[package]] @@ -1035,7 +1034,21 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 1.0.109", +] + +[[package]] +name = "darling_core" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.23", ] [[package]] @@ -1044,9 +1057,20 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ - "darling_core", + "darling_core 0.14.4", "quote", - "syn", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" +dependencies = [ + "darling_core 0.20.1", + "quote", + "syn 2.0.23", ] [[package]] @@ -1059,7 +1083,39 @@ dependencies = [ "hashbrown 0.12.3", "lock_api", "once_cell", - "parking_lot_core 0.9.7", + "parking_lot_core 0.9.8", +] + +[[package]] +name = "deadpool" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "retain_mut", + "tokio", +] + +[[package]] +name = "deadpool-redis" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f1760f60ffc6653b4afd924c5792098d8c00d9a3deb6b3d989eac17949dc422" +dependencies = [ + "deadpool", + "redis", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaa37046cc0f6c3cc6090fbdbf73ef0b8ef4cfcc37f6befc0020f63e8cf121e1" +dependencies = [ + "tokio", ] [[package]] @@ -1069,7 +1125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" dependencies = [ "serde", - "uuid 1.3.0", + "uuid 1.4.0", ] [[package]] @@ -1082,7 +1138,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.0", - "syn", + "syn 1.0.109", ] [[package]] @@ -1091,14 +1147,14 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.7", ] [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "crypto-common", @@ -1133,9 +1189,9 @@ checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" [[package]] name = "dotenvy" -version = "0.15.6" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "either" @@ -1170,13 +1226,13 @@ dependencies = [ [[package]] name = "errno" -version = "0.2.8" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "winapi", + "windows-sys", ] [[package]] @@ -1197,9 +1253,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "exr" -version = "1.6.3" +version = "1.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd2162b720141a91a054640662d3edce3d50a944a50ffca5313cd951abb35b4" +checksum = "85a7b44a196573e272e0cf0bcf130281c71e9a0c67062954b3323fd364bfdac9" dependencies = [ "bit_field", "flume", @@ -1220,6 +1276,15 @@ dependencies = [ "instant", ] +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + [[package]] name = "findshlibs" version = "0.10.2" @@ -1234,9 +1299,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", "miniz_oxide", @@ -1252,7 +1317,7 @@ dependencies = [ "futures-sink", "nanorand", "pin-project", - "spin 0.9.5", + "spin 0.9.8", ] [[package]] @@ -1278,18 +1343,24 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] [[package]] -name = "futures" -version = "0.3.27" +name = "funty" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -1302,9 +1373,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", @@ -1312,15 +1383,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -1340,15 +1411,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-lite" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ "fastrand", "futures-core", @@ -1361,26 +1432,26 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.23", ] [[package]] name = "futures-sink" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-timer" @@ -1390,9 +1461,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", @@ -1426,9 +1497,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -1436,9 +1507,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -1449,9 +1520,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.11.4" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" dependencies = [ "color_quant", "weezl", @@ -1459,15 +1530,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] name = "h2" -version = "0.3.16" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" dependencies = [ "bytes", "fnv", @@ -1519,12 +1590,22 @@ dependencies = [ ] [[package]] -name = "hashlink" -version = "0.8.1" +name = "hashbrown" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" dependencies = [ - "hashbrown 0.12.3", + "ahash 0.8.3", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" +dependencies = [ + "hashbrown 0.14.0", ] [[package]] @@ -1550,18 +1631,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.2.6" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "hex" @@ -1594,7 +1666,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -1650,9 +1722,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.25" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", @@ -1687,26 +1759,25 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi", + "windows", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] @@ -1717,20 +1788,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.2.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "idna" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1744,9 +1804,9 @@ checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" [[package]] name = "image" -version = "0.24.5" +version = "0.24.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" +checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" dependencies = [ "bytemuck", "byteorder", @@ -1757,21 +1817,30 @@ dependencies = [ "num-rational", "num-traits", "png", - "scoped_threadpool", + "qoi", "tiff", ] [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", "serde", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array 0.14.7", +] + [[package]] name = "instant" version = "0.1.12" @@ -1783,30 +1852,30 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.6" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ + "hermit-abi", "libc", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] name = "ipnet" -version = "2.7.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" [[package]] name = "is-terminal" -version = "0.4.4" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", - "windows-sys 0.45.0", + "hermit-abi", + "rustix 0.38.3", + "windows-sys", ] [[package]] @@ -1842,16 +1911,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b51dd97fa24074214b9eb14da518957573f4dec3189112610ae1ccec9ac464" dependencies = [ - "nom 5.1.2", -] - -[[package]] -name = "itertools" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" -dependencies = [ - "either", + "nom 5.1.3", ] [[package]] @@ -1864,10 +1924,19 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.6" +name = "itertools" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" [[package]] name = "jobserver" @@ -1889,20 +1958,20 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] [[package]] name = "jsonwebtoken" -version = "8.2.0" +version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f4f04699947111ec1733e71778d763555737579e44b85844cae8e1940a1828" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ - "base64 0.13.1", + "base64 0.21.2", "ring", "serde", "serde_json", @@ -1918,13 +1987,14 @@ dependencies = [ "actix-rt", "actix-web", "async-trait", - "base64 0.21.0", - "bitflags", + "base64 0.21.2", + "bitflags 1.3.2", "bytes", "censor", "chrono", "color-thief", "dashmap", + "deadpool-redis", "dotenvy", "env_logger", "futures", @@ -1932,11 +2002,13 @@ dependencies = [ "hex", "hmac 0.11.0", "image", - "itertools 0.10.5", + "itertools 0.11.0", "lazy_static", "log", "meilisearch-sdk", "rand", + "rand_chacha", + "redis", "regex", "reqwest", "rust-s3", @@ -1956,6 +2028,7 @@ dependencies = [ "url", "urlencoding", "validator", + "woothee", "xml-rs", "yaserde", "yaserde_derive", @@ -1987,7 +2060,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" dependencies = [ "arrayvec 0.5.2", - "bitflags", + "bitflags 1.3.2", "cfg-if 1.0.0", "ryu", "static_assertions", @@ -1995,9 +2068,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.140" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libnghttp2-sys" @@ -2011,9 +2084,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" dependencies = [ "cc", "libc", @@ -2021,15 +2094,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "link-cplusplus" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2038,9 +2102,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" [[package]] name = "local-channel" @@ -2062,9 +2132,9 @@ checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -2072,12 +2142,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if 1.0.0", -] +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "lru-cache" @@ -2103,12 +2170,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - [[package]] name = "maybe-async" version = "0.2.7" @@ -2117,7 +2178,7 @@ checksum = "0f1b8c13cb1f814b634a96b2c725449fe7ed464a7b8781de8688be5ffbd3f305" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -2126,7 +2187,7 @@ version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -2137,25 +2198,26 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "meilisearch-index-setting-macro" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36007bfec8990e7b77d32706a21d9118a437c3800b4ebe3fc04749fcd20dbd70" +checksum = "c9fedd7e2fabfbcc91679f3d76f6d648ea7fc9ea87c841b10d26c2a258f408da" dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "meilisearch-sdk" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534120322f7ac79114569ed86978840794fa86ad2c22fcbdb8adf392efcafd50" +checksum = "8e6be928c91e1b23689725586b56f3284f394d93185accfa2771caec3e10015d" dependencies = [ "async-trait", "either", "futures", + "futures-io", "isahc", "iso8601-duration", "js-sys", @@ -2165,8 +2227,8 @@ dependencies = [ "serde", "serde_json", "thiserror", - "time 0.3.20", - "uuid 1.3.0", + "time 0.3.22", + "uuid 1.4.0", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -2190,18 +2252,18 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" @@ -2215,9 +2277,9 @@ dependencies = [ [[package]] name = "minidom" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9ce45d459e358790a285e7609ff5ae4cfab88b75f237e8838e62029dda397b" +checksum = "f45614075738ce1b77a1768912a60c0227525971b03e09122a05b8a34a2a6278" dependencies = [ "rxml", ] @@ -2230,23 +2292,24 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", + "simd-adler32", ] [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -2282,7 +2345,7 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cc", "cfg-if 0.1.10", "libc", @@ -2295,7 +2358,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if 1.0.0", "libc", "static_assertions", @@ -2309,9 +2372,9 @@ checksum = "cf51a729ecf40266a2368ad335a5fdde43471f545a967109cd62146ecf8b66ff" [[package]] name = "nom" -version = "5.1.2" +version = "5.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" dependencies = [ "lexical-core", "memchr", @@ -2371,28 +2434,28 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi", "libc", ] [[package]] name = "object" -version = "0.30.3" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "oncemutex" @@ -2408,11 +2471,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.45" +version = "0.10.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if 1.0.0", "foreign-types", "libc", @@ -2423,13 +2486,13 @@ dependencies = [ [[package]] name = "openssl-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.23", ] [[package]] @@ -2440,11 +2503,10 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.80" +version = "0.9.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" dependencies = [ - "autocfg", "cc", "libc", "pkg-config", @@ -2463,9 +2525,9 @@ dependencies = [ [[package]] name = "os_info" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c424bc68d15e0778838ac013b5b3449544d8133633d8016319e7e05a820b8c0" +checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" dependencies = [ "log", "serde", @@ -2478,7 +2540,7 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49dfc200733ac34dcd9a1e4a7e454b521723936010bef3710e2d8024a32d685f" dependencies = [ - "bitflags", + "bitflags 1.3.2", "heapless", "lazy_static", "libc", @@ -2491,9 +2553,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" [[package]] name = "parking_lot" @@ -2513,7 +2575,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.7", + "parking_lot_core 0.9.8", ] [[package]] @@ -2525,22 +2587,22 @@ dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "winapi", ] [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "smallvec", - "windows-sys 0.45.0", + "windows-targets", ] [[package]] @@ -2562,9 +2624,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35" [[package]] name = "pbkdf2" @@ -2572,31 +2634,31 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "hmac 0.12.1", "password-hash", - "sha2 0.10.6", + "sha2 0.10.7", ] [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "phonenumber" -version = "0.3.1+8.12.9" +version = "0.3.2+8.13.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261a014e5f5e048bf2c6f1a72fa5e4c223009dc5f296a385b95fe19b464608f" +checksum = "34749f64ea9d76f10cdc8a859588b57775f59177c7dd91f744d620bd62982d6f" dependencies = [ "bincode", "either", "fnv", - "itertools 0.9.0", + "itertools 0.10.5", "lazy_static", - "nom 5.1.2", - "quick-xml", + "nom 7.1.3", + "quick-xml 0.28.2", "regex", "regex-cache", "serde", @@ -2606,29 +2668,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.12" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.12" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.23", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" [[package]] name = "pin-utils" @@ -2638,36 +2700,37 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "png" -version = "0.17.7" +version = "0.17.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" +checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crc32fast", + "fdeflate", "flate2", "miniz_oxide", ] [[package]] name = "polling" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e1f879b2998099c2d69ab9605d145d5b661195627eccc680002c4918a7fb6fa" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", - "bitflags", + "bitflags 1.3.2", "cfg-if 1.0.0", "concurrent-queue", "libc", "log", "pin-project-lite", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -2714,7 +2777,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "version_check", ] @@ -2731,9 +2794,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.52" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" dependencies = [ "unicode-ident", ] @@ -2767,27 +2830,63 @@ checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", ] [[package]] name = "quick-xml" -version = "0.18.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cc440ee4802a86e357165021e3e255a9143724da31db1e2ea540214c96a0f82" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quick-xml" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.24" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50686e0021c4136d1d453b2dfe059902278681512a34d4248435dc34b6b5c8ec" +checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" dependencies = [ "proc-macro2", ] +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot 0.12.1", + "scheduled-thread-pool", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -2840,13 +2939,44 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "redis" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea8c51b5dc1d8e5fd3350ec8167f464ec0995e79f2e90a075b63371500d557f" +dependencies = [ + "ahash 0.7.6", + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "r2d2", + "ryu", + "sha1_smol", + "tokio", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", ] [[package]] @@ -2856,19 +2986,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom", - "redox_syscall", + "redox_syscall 0.2.16", "thiserror", ] [[package]] name = "regex" -version = "1.7.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-automata", + "regex-syntax 0.7.3", +] + +[[package]] +name = "regex-automata" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9aaecc05d5c4b5f7da074b9a0d1a0867e71fd36e7fc0482d8bcfe8e8fc56290" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.3", ] [[package]] @@ -2880,14 +3022,20 @@ dependencies = [ "lru-cache", "oncemutex", "regex", - "regex-syntax", + "regex-syntax 0.6.29", ] [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846" [[package]] name = "rend" @@ -2900,11 +3048,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.14" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ - "base64 0.21.0", + "base64 0.21.2", "bytes", "encoding_rs", "futures-core", @@ -2938,6 +3086,12 @@ dependencies = [ "winreg", ] +[[package]] +name = "retain_mut" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" + [[package]] name = "rgb" version = "0.8.36" @@ -2964,27 +3118,30 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.40" +version = "0.7.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c30f1d45d9aa61cbc8cd1eb87705470892289bb2d01943e7803b873a57404dc3" +checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" dependencies = [ + "bitvec", "bytecheck", "hashbrown 0.12.3", "ptr_meta", "rend", "rkyv_derive", "seahash", + "tinyvec", + "uuid 1.4.0", ] [[package]] name = "rkyv_derive" -version = "0.7.40" +version = "0.7.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff26ed6c7c4dfc2aa9480b86a60e3c7233543a270a680e10758a507c5a4ce476" +checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -2999,15 +3156,17 @@ dependencies = [ [[package]] name = "rust-s3" -version = "0.32.3" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6009d9d4cf910505534d62d380a0aa305805a2af0b5c3ad59a3024a0715b847" +checksum = "1b2ac5ff6acfbe74226fa701b5ef793aaa054055c13ebb7060ad36942956e027" dependencies = [ "async-trait", "aws-creds", "aws-region", "base64 0.13.1", + "bytes", "cfg-if 1.0.0", + "futures", "hex", "hmac 0.12.1", "http", @@ -3016,13 +3175,13 @@ dependencies = [ "md5", "minidom", "percent-encoding", + "quick-xml 0.26.0", "reqwest", "serde", - "serde-xml-rs", "serde_derive", - "sha2 0.10.6", + "sha2 0.10.7", "thiserror", - "time 0.3.20", + "time 0.3.22", "tokio", "tokio-stream", "url", @@ -3030,11 +3189,11 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.29.0" +version = "1.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b1b21b8760b0ef8ae5b43d40913ff711a2053cb7ff892a34facff7a6365375a" +checksum = "d0446843641c69436765a35a5a77088e28c2e6a12da93e84aa3ab1cd4aa5a042" dependencies = [ - "arrayvec 0.7.2", + "arrayvec 0.7.4", "borsh", "bytecheck", "byteorder", @@ -3048,9 +3207,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.21" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc_version" @@ -3082,16 +3241,29 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.9" +version = "0.37.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", - "windows-sys 0.45.0", + "linux-raw-sys 0.3.8", + "windows-sys", +] + +[[package]] +name = "rustix" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys 0.4.3", + "windows-sys", ] [[package]] @@ -3108,52 +3280,53 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.0", + "base64 0.21.2", ] [[package]] name = "rxml" -version = "0.8.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a071866b8c681dc2cfffa77184adc32b57b0caad4e620b6292609703bceb804" +checksum = "a98f186c7a2f3abbffb802984b7f1dfd65dac8be1aafdaabbca4137f53f0dff7" dependencies = [ "bytes", - "pin-project-lite", "rxml_validation", "smartstring", - "tokio", ] [[package]] name = "rxml_validation" -version = "0.8.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53bc79743f9a66c2fb1f951cd83735f275d46bfe466259fbc5897bb60a0d00ee" +checksum = "22a197350ece202f19a166d1ad6d9d6de145e1d2a8ef47db299abe164dbd7530" [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" [[package]] name = "schannel" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys 0.42.0", + "windows-sys", ] [[package]] -name = "scoped_threadpool" -version = "0.1.9" +name = "scheduled-thread-pool" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot 0.12.1", +] [[package]] name = "scopeguard" @@ -3161,12 +3334,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "scratch" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" - [[package]] name = "sct" version = "0.7.0" @@ -3185,11 +3352,11 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "2.8.2" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -3198,9 +3365,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" dependencies = [ "core-foundation-sys", "libc", @@ -3229,9 +3396,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "sentry" -version = "0.30.0" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5ce6d3512e2617c209ec1e86b0ca2fea06454cd34653c91092bf0f3ec41f8e3" +checksum = "01b0ad16faa5d12372f914ed40d00bda21a6d1bdcc99264c5e5e1c9495cf3654" dependencies = [ "httpdate", "native-tls", @@ -3241,15 +3408,16 @@ dependencies = [ "sentry-core", "sentry-debug-images", "sentry-panic", + "sentry-tracing", "tokio", "ureq", ] [[package]] name = "sentry-actix" -version = "0.30.0" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c210068218470670c03fbe59f53944061f9b1fcdb7748f9326248ab1ddf56238" +checksum = "e9f81ee4dfb8e109fd99f0eb036fec548e66fd1db17a0224304c4a31ab0749ef" dependencies = [ "actix-web", "futures-util", @@ -3258,9 +3426,9 @@ dependencies = [ [[package]] name = "sentry-backtrace" -version = "0.30.0" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7fe408d4d1f8de188a9309916e02e129cbe51ca19e55badea5a64899399b1a" +checksum = "11f2ee8f147bb5f22ac59b5c35754a759b9a6f6722402e2a14750b2a63fc59bd" dependencies = [ "backtrace", "once_cell", @@ -3270,9 +3438,9 @@ dependencies = [ [[package]] name = "sentry-contexts" -version = "0.30.0" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5695096a059a89973ec541062d331ff4c9aeef9c2951416c894f0fff76340e7d" +checksum = "dcd133362c745151eeba0ac61e3ba8350f034e9fe7509877d08059fe1d7720c6" dependencies = [ "hostname", "libc", @@ -3284,9 +3452,9 @@ dependencies = [ [[package]] name = "sentry-core" -version = "0.30.0" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b22828bfd118a7b660cf7a155002a494755c0424cebb7061e4743ecde9c7dbc" +checksum = "7163491708804a74446642ff2c80b3acd668d4b9e9f497f85621f3d250fd012b" dependencies = [ "build_id", "findshlibs", @@ -3300,14 +3468,14 @@ dependencies = [ "serde", "serde_json", "sys-info", - "uuid 1.3.0", + "uuid 1.4.0", ] [[package]] name = "sentry-debug-images" -version = "0.30.0" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9164d44a2929b1b7670afd7e87552514b70d3ae672ca52884639373d912a3d" +checksum = "6a5003d7ff08aa3b2b76994080b183e8cfa06c083e280737c9cee02ca1c70f5e" dependencies = [ "findshlibs", "once_cell", @@ -3316,19 +3484,31 @@ dependencies = [ [[package]] name = "sentry-panic" -version = "0.30.0" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4ced2a7a8c14899d58eec402d946f69d5ed26a3fc363a7e8b1e5cb88473a01" +checksum = "c4dfe8371c9b2e126a8b64f6fefa54cef716ff2a50e63b5558a48b899265bccd" dependencies = [ "sentry-backtrace", "sentry-core", ] [[package]] -name = "sentry-types" -version = "0.30.0" +name = "sentry-tracing" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360ee3270f7a4a1eee6c667f7d38360b995431598a73b740dfe420da548d9cc9" +checksum = "5aca8b88978677a27ee1a91beafe4052306c474c06f582321fde72d2e2cc2f7f" +dependencies = [ + "sentry-backtrace", + "sentry-core", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "sentry-types" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e7a88e0c1922d19b3efee12a8215f6a8a806e442e665ada71cc222cab72985f" dependencies = [ "debugid", "getrandom", @@ -3336,48 +3516,36 @@ dependencies = [ "serde", "serde_json", "thiserror", - "time 0.3.20", + "time 0.3.22", "url", - "uuid 1.3.0", + "uuid 1.4.0", ] [[package]] name = "serde" -version = "1.0.155" +version = "1.0.167" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71f2b4817415c6d4210bfe1c7bfcf4801b2d904cb4d0e1a8fdb651013c9e86b8" +checksum = "7daf513456463b42aa1d94cff7e0c24d682b429f020b9afa4f5ba5c40a22b237" dependencies = [ "serde_derive", ] -[[package]] -name = "serde-xml-rs" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65162e9059be2f6a3421ebbb4fef3e74b7d9e7c60c50a0e292c6239f19f1edfa" -dependencies = [ - "log", - "serde", - "thiserror", - "xml-rs", -] - [[package]] name = "serde_derive" -version = "1.0.155" +version = "1.0.167" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d071a94a3fac4aff69d023a7f411e33f40f3483f8c5190b1953822b6b76d7630" +checksum = "b69b106b68bc8054f0e974e70d19984040f8a5cf9215ca82626ea4853f82c4b9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.23", ] [[package]] name = "serde_json" -version = "1.0.94" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" dependencies = [ "itoa", "ryu", @@ -3407,30 +3575,30 @@ dependencies = [ [[package]] name = "serde_with" -version = "2.3.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85456ffac572dc8826334164f2fb6fb40a7c766aebe195a2a21ee69ee2885ecf" +checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513" dependencies = [ - "base64 0.13.1", + "base64 0.21.2", "chrono", "hex", "indexmap", "serde", "serde_json", "serde_with_macros", - "time 0.3.20", + "time 0.3.22", ] [[package]] name = "serde_with_macros" -version = "2.3.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cbcd6104f8a4ab6af7f6be2a0da6be86b9de3c401f6e86bb856ab2af739232f" +checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070" dependencies = [ - "darling", + "darling 0.20.1", "proc-macro2", "quote", - "syn", + "syn 2.0.23", ] [[package]] @@ -3450,7 +3618,7 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -3474,13 +3642,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -3494,9 +3662,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14a5df39617d7c8558154693a1bb8157a4aab8179209540cc0b10e5dc24e0b18" +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" [[package]] name = "simdutf8" @@ -3526,17 +3694,19 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "smartstring" -version = "0.2.10" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e714dff2b33f2321fdcd475b71cec79781a692d846f37f415fb395a1d2bcd48e" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" dependencies = [ + "autocfg", "static_assertions", + "version_check", ] [[package]] @@ -3551,9 +3721,9 @@ dependencies = [ [[package]] name = "spdx" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dd48832ddda0d79ca6062064d530680e24c5ee85ba1d9fae41f102b2d9f34f" +checksum = "2971cb691ca629f46174f73b1f95356c5617f89b4167f04107167c3dccb8dd89" dependencies = [ "smallvec", ] @@ -3566,9 +3736,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" -version = "0.9.5" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dccf47db1b41fa1573ed27ccf5e08e3ca771cb994f776668c5ebda893b248fc" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" dependencies = [ "lock_api", ] @@ -3586,9 +3756,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9249290c05928352f71c077cc44a464d880c63f26f7534728cca008e135c0428" +checksum = "f8de3b03a925878ed54a954f621e64bf55a3c1bd29652d0d1a17830405350188" dependencies = [ "sqlx-core", "sqlx-macros", @@ -3596,14 +3766,14 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105" +checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" dependencies = [ "ahash 0.7.6", "atoi", "base64 0.13.1", - "bitflags", + "bitflags 1.3.2", "byteorder", "bytes", "chrono", @@ -3638,7 +3808,7 @@ dependencies = [ "serde", "serde_json", "sha1 0.10.5", - "sha2 0.10.6", + "sha2 0.10.7", "smallvec", "sqlformat", "sqlx-rt", @@ -3652,9 +3822,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b850fa514dc11f2ee85be9d055c512aa866746adfacd1cb42d867d68e6a5b0d9" +checksum = "9966e64ae989e7e575b19d7265cb79d7fc3cbbdf179835cb0d716f294c2049c9" dependencies = [ "dotenvy", "either", @@ -3665,18 +3835,18 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2 0.10.6", + "sha2 0.10.7", "sqlx-core", "sqlx-rt", - "syn", + "syn 1.0.109", "url", ] [[package]] name = "sqlx-rt" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24c5b2d25fa654cc5f841750b8e1cdedbe21189bf9a9382ee90bfa9dd3562396" +checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" dependencies = [ "once_cell", "tokio", @@ -3726,7 +3896,7 @@ dependencies = [ "debugid", "memmap2", "stable_deref_trait", - "uuid 1.3.0", + "uuid 1.4.0", ] [[package]] @@ -3750,6 +3920,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sys-info" version = "0.9.1" @@ -3761,16 +3942,23 @@ dependencies = [ ] [[package]] -name = "tempfile" -version = "3.4.0" +name = "tap" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ + "autocfg", "cfg-if 1.0.0", "fastrand", - "redox_syscall", - "rustix", - "windows-sys 0.42.0", + "redox_syscall 0.3.5", + "rustix 0.37.23", + "windows-sys", ] [[package]] @@ -3784,22 +3972,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.39" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" +checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.39" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" +checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.23", ] [[package]] @@ -3826,9 +4014,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" dependencies = [ "itoa", "serde", @@ -3838,15 +4026,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" dependencies = [ "time-core", ] @@ -3868,21 +4056,21 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.26.0" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ "autocfg", + "backtrace", "bytes", "libc", - "memchr", "mio", "num_cpus", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -3908,9 +4096,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", @@ -3919,9 +4107,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", @@ -3961,22 +4149,23 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.23", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", + "valuable", ] [[package]] @@ -3989,6 +4178,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "tracing-core", +] + [[package]] name = "try-lock" version = "0.2.4" @@ -4031,15 +4229,15 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.11" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524b68aca1d05e03fdf03fcdce2c6c94b6daf6d16861ddaa7e4f2b6638a9052c" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" [[package]] name = "unicode-normalization" @@ -4056,12 +4254,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - [[package]] name = "unicode_categories" version = "0.1.1" @@ -4076,11 +4268,11 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "ureq" -version = "2.6.2" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d" +checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9" dependencies = [ - "base64 0.13.1", + "base64 0.21.2", "log", "native-tls", "once_cell", @@ -4089,12 +4281,12 @@ dependencies = [ [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", - "idna 0.3.0", + "idna", "percent-encoding", "serde", ] @@ -4113,9 +4305,9 @@ checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" [[package]] name = "uuid" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" dependencies = [ "getrandom", "serde", @@ -4123,11 +4315,11 @@ dependencies = [ [[package]] name = "validator" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ad5bf234c7d3ad1042e5252b7eddb2c4669ee23f32c7dd0e9b7705f07ef591" +checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" dependencies = [ - "idna 0.2.3", + "idna", "lazy_static", "phonenumber", "regex", @@ -4150,7 +4342,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn", + "syn 1.0.109", "validator_types", ] @@ -4161,9 +4353,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" dependencies = [ "proc-macro2", - "syn", + "syn 1.0.109", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -4190,11 +4388,10 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -4212,9 +4409,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -4222,24 +4419,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.23", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.34" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -4249,9 +4446,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4259,22 +4456,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.23", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-streams" @@ -4291,9 +4488,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", @@ -4326,20 +4523,14 @@ checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" [[package]] name = "whoami" -version = "1.3.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45dbc71f0cdca27dc261a9bd37ddec174e4a0af2b900b890f378460f745426e3" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" dependencies = [ "wasm-bindgen", "web-sys", ] -[[package]] -name = "wildmatch" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee583bdc5ff1cf9db20e9db5bb3ff4c3089a8f6b8b31aff265c9aba85812db86" - [[package]] name = "winapi" version = "0.3.9" @@ -4372,34 +4563,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.42.0" +name = "windows" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows-targets", ] [[package]] name = "windows-sys" -version = "0.45.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" -version = "0.42.1" +version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -4412,45 +4597,45 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winreg" @@ -4462,10 +4647,29 @@ dependencies = [ ] [[package]] -name = "xml-rs" -version = "0.8.4" +name = "woothee" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" +checksum = "896174c6a4779d4d7d4523dd27aef7d46609eda2497e370f6c998325c6bf6971" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xml-rs" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a56c84a8ccd4258aed21c92f70c0f6dea75356b6892ae27c24139da456f9336" [[package]] name = "yaserde" @@ -4487,7 +4691,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "xml-rs", ] @@ -4503,9 +4707,9 @@ dependencies = [ [[package]] name = "zip" -version = "0.6.4" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0445d0fbc924bb93539b4316c11afb121ea39296f99a3c4c9edad09e3658cdef" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ "aes", "byteorder", @@ -4517,7 +4721,7 @@ dependencies = [ "hmac 0.12.1", "pbkdf2", "sha1 0.10.5", - "time 0.3.20", + "time 0.3.22", "zstd 0.11.2+zstd.1.5.2", ] @@ -4536,7 +4740,7 @@ version = "0.12.3+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" dependencies = [ - "zstd-safe 6.0.4+zstd.1.5.4", + "zstd-safe 6.0.5+zstd.1.5.4", ] [[package]] @@ -4551,9 +4755,9 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "6.0.4+zstd.1.5.4" +version = "6.0.5+zstd.1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7afb4b54b8910cf5447638cb54bf4e8a65cbedd783af98b98c62ffe91f185543" +checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b" dependencies = [ "libc", "zstd-sys", @@ -4561,9 +4765,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.7+zstd.1.5.4" +version = "2.0.8+zstd.1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94509c3ba2fe55294d752b79842c530ccfab760192521df74a081a78d2b3c7f5" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" dependencies = [ "cc", "libc", @@ -4572,9 +4776,9 @@ dependencies = [ [[package]] name = "zune-inflate" -version = "0.2.51" +version = "0.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01728b79fb9b7e28a8c11f715e1cd8dc2cda7416a007d66cac55cebb3a8ac6b" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" dependencies = [ "simd-adler32", ] diff --git a/Cargo.toml b/Cargo.toml index cc801657e..dd5b2d010 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,63 +12,68 @@ path = "src/main.rs" [dependencies] actix = "0.13.0" -actix-web = "4.3.0" +actix-web = "4.3.1" actix-rt = "2.8.0" actix-multipart = "0.6.0" actix-cors = "0.6.4" -tokio = { version = "1.25.0", features = ["sync"] } -tokio-stream = "0.1.11" +tokio = { version = "1.29.1", features = ["sync"] } +tokio-stream = "0.1.14" -futures = "0.3.26" +futures = "0.3.28" futures-timer = "3.0.2" -async-trait = "0.1.64" +async-trait = "0.1.70" dashmap = "5.4.0" lazy_static = "1.4.0" meilisearch-sdk = "0.22.0" -rust-s3 = "0.32.3" -reqwest = { version = "0.11.14", features = ["json", "multipart"] } +rust-s3 = "0.33.0" +reqwest = { version = "0.11.18", features = ["json", "multipart"] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -serde_with = "2.2.0" -chrono = { version = "0.4.23", features = ["serde"]} +serde_with = "3.0.0" +chrono = { version = "0.4.26", features = ["serde"]} yaserde = "0.8.0" yaserde_derive = "0.8.0" -xml-rs = "0.8.4" +xml-rs = "0.8.15" rand = "0.8.5" +rand_chacha = "0.3.1" bytes = "1.4.0" -base64 = "0.21.0" +base64 = "0.21.2" sha1 = { version = "0.6.1", features = ["std"] } sha2 = "0.9.9" hmac = "0.11.0" bitflags = "1.3.2" hex = "0.4.3" -url = "2.3.1" +url = "2.4.0" urlencoding = "2.1.2" -zip = "0.6.4" +zip = "0.6.6" -itertools = "0.10.5" +itertools = "0.11.0" -validator = { version = "0.16.0", features = ["derive", "phone"] } -regex = "1.7.1" +validator = { version = "0.16.1", features = ["derive", "phone"] } +regex = "1.8.4" censor = "0.3.0" -spdx = { version = "0.10.0", features = ["text"] } +spdx = { version = "0.10.1", features = ["text"] } -dotenvy = "0.15.6" -log = "0.4.17" +dotenvy = "0.15.7" +log = "0.4.19" env_logger = "0.10.0" -thiserror = "1.0.38" +thiserror = "1.0.41" -sqlx = { version = "0.6.2", features = ["runtime-actix-rustls", "postgres", "chrono", "offline", "macros", "migrate", "decimal", "json"] } -rust_decimal = { version = "1.28.1", features = ["serde-with-float", "serde-with-str"] } +sqlx = { version = "0.6.3", features = ["offline", "runtime-tokio-rustls", "postgres", "chrono", "macros", "migrate", "decimal", "json"] } +rust_decimal = { version = "1.30.0", features = ["serde-with-float", "serde-with-str"] } +redis = { version = "0.23.0", features = ["tokio-comp", "ahash", "r2d2"]} +deadpool-redis = "0.12.0" -sentry = { version = "0.30.0", features = ["profiling"] } -sentry-actix = "0.30.0" +sentry = { version = "0.31.5", features = ["profiling"] } +sentry-actix = "0.31.5" -image = "0.24.5" +image = "0.24.6" color-thief = "0.2.2" + +woothee = "0.13.0" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index f65436098..c88a72bc6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,14 @@ services: - meilisearch-data:/meili_data environment: MEILI_MASTER_KEY: modrinth + redis: + image: redis:alpine + restart: on-failure + ports: + - '6379:6379' + volumes: + - redis-data:/data volumes: meilisearch-data: db-data: + redis-data: \ No newline at end of file diff --git a/migrations/20230628180115_kill-ory.sql b/migrations/20230628180115_kill-ory.sql new file mode 100644 index 000000000..920ab9e1a --- /dev/null +++ b/migrations/20230628180115_kill-ory.sql @@ -0,0 +1,48 @@ +ALTER TABLE users DROP COLUMN kratos_id; + +ALTER TABLE states ADD COLUMN provider varchar(64) NOT NULL default 'github'; + +ALTER TABLE users ADD COLUMN discord_id bigint; +ALTER TABLE users ADD COLUMN gitlab_id bigint; +ALTER TABLE users ADD COLUMN google_id varchar(256); +ALTER TABLE users ADD COLUMN steam_id bigint; +ALTER TABLE users ADD COLUMN microsoft_id varchar(256); + +CREATE INDEX users_discord_id + ON users (discord_id); +CREATE INDEX users_gitlab_id + ON users (gitlab_id); +CREATE INDEX users_google_id + ON users (google_id); +CREATE INDEX users_steam_id + ON users (steam_id); +CREATE INDEX users_microsoft_id + ON users (microsoft_id); + +ALTER TABLE users ALTER COLUMN avatar_url TYPE varchar(1024); +ALTER TABLE users ADD COLUMN password TEXT NULL; +ALTER TABLE users ADD COLUMN email_verified BOOLEAN NOT NULL DEFAULT FALSE; + +CREATE TABLE sessions ( + id bigint NOT NULL PRIMARY KEY, + session varchar(64) NOT NULL UNIQUE, + user_id BIGINT NOT NULL REFERENCES users(id), + created timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + last_login timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + expires timestamptz DEFAULT CURRENT_TIMESTAMP + interval '14 days' NOT NULL, + refresh_expires timestamptz DEFAULT CURRENT_TIMESTAMP + interval '60 days' NOT NULL, + + city varchar(256) NULL, + country varchar(256) NULL, + ip varchar(512) NOT NULL, + + os varchar(256) NULL, + platform varchar(256) NULL, + user_agent varchar(1024) NOT NULL +); + +CREATE INDEX sessions_user_id + ON sessions (user_id); + +ALTER TABLE mods DROP COLUMN game_versions; +ALTER TABLE mods DROP COLUMN loaders; diff --git a/sqlx-data.json b/sqlx-data.json index ca235420a..06a76b83f 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -12,18 +12,6 @@ }, "query": "\n UPDATE mods\n SET moderation_message = NULL, moderation_message_body = NULL, queued = NOW()\n WHERE (id = $1)\n " }, - "0267d1ea5387d4acfc132aeb4776004a1ebb048e7789e686bfaba3357d392f62": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM mods_donations\n WHERE joining_mod_id = $1\n " - }, "02843e787de72594e186a14734bd02099ca6d2f07dcc06da8d6d8a069638ca2a": { "describe": { "columns": [ @@ -86,18 +74,6 @@ }, "query": "\n SELECT EXISTS(SELECT 1 FROM users WHERE id = $1 AND email IS NULL)\n " }, - "041f499f542ddab1b81bd445d6cabe225b1b2ad3ec7bbc1f755346c016ae06e6": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM reports\n WHERE user_id = $1\n " - }, "04345d9c23430267f755b1420520df91bd403524fd60ba1a94e3a239ea70cae7": { "describe": { "columns": [], @@ -180,6 +156,38 @@ }, "query": "\n INSERT INTO mods_categories (joining_mod_id, joining_category_id, is_additional)\n VALUES ($1, $2, FALSE)\n " }, + "0640761c8c14cfbcd7009bd50185f39fd7f1467ea81ae3d1a2477887075d72d5": { + "describe": { + "columns": [ + { + "name": "url", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "expires", + "ordinal": 1, + "type_info": "Timestamptz" + }, + { + "name": "provider", + "ordinal": 2, + "type_info": "Varchar" + } + ], + "nullable": [ + false, + false, + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT url, expires, provider FROM states\n WHERE id = $1\n " + }, "06a92b638c77276f36185788748191e7731a2cce874ecca4af913d0d0412d223": { "describe": { "columns": [], @@ -193,18 +201,6 @@ }, "query": "\n UPDATE versions\n SET downloads = $1\n WHERE (id = $2)\n " }, - "0794b913ad194908048fa8f303f736f3d437a1699b975691f9d50e8ffc3f5f78": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM historical_payouts\n WHERE user_id = $1\n " - }, "07ebc9dc82cd012cd4f5880b1eb3d82602c195a3e3ddd557103ee037aa6dad1c": { "describe": { "columns": [], @@ -219,6 +215,26 @@ }, "query": "\n INSERT INTO mods_donations (joining_mod_id, joining_platform_id, url)\n VALUES ($1, $2, $3)\n " }, + "08baa3d4e15821d791a1981a6abf653991dcc0901cea49156cd202d10ed2968c": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "SELECT id FROM users WHERE github_id = $1" + }, "08f6bc80d18c171e54dd1db90e15569a02b526d708a9c918c90d79c764cb02fa": { "describe": { "columns": [ @@ -274,6 +290,75 @@ }, "query": "\n UPDATE versions\n SET name = $1\n WHERE (id = $2)\n " }, + "0b79ae3825e05ae07058a0a9d02fb0bd68ce37f3c7cf0356d565c23520988816": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "version_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "mod_id", + "ordinal": 2, + "type_info": "Int8" + }, + { + "name": "url", + "ordinal": 3, + "type_info": "Varchar" + }, + { + "name": "filename", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "is_primary", + "ordinal": 5, + "type_info": "Bool" + }, + { + "name": "size", + "ordinal": 6, + "type_info": "Int4" + }, + { + "name": "file_type", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "hashes", + "ordinal": 8, + "type_info": "Jsonb" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + null + ], + "parameters": { + "Left": [ + "Text", + "ByteaArray" + ] + } + }, + "query": "\n SELECT f.id, f.version_id, v.mod_id, f.url, f.filename, f.is_primary, f.size, f.file_type,\n JSONB_AGG(DISTINCT jsonb_build_object('algorithm', h.algorithm, 'hash', encode(h.hash, 'escape'))) filter (where h.hash is not null) hashes\n FROM files f\n INNER JOIN versions v on v.id = f.version_id\n INNER JOIN hashes h on h.file_id = f.id\n WHERE h.algorithm = $1 AND h.hash = ANY($2)\n GROUP BY f.id, v.mod_id, v.date_published\n ORDER BY v.date_published\n " + }, "0ba5a9f4d1381ed37a67b7dc90edf7e3ec86cae6c2860e5db1e53144d4654e58": { "describe": { "columns": [ @@ -326,224 +411,6 @@ }, "query": "\n UPDATE pats SET\n name = $1,\n expires_at = $2\n WHERE id = $3\n " }, - "0f0244e77f60e69b3ab1320265749656e25da0b021b3df9013a2da470dbc8d46": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n UPDATE payouts_values\n SET mod_id = NULL\n WHERE (mod_id = $1)\n " - }, - "0f256de1470996cc2347dae14ecb3bac81193452e2d6e7adc55ab2c65ad7b569": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "project_type", - "ordinal": 1, - "type_info": "Int4" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "description", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "downloads", - "ordinal": 4, - "type_info": "Int4" - }, - { - "name": "follows", - "ordinal": 5, - "type_info": "Int4" - }, - { - "name": "icon_url", - "ordinal": 6, - "type_info": "Varchar" - }, - { - "name": "body", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "published", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "updated", - "ordinal": 9, - "type_info": "Timestamptz" - }, - { - "name": "approved", - "ordinal": 10, - "type_info": "Timestamptz" - }, - { - "name": "queued", - "ordinal": 11, - "type_info": "Timestamptz" - }, - { - "name": "status", - "ordinal": 12, - "type_info": "Varchar" - }, - { - "name": "requested_status", - "ordinal": 13, - "type_info": "Varchar" - }, - { - "name": "issues_url", - "ordinal": 14, - "type_info": "Varchar" - }, - { - "name": "source_url", - "ordinal": 15, - "type_info": "Varchar" - }, - { - "name": "wiki_url", - "ordinal": 16, - "type_info": "Varchar" - }, - { - "name": "discord_url", - "ordinal": 17, - "type_info": "Varchar" - }, - { - "name": "license_url", - "ordinal": 18, - "type_info": "Varchar" - }, - { - "name": "team_id", - "ordinal": 19, - "type_info": "Int8" - }, - { - "name": "client_side", - "ordinal": 20, - "type_info": "Int4" - }, - { - "name": "server_side", - "ordinal": 21, - "type_info": "Int4" - }, - { - "name": "license", - "ordinal": 22, - "type_info": "Varchar" - }, - { - "name": "slug", - "ordinal": 23, - "type_info": "Varchar" - }, - { - "name": "moderation_message", - "ordinal": 24, - "type_info": "Varchar" - }, - { - "name": "moderation_message_body", - "ordinal": 25, - "type_info": "Varchar" - }, - { - "name": "webhook_sent", - "ordinal": 26, - "type_info": "Bool" - }, - { - "name": "color", - "ordinal": 27, - "type_info": "Int4" - }, - { - "name": "loaders", - "ordinal": 28, - "type_info": "VarcharArray" - }, - { - "name": "game_versions", - "ordinal": 29, - "type_info": "VarcharArray" - }, - { - "name": "thread_id", - "ordinal": 30, - "type_info": "Int8" - }, - { - "name": "monetization_status", - "ordinal": 31, - "type_info": "Varchar" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - true, - true, - false, - true, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true, - true, - true, - false, - true, - false, - false, - true, - false - ], - "parameters": { - "Left": [ - "Int8Array" - ] - } - }, - "query": "\n SELECT id, project_type, title, description, downloads, follows,\n icon_url, body, published,\n updated, approved, queued, status, requested_status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug,\n moderation_message, moderation_message_body,\n webhook_sent, color, loaders, game_versions, thread_id, monetization_status\n FROM mods\n WHERE id = ANY($1)\n " - }, "0f29bb5ba767ebd0669c860994e48e3cb2674f0d53f6c4ab85c79d46b04cbb40": { "describe": { "columns": [ @@ -693,110 +560,6 @@ }, "query": "\n INSERT INTO reports (\n id, report_type_id, mod_id, version_id, user_id,\n body, reporter, thread_id\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8\n )\n " }, - "14ecb5fda3352c8bba9fc0b4570639f222f6fa83bbc9ecc618dca61761118f0e": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "kratos_id", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "name", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "email", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "github_id", - "ordinal": 4, - "type_info": "Int8" - }, - { - "name": "avatar_url", - "ordinal": 5, - "type_info": "Varchar" - }, - { - "name": "username", - "ordinal": 6, - "type_info": "Varchar" - }, - { - "name": "bio", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "created", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "role", - "ordinal": 9, - "type_info": "Varchar" - }, - { - "name": "badges", - "ordinal": 10, - "type_info": "Int8" - }, - { - "name": "balance", - "ordinal": 11, - "type_info": "Numeric" - }, - { - "name": "payout_wallet", - "ordinal": 12, - "type_info": "Varchar" - }, - { - "name": "payout_wallet_type", - "ordinal": 13, - "type_info": "Varchar" - }, - { - "name": "payout_address", - "ordinal": 14, - "type_info": "Varchar" - } - ], - "nullable": [ - false, - true, - true, - true, - true, - true, - false, - true, - false, - false, - false, - false, - true, - true, - true - ], - "parameters": { - "Left": [ - "Int8Array" - ] - } - }, - "query": "\n SELECT u.id, u.kratos_id, u.name, u.email, u.github_id,\n u.avatar_url, u.username, u.bio,\n u.created, u.role, u.badges,\n u.balance, u.payout_wallet, u.payout_wallet_type,\n u.payout_address\n FROM users u\n WHERE u.id = ANY($1)\n " - }, "1510e820cdc31b62222d2be6a838e69876fe3c3c742dea188ca1a6b7f894d610": { "describe": { "columns": [ @@ -969,18 +732,6 @@ }, "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.published published, m.approved approved, m.updated updated,\n m.team_id team_id, m.license license, m.slug slug, m.status status_name, m.color color,\n cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, u.username username,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT gv.version) filter (where gv.version is not null) versions,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,\n JSONB_AGG(DISTINCT jsonb_build_object('id', mdep.id, 'dep_type', d.dependency_type)) filter (where mdep.id is not null) dependencies\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id AND v.status != ALL($1)\n LEFT OUTER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id\n LEFT OUTER JOIN game_versions gv ON gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n LEFT OUTER JOIN dependencies d ON d.dependent_id = v.id\n LEFT OUTER JOIN mods mdep ON mdep.id = d.mod_dependency_id\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n WHERE m.status = ANY($2)\n GROUP BY m.id, cs.id, ss.id, pt.id, u.id;\n " }, - "15b8ea323c2f6d03c2e385d9c46d7f13460764f2f106fd638226c42ae0217f75": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM notifications\n WHERE user_id = $1\n " - }, "16049957962ded08751d5a4ddce2ffac17ecd486f61210c51a952508425d83e6": { "describe": { "columns": [], @@ -1014,18 +765,17 @@ }, "query": "\n SELECT mod_id FROM versions WHERE id = $1\n " }, - "177716d2b04fd2a2b63b2e14c8ffdfa554d84254b14053496c118dec24bf5049": { + "166d93a7d4ac629444eadcd51d793490220bbf1e503bf85ec97b37500c8f74aa": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Int8", - "TextArray" + "Int8" ] } }, - "query": "\n UPDATE mods\n SET game_versions = (\n SELECT COALESCE(ARRAY_AGG(DISTINCT gv.version) filter (where gv.version is not null), array[]::varchar[])\n FROM versions v\n INNER JOIN game_versions_versions gvv ON v.id = gvv.joining_version_id\n INNER JOIN game_versions gv on gvv.game_version_id = gv.id\n WHERE v.mod_id = mods.id AND v.status != ALL($2)\n )\n WHERE id = $1\n " + "query": "\n DELETE FROM sessions WHERE id = $1\n " }, "1931ff3846345c0af4e15c3a84dcbfc7c9cbb92c98d2e73634f611a1e5358c7a": { "describe": { @@ -1062,122 +812,6 @@ }, "query": "\n INSERT INTO payouts_values (user_id, mod_id, amount, created)\n VALUES ($1, $2, $3, $4)\n " }, - "19bcfcd376172d2b293e86e9dd69ee778f7447ae708fd0c3c70239d2c8b6a419": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "mod_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "author_id", - "ordinal": 2, - "type_info": "Int8" - }, - { - "name": "version_name", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "version_number", - "ordinal": 4, - "type_info": "Varchar" - }, - { - "name": "changelog", - "ordinal": 5, - "type_info": "Varchar" - }, - { - "name": "date_published", - "ordinal": 6, - "type_info": "Timestamptz" - }, - { - "name": "downloads", - "ordinal": 7, - "type_info": "Int4" - }, - { - "name": "version_type", - "ordinal": 8, - "type_info": "Varchar" - }, - { - "name": "featured", - "ordinal": 9, - "type_info": "Bool" - }, - { - "name": "status", - "ordinal": 10, - "type_info": "Varchar" - }, - { - "name": "requested_status", - "ordinal": 11, - "type_info": "Varchar" - }, - { - "name": "game_versions", - "ordinal": 12, - "type_info": "Jsonb" - }, - { - "name": "loaders", - "ordinal": 13, - "type_info": "VarcharArray" - }, - { - "name": "files", - "ordinal": 14, - "type_info": "Jsonb" - }, - { - "name": "hashes", - "ordinal": 15, - "type_info": "Jsonb" - }, - { - "name": "dependencies", - "ordinal": 16, - "type_info": "Jsonb" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - null, - null, - null, - null, - null - ], - "parameters": { - "Left": [ - "Int8Array" - ] - } - }, - "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.date_published date_published, v.downloads downloads,\n v.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status,\n JSONB_AGG(DISTINCT jsonb_build_object('version', gv.version, 'created', gv.created)) filter (where gv.version is not null) game_versions,\n ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,\n JSONB_AGG(DISTINCT jsonb_build_object('id', f.id, 'url', f.url, 'filename', f.filename, 'primary', f.is_primary, 'size', f.size, 'file_type', f.file_type)) filter (where f.id is not null) files,\n JSONB_AGG(DISTINCT jsonb_build_object('algorithm', h.algorithm, 'hash', encode(h.hash, 'escape'), 'file_id', h.file_id)) filter (where h.hash is not null) hashes,\n JSONB_AGG(DISTINCT jsonb_build_object('project_id', d.mod_dependency_id, 'version_id', d.dependency_id, 'dependency_type', d.dependency_type,'file_name', dependency_file_name)) filter (where d.dependency_type is not null) dependencies\n FROM versions v\n LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id\n LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n WHERE v.id = ANY($1)\n GROUP BY v.id\n ORDER BY v.date_published ASC;\n " - }, "19dc22c4d6d14222f8e8bace74c2961761c53b7375460ade15af921754d5d7da": { "describe": { "columns": [], @@ -1204,261 +838,6 @@ }, "query": "\n UPDATE users\n SET avatar_url = $1\n WHERE (id = $2)\n " }, - "1c1911c07c72a37baabaf792b07333a1130efb32af17eb6c27507fa898ed068b": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "project_type", - "ordinal": 1, - "type_info": "Int4" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "description", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "downloads", - "ordinal": 4, - "type_info": "Int4" - }, - { - "name": "follows", - "ordinal": 5, - "type_info": "Int4" - }, - { - "name": "icon_url", - "ordinal": 6, - "type_info": "Varchar" - }, - { - "name": "body", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "published", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "updated", - "ordinal": 9, - "type_info": "Timestamptz" - }, - { - "name": "approved", - "ordinal": 10, - "type_info": "Timestamptz" - }, - { - "name": "queued", - "ordinal": 11, - "type_info": "Timestamptz" - }, - { - "name": "status", - "ordinal": 12, - "type_info": "Varchar" - }, - { - "name": "requested_status", - "ordinal": 13, - "type_info": "Varchar" - }, - { - "name": "issues_url", - "ordinal": 14, - "type_info": "Varchar" - }, - { - "name": "source_url", - "ordinal": 15, - "type_info": "Varchar" - }, - { - "name": "wiki_url", - "ordinal": 16, - "type_info": "Varchar" - }, - { - "name": "discord_url", - "ordinal": 17, - "type_info": "Varchar" - }, - { - "name": "license_url", - "ordinal": 18, - "type_info": "Varchar" - }, - { - "name": "team_id", - "ordinal": 19, - "type_info": "Int8" - }, - { - "name": "client_side", - "ordinal": 20, - "type_info": "Int4" - }, - { - "name": "server_side", - "ordinal": 21, - "type_info": "Int4" - }, - { - "name": "license", - "ordinal": 22, - "type_info": "Varchar" - }, - { - "name": "slug", - "ordinal": 23, - "type_info": "Varchar" - }, - { - "name": "moderation_message", - "ordinal": 24, - "type_info": "Varchar" - }, - { - "name": "moderation_message_body", - "ordinal": 25, - "type_info": "Varchar" - }, - { - "name": "client_side_type", - "ordinal": 26, - "type_info": "Varchar" - }, - { - "name": "server_side_type", - "ordinal": 27, - "type_info": "Varchar" - }, - { - "name": "project_type_name", - "ordinal": 28, - "type_info": "Varchar" - }, - { - "name": "webhook_sent", - "ordinal": 29, - "type_info": "Bool" - }, - { - "name": "color", - "ordinal": 30, - "type_info": "Int4" - }, - { - "name": "loaders", - "ordinal": 31, - "type_info": "VarcharArray" - }, - { - "name": "game_versions", - "ordinal": 32, - "type_info": "VarcharArray" - }, - { - "name": "thread_id", - "ordinal": 33, - "type_info": "Int8" - }, - { - "name": "monetization_status", - "ordinal": 34, - "type_info": "Varchar" - }, - { - "name": "categories", - "ordinal": 35, - "type_info": "VarcharArray" - }, - { - "name": "additional_categories", - "ordinal": 36, - "type_info": "VarcharArray" - }, - { - "name": "versions", - "ordinal": 37, - "type_info": "Jsonb" - }, - { - "name": "gallery", - "ordinal": 38, - "type_info": "Jsonb" - }, - { - "name": "donations", - "ordinal": 39, - "type_info": "Jsonb" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - true, - true, - false, - true, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true, - true, - true, - false, - false, - false, - false, - true, - false, - false, - true, - false, - null, - null, - null, - null, - null - ], - "parameters": { - "Left": [ - "Int8Array", - "TextArray" - ] - } - }, - "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.published published,\n m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.webhook_sent, m.color,\n m.loaders loaders, m.game_versions game_versions, m.thread_id thread_id, m.monetization_status monetization_status,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n JSONB_AGG(DISTINCT jsonb_build_object('id', v.id, 'date_published', v.date_published)) filter (where v.id is not null) versions,\n JSONB_AGG(DISTINCT jsonb_build_object('image_url', mg.image_url, 'featured', mg.featured, 'title', mg.title, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering)) filter (where mg.image_url is not null) gallery,\n JSONB_AGG(DISTINCT jsonb_build_object('platform_id', md.joining_platform_id, 'platform_short', dp.short, 'platform_name', dp.name,'url', md.url)) filter (where md.joining_platform_id is not null) donations\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n LEFT JOIN mods_donations md ON md.joining_mod_id = m.id\n LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n LEFT JOIN versions v ON v.mod_id = m.id AND v.status = ANY($2)\n LEFT JOIN mods_gallery mg ON mg.mod_id = m.id\n WHERE m.id = ANY($1)\n GROUP BY pt.id, cs.id, ss.id, m.id;\n " - }, "1c7b0eb4341af5a7942e52f632cf582561f10b4b6a41a082fb8a60f04ac17c6e": { "describe": { "columns": [ @@ -1479,37 +858,6 @@ }, "query": "SELECT EXISTS(SELECT 1 FROM states WHERE id=$1)" }, - "1cb2e27dc45e65fd6f2f5118cc3547860762ceef37a75c352ec0ac0ea4214c32": { - "describe": { - "columns": [ - { - "name": "version_id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "date_published", - "ordinal": 1, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Left": [ - "Int8", - "VarcharArray", - "VarcharArray", - "Varchar", - "Int8", - "Int8" - ] - } - }, - "query": "\n SELECT DISTINCT ON(v.date_published, v.id) version_id, v.date_published FROM versions v\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id\n INNER JOIN game_versions gv on gvv.game_version_id = gv.id AND (cardinality($2::varchar[]) = 0 OR gv.version = ANY($2::varchar[]))\n INNER JOIN loaders_versions lv ON lv.version_id = v.id\n INNER JOIN loaders l on lv.loader_id = l.id AND (cardinality($3::varchar[]) = 0 OR l.loader = ANY($3::varchar[]))\n WHERE v.mod_id = $1 AND ($4::varchar IS NULL OR v.version_type = $4)\n ORDER BY v.date_published DESC, v.id\n LIMIT $5 OFFSET $6\n " - }, "1cefe4924d3c1f491739858ce844a22903d2dbe26f255219299f1833a10ce3d7": { "describe": { "columns": [ @@ -1621,18 +969,6 @@ }, "query": "\n UPDATE threads_messages\n SET body = $2\n WHERE id = $1\n " }, - "2162043897db26d0b55a0652c1a6db66c555f1d148ce69bd0bd0d2122de1bd6a": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM mods_gallery\n WHERE mod_id = $1\n " - }, "21ef50f46b7b3e62b91e7d067c1cb33806e14c33bb76d63c2711f822c44261f6": { "describe": { "columns": [ @@ -1673,32 +1009,6 @@ }, "query": "\n SELECT m.id FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.accepted = TRUE\n WHERE tm.user_id = $1\n ORDER BY m.downloads DESC\n " }, - "2278a7db5eb0474576fa9c86ba97bd6bf13864b3f9ce55ed2ab0cb94edbadaf5": { - "describe": { - "columns": [ - { - "name": "url", - "ordinal": 0, - "type_info": "Varchar" - }, - { - "name": "expires", - "ordinal": 1, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT url, expires FROM states\n WHERE id = $1\n " - }, "232d7d0319c20dd5fff29331b067d6c6373bcff761a77958a2bb5f59068a83a5": { "describe": { "columns": [], @@ -1765,18 +1075,6 @@ }, "query": "\n SELECT m.id id, tm.user_id user_id, tm.payouts_split payouts_split\n FROM mods m\n INNER JOIN team_members tm on m.team_id = tm.team_id AND tm.accepted = TRUE\n WHERE m.id = ANY($1)\n " }, - "27a35fca63dfc3801f95958604f0ac27afd81800e2dc981382d6f923c4415d32": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8Array" - ] - } - }, - "query": "\n DELETE FROM notifications_actions\n WHERE notification_id = ANY($1)\n " - }, "281e3faffa65b51fadc93108ccc93d3d19934c8f26efb568f4794e4c6f16cefe": { "describe": { "columns": [ @@ -1889,53 +1187,6 @@ }, "query": "\n UPDATE team_members\n SET ordering = $1\n WHERE (team_id = $2 AND user_id = $3)\n " }, - "2a4d12bd340ed79773d36d68b0bdaf86cd5df5f1d83389d9f4a1b530b53e7bc6": { - "describe": { - "columns": [ - { - "name": "url", - "ordinal": 0, - "type_info": "Varchar" - }, - { - "name": "hash", - "ordinal": 1, - "type_info": "Bytea" - }, - { - "name": "algorithm", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "version_id", - "ordinal": 3, - "type_info": "Int8" - }, - { - "name": "project_id", - "ordinal": 4, - "type_info": "Int8" - } - ], - "nullable": [ - false, - false, - false, - false, - false - ], - "parameters": { - "Left": [ - "TextArray", - "ByteaArray", - "Text", - "TextArray" - ] - } - }, - "query": "\n SELECT f.url url, h.hash hash, h.algorithm algorithm, f.version_id version_id, v.mod_id project_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ALL($4)\n " - }, "2b8dafe9c3df9fd25235a13868e8e7607decfbe96a413cc576919a1fb510f269": { "describe": { "columns": [], @@ -1949,6 +1200,32 @@ }, "query": "\n UPDATE mods\n SET discord_url = $1\n WHERE (id = $2)\n " }, + "2bfde0471537cbdadd768006ff616e7513703971f9d60211106933d3eb759ad2": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM team_members\n WHERE user_id = $1\n " + }, + "2ce3c13256d774b389ce2296c8f3d2b0e9d764b6ead010d37616bd8eec8b647e": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Varchar", + "Varchar" + ] + } + }, + "query": "\n INSERT INTO states (id, url, provider)\n VALUES ($1, $2, $3)\n " + }, "2d460f25461e95c744c835af5d67f8a7dd2438a46e3033611dfc0edd74fb9180": { "describe": { "columns": [ @@ -1970,6 +1247,39 @@ }, "query": "\n SELECT COUNT(v.id)\n FROM versions v\n INNER JOIN mods m on v.mod_id = m.id AND m.status = ANY($1)\n WHERE v.status = ANY($2)\n " }, + "2e14706127d9822d5a0d7ada02425d224805637d03eda1343e12111f7deba443": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1\n " + }, + "2f4a620f954c7488e8bdb94a3d6968cec6d1332942b9e9f60925d14a8c2040f7": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + } + }, + "query": "\n SELECT m.id FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id\n WHERE tm.user_id = $1 AND tm.role = $2\n " + }, "320d73cd900a6e00f0e74b7a8c34a7658d16034b01a35558cb42fa9c16185eb5": { "describe": { "columns": [ @@ -2039,7 +1349,7 @@ }, "query": "\n DELETE FROM loaders_versions\n WHERE loaders_versions.version_id = $1\n " }, - "34c0c25212dd8bc133f1e79b968d18d2b66eb537aeaba752e7ab2847a2214db4": { + "34354792d062d1d4e4d80d28c1bbc3c9b0abe0c6fb03e0387f102903d2b397b5": { "describe": { "columns": [ { @@ -2057,7 +1367,7 @@ ] } }, - "query": "\n SELECT u.id\n FROM users u\n WHERE u.stripe_customer_id = $1\n " + "query": "SELECT id FROM users WHERE google_id = $1" }, "352185977065c9903c2504081ef7c400075807785d4b62fdb48d0a45ca560f51": { "describe": { @@ -2079,18 +1389,6 @@ }, "query": "SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1)" }, - "36a56feb27d6f07b7c3e7a1aa3a9e6358b5c1b08e7961343a021e2b1790877f5": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM states\n WHERE id = $1\n " - }, "371048e45dd74c855b84cdb8a6a565ccbef5ad166ec9511ab20621c336446da6": { "describe": { "columns": [], @@ -2224,6 +1522,140 @@ }, "query": "\n SELECT id FROM mods\n WHERE status = $1\n ORDER BY queued ASC\n LIMIT $2;\n " }, + "3bb3bcad044ebb7f94e9a73661295345f413c65b15e27b98fc9481caac46b48e": { + "describe": { + "columns": [ + { + "name": "expires_at", + "ordinal": 0, + "type_info": "Timestamp" + }, + { + "name": "id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "name", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "email", + "ordinal": 3, + "type_info": "Varchar" + }, + { + "name": "avatar_url", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "username", + "ordinal": 5, + "type_info": "Varchar" + }, + { + "name": "bio", + "ordinal": 6, + "type_info": "Varchar" + }, + { + "name": "created", + "ordinal": 7, + "type_info": "Timestamptz" + }, + { + "name": "role", + "ordinal": 8, + "type_info": "Varchar" + }, + { + "name": "badges", + "ordinal": 9, + "type_info": "Int8" + }, + { + "name": "balance", + "ordinal": 10, + "type_info": "Numeric" + }, + { + "name": "payout_wallet", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "payout_wallet_type", + "ordinal": 12, + "type_info": "Varchar" + }, + { + "name": "payout_address", + "ordinal": 13, + "type_info": "Varchar" + }, + { + "name": "github_id", + "ordinal": 14, + "type_info": "Int8" + }, + { + "name": "discord_id", + "ordinal": 15, + "type_info": "Int8" + }, + { + "name": "gitlab_id", + "ordinal": 16, + "type_info": "Int8" + }, + { + "name": "google_id", + "ordinal": 17, + "type_info": "Varchar" + }, + { + "name": "steam_id", + "ordinal": 18, + "type_info": "Int8" + }, + { + "name": "microsoft_id", + "ordinal": 19, + "type_info": "Varchar" + } + ], + "nullable": [ + false, + false, + true, + true, + true, + false, + true, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "\n SELECT pats.expires_at,\n u.id, u.name, u.email,\n u.avatar_url, u.username, u.bio,\n u.created, u.role, u.badges,\n u.balance, u.payout_wallet, u.payout_wallet_type, u.payout_address,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id\n FROM pats LEFT OUTER JOIN users u ON pats.user_id = u.id\n WHERE access_token = $1\n " + }, "3bdcbfa5abe43cc9b4f996f147277a7f6921cca00f82cad0ef5d85032c761a36": { "describe": { "columns": [], @@ -2305,19 +1737,6 @@ }, "query": "\n UPDATE mods\n SET title = $1\n WHERE (id = $2)\n " }, - "3dd2c3bfd40e2a3c0bf0fd94023d35bd821186670780a6883691ab1a097e2956": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - } - }, - "query": "\n UPDATE threads_messages\n SET body = '{\"type\": \"deleted\"}', author_id = $2\n WHERE author_id = $1\n " - }, "3f2f05653552ce8c1be95ce0a922ab41f52f40f8ff6c91c6621481102c8f35e3": { "describe": { "columns": [], @@ -2355,18 +1774,6 @@ }, "query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1 AND is_additional = TRUE\n " }, - "414951c52e3342b4009cd1d0169bc34b164ab00db0af8c2d446a178a52e5fd6c": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n UPDATE users\n SET stripe_customer_id = NULL, midas_expires = NULL, is_overdue = NULL\n WHERE (stripe_customer_id = $1)\n " - }, "447350097928db863d47d756354cd52668f52f7156dd7f3673a826f7b9aca2fd": { "describe": { "columns": [ @@ -2412,116 +1819,6 @@ }, "query": "\n SELECT gv.id id, gv.version version_, gv.type type_, gv.created created, gv.major major FROM game_versions gv\n WHERE major = $1 AND type = $2\n ORDER BY created DESC\n " }, - "448e0dc80b5722160a52bdcae7e256524a1b6ebd7cd918566e90383693b519b9": { - "describe": { - "columns": [ - { - "name": "expires_at", - "ordinal": 0, - "type_info": "Timestamp" - }, - { - "name": "id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "name", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "kratos_id", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "email", - "ordinal": 4, - "type_info": "Varchar" - }, - { - "name": "github_id", - "ordinal": 5, - "type_info": "Int8" - }, - { - "name": "avatar_url", - "ordinal": 6, - "type_info": "Varchar" - }, - { - "name": "username", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "bio", - "ordinal": 8, - "type_info": "Varchar" - }, - { - "name": "created", - "ordinal": 9, - "type_info": "Timestamptz" - }, - { - "name": "role", - "ordinal": 10, - "type_info": "Varchar" - }, - { - "name": "badges", - "ordinal": 11, - "type_info": "Int8" - }, - { - "name": "balance", - "ordinal": 12, - "type_info": "Numeric" - }, - { - "name": "payout_wallet", - "ordinal": 13, - "type_info": "Varchar" - }, - { - "name": "payout_wallet_type", - "ordinal": 14, - "type_info": "Varchar" - }, - { - "name": "payout_address", - "ordinal": 15, - "type_info": "Varchar" - } - ], - "nullable": [ - false, - false, - true, - true, - true, - true, - true, - false, - true, - false, - false, - false, - false, - true, - true, - true - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT pats.expires_at,\n u.id, u.name, u.kratos_id, u.email, u.github_id,\n u.avatar_url, u.username, u.bio,\n u.created, u.role, u.badges,\n u.balance, u.payout_wallet, u.payout_wallet_type,\n u.payout_address\n FROM pats LEFT OUTER JOIN users u ON pats.user_id = u.id\n WHERE access_token = $1\n " - }, "4567790f0dc98ff20b596a33161d1f6ac8af73da67fe8c54192724626c6bf670": { "describe": { "columns": [], @@ -2605,27 +1902,6 @@ }, "query": "UPDATE mods\n SET downloads = downloads + 1\n WHERE (id = $1)" }, - "4a4b4166248877eefcd63603945fdcd392f76812bdec7c70f8ffeb06ee7e737f": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Int8", - "Text" - ] - } - }, - "query": "\n SELECT m.id FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id\n WHERE tm.user_id = $1 AND tm.role = $2\n " - }, "4a54d350b4695c32a802675506e85b0506fc62a63ca0ee5f38890824301d6515": { "describe": { "columns": [], @@ -2639,42 +1915,17 @@ }, "query": "\n UPDATE mods\n SET server_side = $1\n WHERE (id = $2)\n " }, - "4ad05a5f35600c5dadedfe93e91374ef20ba55c6a9ac6016a01422f2ae8dbb72": { + "4c58727309e5c79cc0505e57aeba0c977f308429f97b0ed296ab3bc0ebebb435": { "describe": { - "columns": [ - { - "name": "version_id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "mod_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "date_published", - "ordinal": 2, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - false - ], + "columns": [], + "nullable": [], "parameters": { "Left": [ - "Int8Array", - "VarcharArray", - "VarcharArray", - "Varchar", - "Int8", "Int8" ] } }, - "query": "\n SELECT DISTINCT ON(v.date_published, v.id) version_id, v.mod_id, v.date_published FROM versions v\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id\n INNER JOIN game_versions gv on gvv.game_version_id = gv.id AND (cardinality($2::varchar[]) = 0 OR gv.version = ANY($2::varchar[]))\n INNER JOIN loaders_versions lv ON lv.version_id = v.id\n INNER JOIN loaders l on lv.loader_id = l.id AND (cardinality($3::varchar[]) = 0 OR l.loader = ANY($3::varchar[]))\n WHERE v.mod_id = ANY($1) AND ($4::varchar IS NULL OR v.version_type = $4)\n ORDER BY v.date_published, v.id ASC\n LIMIT $5 OFFSET $6\n " + "query": "\n DELETE FROM reports\n WHERE user_id = $1\n " }, "4c9e2190e2a68ffc093a69aaa1fc9384957138f57ac9cd85cbc6179613c13a08": { "describe": { @@ -2696,26 +1947,6 @@ }, "query": "SELECT EXISTS(SELECT 1 FROM mods WHERE id = $1)" }, - "4cfafb61d38608152743c38cb8fb9a9c35e788fcbefe6f7f81476a3f144af3f8": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT id FROM mods\n WHERE id = $1\n " - }, "4d54032b02c860f4facec39eacb4548a0701d4505e7a80b4834650696df69c2b": { "describe": { "columns": [], @@ -2740,6 +1971,135 @@ }, "query": "\n DELETE FROM mods_gallery\n WHERE id = $1\n " }, + "4dfc14e7ba6fe3a8e0e078d91efef33743be2939838a6e621c8abeaadc12ff29": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "email", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "avatar_url", + "ordinal": 3, + "type_info": "Varchar" + }, + { + "name": "username", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "bio", + "ordinal": 5, + "type_info": "Varchar" + }, + { + "name": "created", + "ordinal": 6, + "type_info": "Timestamptz" + }, + { + "name": "role", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "badges", + "ordinal": 8, + "type_info": "Int8" + }, + { + "name": "balance", + "ordinal": 9, + "type_info": "Numeric" + }, + { + "name": "payout_wallet", + "ordinal": 10, + "type_info": "Varchar" + }, + { + "name": "payout_wallet_type", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "payout_address", + "ordinal": 12, + "type_info": "Varchar" + }, + { + "name": "github_id", + "ordinal": 13, + "type_info": "Int8" + }, + { + "name": "discord_id", + "ordinal": 14, + "type_info": "Int8" + }, + { + "name": "gitlab_id", + "ordinal": 15, + "type_info": "Int8" + }, + { + "name": "google_id", + "ordinal": 16, + "type_info": "Varchar" + }, + { + "name": "steam_id", + "ordinal": 17, + "type_info": "Int8" + }, + { + "name": "microsoft_id", + "ordinal": 18, + "type_info": "Varchar" + } + ], + "nullable": [ + false, + true, + true, + true, + false, + true, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true + ], + "parameters": { + "Left": [ + "Int8Array", + "TextArray" + ] + } + }, + "query": "\n SELECT id, name, email,\n avatar_url, username, bio,\n created, role, badges,\n balance, payout_wallet, payout_wallet_type, payout_address,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id\n FROM users\n WHERE id = ANY($1) OR username = ANY($2)\n " + }, "4e9f9eafbfd705dfc94571018cb747245a98ea61bad3fae4b3ce284229d99955": { "describe": { "columns": [], @@ -2753,19 +2113,6 @@ }, "query": "\n UPDATE mods\n SET description = $1\n WHERE (id = $2)\n " }, - "4f307a8851b0cab7870798ba017955c8ebaba7444791dd65ffebcbac32d3585d": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Varchar" - ] - } - }, - "query": "\n INSERT INTO states (id, url)\n VALUES ($1, $2)\n " - }, "4fb5bd341369b4beb6b4a88de296b608ea5441a96db9f7360fbdccceb4628202": { "describe": { "columns": [], @@ -2791,26 +2138,6 @@ }, "query": "\n DELETE FROM game_versions_versions WHERE joining_version_id = $1\n " }, - "515a3629aeef7d0789fe5e57a28d77aaa35a27cb7b35df70c959f95ccbbc25f3": { - "describe": { - "columns": [ - { - "name": "thread_id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - true - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT thread_id FROM mods\n WHERE id = $1\n " - }, "5295fba2053675c8414c0b37a59943535b9a438a642ea1c68045e987f05ade13": { "describe": { "columns": [ @@ -3114,6 +2441,26 @@ }, "query": "\n UPDATE users\n SET role = $1\n WHERE (id = $2)\n " }, + "5d0b9862547d0920a5fd5ccc3460c6bf28bc7c0b1b832274ada6ce5d48b705a9": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "SELECT id FROM users WHERE gitlab_id = $1" + }, "5d7425cfa91e332bf7cc14aa5c300b997e941c49757606f6b906cb5e060d3179": { "describe": { "columns": [], @@ -3126,17 +2473,25 @@ }, "query": "\n UPDATE mods\n SET updated = NOW()\n WHERE id = $1\n " }, - "5d7d7e33c2952199225d7b93f5a74f3436ba18aa24e6ef1840becbf236447fd6": { + "5dd9503c98266d44dfef73dda81f0051789280b78d1b0fb4de509ac6ccfcb86a": { "describe": { - "columns": [], - "nullable": [], + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], "parameters": { "Left": [ "Int8" ] } }, - "query": "\n DELETE FROM mod_follows\n WHERE mod_id = $1\n " + "query": "SELECT id FROM users WHERE steam_id = $1" }, "5eb2795d25d6d03e22564048c198d821cd5ff22eb4e39b9dd7f198c9113d4f87": { "describe": { @@ -3232,86 +2587,7 @@ }, "query": "\n UPDATE mods_gallery\n SET featured = $2\n WHERE mod_id = $1\n " }, - "665e294e9737fd0299fc4639127d56811485dc8a5a4e08a4e7292044d8a2fb7a": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Varchar", - "Int8" - ] - } - }, - "query": "\n UPDATE reports\n SET body = $1\n WHERE (id = $2)\n " - }, - "66b06ddcd0a4cf01e716331befa393a12631fe6752a7d078bda06b24d50daae2": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Varchar", - "Int8" - ] - } - }, - "query": "\n UPDATE mods\n SET requested_status = $1\n WHERE (id = $2)\n " - }, - "67d021f0776276081d3c50ca97afa6b78b98860bf929009e845e9c00a192e3b5": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int4" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT id FROM report_types\n WHERE name = $1\n " - }, - "6a7b7704c2a0c52a70f5d881a1e6d3e8e77ddaa83ecc5688cd86bf327775fb76": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Bytea", - "Text" - ] - } - }, - "query": "\n SELECT f.id id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n WHERE h.algorithm = $2 AND h.hash = $1\n " - }, - "6b28cb8b54ef57c9b6f03607611f688455f0e2b27eb5deda5a8cbc5b506b4602": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM mods\n WHERE id = $1\n " - }, - "6b5be5b92fe666049c5ae0847d315101f1e625a031f4a73c2b0418e246c319fe": { + "61ebde440ef7ce59c38d92725b93b666ee22e04d6237438c2579d771f5b51240": { "describe": { "columns": [ { @@ -3360,69 +2636,34 @@ "type_info": "Varchar" }, { - "name": "email", + "name": "avatar_url", "ordinal": 9, "type_info": "Varchar" }, { - "name": "kratos_id", + "name": "username", "ordinal": 10, "type_info": "Varchar" }, - { - "name": "github_id", - "ordinal": 11, - "type_info": "Int8" - }, - { - "name": "avatar_url", - "ordinal": 12, - "type_info": "Varchar" - }, - { - "name": "username", - "ordinal": 13, - "type_info": "Varchar" - }, { "name": "bio", - "ordinal": 14, + "ordinal": 11, "type_info": "Varchar" }, { "name": "created", - "ordinal": 15, + "ordinal": 12, "type_info": "Timestamptz" }, { "name": "user_role", - "ordinal": 16, + "ordinal": 13, "type_info": "Varchar" }, { "name": "badges", - "ordinal": 17, + "ordinal": 14, "type_info": "Int8" - }, - { - "name": "balance", - "ordinal": 18, - "type_info": "Numeric" - }, - { - "name": "payout_wallet", - "ordinal": 19, - "type_info": "Varchar" - }, - { - "name": "payout_wallet_type", - "ordinal": 20, - "type_info": "Varchar" - }, - { - "name": "payout_address", - "ordinal": 21, - "type_info": "Varchar" } ], "nullable": [ @@ -3436,18 +2677,11 @@ false, true, true, - true, - true, - true, false, true, false, false, - false, - false, - true, - true, - true + false ], "parameters": { "Left": [ @@ -3455,33 +2689,106 @@ ] } }, - "query": "\n SELECT tm.id id, tm.team_id team_id, tm.role member_role, tm.permissions permissions, tm.accepted accepted, tm.payouts_split payouts_split, tm.ordering,\n u.id user_id, u.name user_name, u.email email, u.kratos_id kratos_id, u.github_id github_id,\n u.avatar_url avatar_url, u.username username, u.bio bio,\n u.created created, u.role user_role, u.badges badges, u.balance balance,\n u.payout_wallet payout_wallet, u.payout_wallet_type payout_wallet_type,\n u.payout_address payout_address\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = ANY($1)\n ORDER BY tm.team_id, tm.ordering\n " + "query": "\n SELECT tm.id id, tm.team_id team_id, tm.role member_role, tm.permissions permissions, tm.accepted accepted, tm.payouts_split payouts_split, tm.ordering,\n u.id user_id, u.name user_name,\n u.avatar_url avatar_url, u.username username, u.bio bio,\n u.created created, u.role user_role, u.badges badges\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = ANY($1)\n ORDER BY tm.team_id, tm.ordering\n " }, - "6b89c2b2557e304c2a3a02d7824327685f9be696254bf2370d0c995aafc6a2d8": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "TextArray" - ] - } - }, - "query": "\n UPDATE mods\n SET loaders = (\n SELECT COALESCE(ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null), array[]::varchar[])\n FROM versions v\n INNER JOIN loaders_versions lv ON lv.version_id = v.id\n INNER JOIN loaders l on lv.loader_id = l.id\n WHERE v.mod_id = mods.id AND v.status != ALL($2)\n )\n WHERE id = $1\n " - }, - "6ba05a1b6ddc18f43ccffd5ef2d9349ce73d0da35479dcf10b6d20b57aa78728": { + "665e294e9737fd0299fc4639127d56811485dc8a5a4e08a4e7292044d8a2fb7a": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ "Varchar", + "Int8" + ] + } + }, + "query": "\n UPDATE reports\n SET body = $1\n WHERE (id = $2)\n " + }, + "66b06ddcd0a4cf01e716331befa393a12631fe6752a7d078bda06b24d50daae2": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + } + }, + "query": "\n UPDATE mods\n SET requested_status = $1\n WHERE (id = $2)\n " + }, + "66d61a9077fd4fdf3c56e9cd6599095409ff3b46aad164210a1359a3154dbdb8": { + "describe": { + "columns": [ + { + "name": "exists", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM sessions WHERE id=$1)" + }, + "67d021f0776276081d3c50ca97afa6b78b98860bf929009e845e9c00a192e3b5": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int4" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ "Text" ] } }, - "query": "\n UPDATE users\n SET email = $1\n WHERE kratos_id = $2\n " + "query": "\n SELECT id FROM report_types\n WHERE name = $1\n " + }, + "680067ff64918882a3bff1438a6a70ca51a5dc52e48e47bbeb6e32d6739422d2": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM notifications\n WHERE user_id = $1\n " + }, + "6a7b7704c2a0c52a70f5d881a1e6d3e8e77ddaa83ecc5688cd86bf327775fb76": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Bytea", + "Text" + ] + } + }, + "query": "\n SELECT f.id id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n WHERE h.algorithm = $2 AND h.hash = $1\n " }, "6c4a42c263ae2787744aa6903e3cd85e90beaa5bea7ba78b45dbf55ce007753d": { "describe": { @@ -3583,6 +2890,18 @@ }, "query": "\n UPDATE files\n SET is_primary = FALSE\n WHERE (version_id = $1)\n " }, + "6db607d629be3047d53ff92bb82c07700595e8f4fcb7b602918540af4ae50d8b": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM users\n WHERE id = $1\n " + }, "6e07cc68675d0f583182eaa9f50853fa5996b9f83543fe8b6c2a073cf6a9cb5d": { "describe": { "columns": [ @@ -3603,6 +2922,18 @@ }, "query": "\n SELECT COUNT(id)\n FROM mods\n WHERE status = ANY($1)\n " }, + "6f594641f9633fbab31a57ebdbd33dd74f89e45252dfc2ae1cdbda549291b21b": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM mod_follows\n WHERE follower_id = $1\n " + }, "6fd06767f42be894c7a35c6b61f43407c55de43dc77ed02b39062278f3de81e3": { "describe": { "columns": [], @@ -3674,26 +3005,6 @@ }, "query": "\n SELECT m.id FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2\n WHERE m.id = ANY($1)\n " }, - "72ad6f4be40d7620a0ec557e3806da41ce95335aeaa910fe35aca2ec7c3f09b6": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT id FROM users\n WHERE id = $1\n " - }, "72c75313688dfd88a659c5250c71b9899abd6186ab32a067a7d4b8a0846ebd18": { "describe": { "columns": [ @@ -3838,6 +3149,18 @@ }, "query": "\n SELECT id, short, name FROM donation_platforms\n " }, + "78699c6d2ca0f13f4609310df479903e8d5e0d2d4c2603df0333be7dc040a4ee": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM dependencies WHERE mod_dependency_id = $1\n " + }, "78a60cf0febcc6e35b8ffe38f2c021c13ab660c81c4775bbb26004d30242a1a8": { "describe": { "columns": [ @@ -3882,41 +3205,6 @@ }, "query": "\n SELECT gv.id id, gv.version version_, gv.type type_, gv.created created, gv.major major FROM game_versions gv\n WHERE major = $1\n ORDER BY created DESC\n " }, - "7910ddb25ce486339d06af68ef32677c77199cc16af2843a05c8a33e4906064a": { - "describe": { - "columns": [ - { - "name": "hash", - "ordinal": 0, - "type_info": "Bytea" - }, - { - "name": "algorithm", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "project_id", - "ordinal": 2, - "type_info": "Int8" - } - ], - "nullable": [ - false, - false, - false - ], - "parameters": { - "Left": [ - "TextArray", - "ByteaArray", - "Text", - "TextArray" - ] - } - }, - "query": "\n SELECT h.hash hash, h.algorithm algorithm, m.id project_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ALL($4)\n " - }, "7916fe4f04067324ae05598ec9dc6f97f18baf9eda30c64f32677158ada87478": { "describe": { "columns": [], @@ -3976,6 +3264,26 @@ }, "query": "\n UPDATE users\n SET balance = balance + $1\n WHERE id = $2\n " }, + "7af44414304c8be404d32daa3cadf99fc4ecf97b74aeb5d39c890b0f35a51f96": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT n.id FROM notifications n\n WHERE n.user_id = $1\n " + }, "7c0cdacf0898155c94008a96a0b918550df4475b9e3362a926d4d00e001880c1": { "describe": { "columns": [ @@ -4029,18 +3337,261 @@ }, "query": "\n INSERT INTO mods_categories (joining_mod_id, joining_category_id, is_additional)\n VALUES ($1, $2, FALSE)\n " }, - "801a4cec0f8621ea8f365bc29213cd94aa529dc001aac51feefafe955469b3ce": { + "7d74e978a3d1bd16c4c3cad4bbb34e64191bae273591019497c8e151a837a5b0": { "describe": { - "columns": [], - "nullable": [], + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "project_type", + "ordinal": 1, + "type_info": "Int4" + }, + { + "name": "title", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "description", + "ordinal": 3, + "type_info": "Varchar" + }, + { + "name": "downloads", + "ordinal": 4, + "type_info": "Int4" + }, + { + "name": "follows", + "ordinal": 5, + "type_info": "Int4" + }, + { + "name": "icon_url", + "ordinal": 6, + "type_info": "Varchar" + }, + { + "name": "body", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "published", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "updated", + "ordinal": 9, + "type_info": "Timestamptz" + }, + { + "name": "approved", + "ordinal": 10, + "type_info": "Timestamptz" + }, + { + "name": "queued", + "ordinal": 11, + "type_info": "Timestamptz" + }, + { + "name": "status", + "ordinal": 12, + "type_info": "Varchar" + }, + { + "name": "requested_status", + "ordinal": 13, + "type_info": "Varchar" + }, + { + "name": "issues_url", + "ordinal": 14, + "type_info": "Varchar" + }, + { + "name": "source_url", + "ordinal": 15, + "type_info": "Varchar" + }, + { + "name": "wiki_url", + "ordinal": 16, + "type_info": "Varchar" + }, + { + "name": "discord_url", + "ordinal": 17, + "type_info": "Varchar" + }, + { + "name": "license_url", + "ordinal": 18, + "type_info": "Varchar" + }, + { + "name": "team_id", + "ordinal": 19, + "type_info": "Int8" + }, + { + "name": "client_side", + "ordinal": 20, + "type_info": "Int4" + }, + { + "name": "server_side", + "ordinal": 21, + "type_info": "Int4" + }, + { + "name": "license", + "ordinal": 22, + "type_info": "Varchar" + }, + { + "name": "slug", + "ordinal": 23, + "type_info": "Varchar" + }, + { + "name": "moderation_message", + "ordinal": 24, + "type_info": "Varchar" + }, + { + "name": "moderation_message_body", + "ordinal": 25, + "type_info": "Varchar" + }, + { + "name": "client_side_type", + "ordinal": 26, + "type_info": "Varchar" + }, + { + "name": "server_side_type", + "ordinal": 27, + "type_info": "Varchar" + }, + { + "name": "project_type_name", + "ordinal": 28, + "type_info": "Varchar" + }, + { + "name": "webhook_sent", + "ordinal": 29, + "type_info": "Bool" + }, + { + "name": "color", + "ordinal": 30, + "type_info": "Int4" + }, + { + "name": "thread_id", + "ordinal": 31, + "type_info": "Int8" + }, + { + "name": "monetization_status", + "ordinal": 32, + "type_info": "Varchar" + }, + { + "name": "loaders", + "ordinal": 33, + "type_info": "VarcharArray" + }, + { + "name": "game_versions", + "ordinal": 34, + "type_info": "Jsonb" + }, + { + "name": "categories", + "ordinal": 35, + "type_info": "VarcharArray" + }, + { + "name": "additional_categories", + "ordinal": 36, + "type_info": "VarcharArray" + }, + { + "name": "versions", + "ordinal": 37, + "type_info": "Jsonb" + }, + { + "name": "gallery", + "ordinal": 38, + "type_info": "Jsonb" + }, + { + "name": "donations", + "ordinal": 39, + "type_info": "Jsonb" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + true, + true, + false, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true, + false, + false, + false, + false, + true, + true, + false, + null, + null, + null, + null, + null, + null, + null + ], "parameters": { "Left": [ - "Varchar", - "Int8" + "Int8Array", + "TextArray", + "TextArray" ] } }, - "query": "\n UPDATE users\n SET kratos_id = $1\n WHERE (id = $2)\n " + "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.published published,\n m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.webhook_sent, m.color,\n m.thread_id thread_id, m.monetization_status monetization_status,\n ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,\n JSONB_AGG(DISTINCT jsonb_build_object('id', gv.version, 'created', gv.created)) filter (where gv.version is not null) game_versions,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n JSONB_AGG(DISTINCT jsonb_build_object('id', v.id, 'date_published', v.date_published)) filter (where v.id is not null) versions,\n JSONB_AGG(DISTINCT jsonb_build_object('image_url', mg.image_url, 'featured', mg.featured, 'title', mg.title, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering)) filter (where mg.image_url is not null) gallery,\n JSONB_AGG(DISTINCT jsonb_build_object('platform_id', md.joining_platform_id, 'platform_short', dp.short, 'platform_name', dp.name,'url', md.url)) filter (where md.joining_platform_id is not null) donations\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n LEFT JOIN mods_gallery mg ON mg.mod_id = m.id\n LEFT JOIN mods_donations md ON md.joining_mod_id = m.id\n LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n LEFT JOIN versions v ON v.mod_id = m.id AND v.status = ANY($3)\n LEFT JOIN loaders_versions lv ON v.id = lv.version_id\n LEFT JOIN loaders l ON lv.loader_id = l.id\n LEFT JOIN game_versions_versions gvv ON v.id = gvv.joining_version_id\n LEFT JOIN game_versions gv ON gvv.game_version_id = gv.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY pt.id, cs.id, ss.id, m.id;\n " }, "83d428e1c07d16e356ef26bdf1d707940b1683b5f631ded1f6674a081453d67b": { "describe": { @@ -4055,6 +3606,31 @@ }, "query": "\n UPDATE mods\n SET source_url = $1\n WHERE (id = $2)\n " }, + "8422dcab178b4121d438a8fe4e365f527467c09d40a470a6c2cbdab71b04be4e": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM historical_payouts\n WHERE user_id = $1\n " + }, + "85463fa221147ee8d409fc92ed681fa27df683e7c80b8dd8616ae94dc1205c24": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + } + }, + "query": "\n UPDATE versions\n SET author_id = $1\n WHERE (author_id = $2)\n " + }, "85b40877c48fc4f23039c1b556007f92056a015f160fe1059b0d3b13615af0fb": { "describe": { "columns": [], @@ -4166,18 +3742,6 @@ }, "query": "\n UPDATE users\n SET username = $1\n WHERE (id = $2)\n " }, - "8a7b2bc070e5e8308e2853ff125bc98f40b22c1d0deeb013dd90ce5768bd0ce8": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM payouts_values\n WHERE user_id = $1\n " - }, "8abb317c85f48c7dd9ccf4a7b8fbc0b58ac73f7ae87ff2dfe67009a51089f784": { "describe": { "columns": [], @@ -4191,6 +3755,18 @@ }, "query": "\n UPDATE mods\n SET wiki_url = $1\n WHERE (id = $2)\n " }, + "8b99c759446f40e4ec9539cd368526ad9bcb1ddb266124c5f890e3b051c74c59": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM mods_gallery\n WHERE mod_id = $1\n " + }, "8ba2b2c38958f1c542e514fc62ab4682f58b0b442ac1842d20625420698e34ec": { "describe": { "columns": [], @@ -4232,25 +3808,29 @@ }, "query": "\n UPDATE threads\n SET show_in_mod_inbox = FALSE\n WHERE id = $1\n " }, - "9381c483b29d364f14c46d5e73bc14b1ec5d0525e27b9e9b099cb0786934fe78": { + "912250d37f13a98a21165c72bfc1eaa8a85b9952dd6750c117dca7fbb1bb8962": { "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], + "columns": [], + "nullable": [], "parameters": { "Left": [ - "Text" + "Int8" ] } }, - "query": "\n SELECT id FROM mods\n WHERE slug = LOWER($1)\n " + "query": "\n DELETE FROM threads_members\n WHERE user_id = $1\n " + }, + "92c00ebff25cfb0464947ea48faac417fabdb3cb3edd5ed45720598c7c12c689": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM payouts_values\n WHERE user_id = $1\n " }, "97690dda7edea8c985891cae5ad405f628ed81e333bc88df5493c928a4324d43": { "describe": { @@ -4377,26 +3957,6 @@ }, "query": "\n SELECT EXISTS(SELECT 1 FROM pats WHERE access_token=$1)\n " }, - "9dc32a9ef59f57fbad862520b6d3a4795a95d7d0db17e05eb8aedc3a2fe600dc": { - "describe": { - "columns": [ - { - "name": "stripe_customer_id", - "ordinal": 0, - "type_info": "Varchar" - } - ], - "nullable": [ - true - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT u.stripe_customer_id\n FROM users u\n WHERE u.id = $1\n " - }, "a0148ff25855202e7bb220b6a2bc9220a95e309fb0dae41d9a05afa86e6b33af": { "describe": { "columns": [], @@ -4409,6 +3969,18 @@ }, "query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1 AND is_additional = FALSE\n " }, + "a0c91184d5a02b986decac3c34e78b61451ff90e103bcf1ec46f8da3bbcc1ff2": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8Array" + ] + } + }, + "query": "\n DELETE FROM notifications_actions\n WHERE notification_id = ANY($1)\n " + }, "a11d613479d09dff5fcdc45ab7a0341fb1b4738f0ede71572d939ef0984bd65f": { "describe": { "columns": [], @@ -4421,6 +3993,20 @@ }, "query": "\n UPDATE mods\n SET approved = NOW()\n WHERE id = $1 AND approved IS NULL\n " }, + "a1a8aa7cc5d7967fbc64b979489222d9f5c154e21227f0edcbce1d96dddad3c6": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Text" + ] + } + }, + "query": "\n UPDATE team_members\n SET user_id = $1\n WHERE (user_id = $2 AND role = $3)\n " + }, "a1ba3b5cc50b1eb24f5529e06be1439f4a313c4ea8845c2733db752e53f5ae1c": { "describe": { "columns": [ @@ -4442,18 +4028,6 @@ }, "query": "\n SELECT COUNT(f.id) FROM files f\n INNER JOIN versions v on f.version_id = v.id AND v.status = ANY($2)\n INNER JOIN mods m on v.mod_id = m.id AND m.status = ANY($1)\n " }, - "a2c3f1dc8939a0df9cb62e7e751847b7681b96b4016389cf5f39ebd1deff6e5a": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n UPDATE users\n SET is_overdue = TRUE\n WHERE (id = $1)\n " - }, "a31bce5cec7583d71c140ff84a2c93a6127efee7b5607ca6e609570396f44f27": { "describe": { "columns": [ @@ -4517,6 +4091,18 @@ }, "query": "\n SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.accepted, tm.payouts_split, tm.ordering FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = TRUE\n WHERE m.id = $1\n " }, + "a39ce28b656032f862b205cffa393a76b989f4803654a615477a94fda5f57354": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM states\n WHERE id = $1\n " + }, "a3e27b758ca441fa82f6bcd42915b92fb23a7db19a7eb27db7ed92eeba4b566e": { "describe": { "columns": [ @@ -4563,17 +4149,121 @@ }, "query": "\n UPDATE mods\n SET status = $1, approved = $2\n WHERE (id = $3)\n " }, - "a647c282a276b63f36d2d8a253c32d0f627cea9cab8eb1b32b39875536bdfcbb": { + "a62767e812783e8836a11b22878a4248123f3fe212a876e192f549acd6edcb39": { "describe": { - "columns": [], - "nullable": [], + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "mod_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "author_id", + "ordinal": 2, + "type_info": "Int8" + }, + { + "name": "version_name", + "ordinal": 3, + "type_info": "Varchar" + }, + { + "name": "version_number", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "changelog", + "ordinal": 5, + "type_info": "Varchar" + }, + { + "name": "date_published", + "ordinal": 6, + "type_info": "Timestamptz" + }, + { + "name": "downloads", + "ordinal": 7, + "type_info": "Int4" + }, + { + "name": "version_type", + "ordinal": 8, + "type_info": "Varchar" + }, + { + "name": "featured", + "ordinal": 9, + "type_info": "Bool" + }, + { + "name": "status", + "ordinal": 10, + "type_info": "Varchar" + }, + { + "name": "requested_status", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "game_versions", + "ordinal": 12, + "type_info": "Jsonb" + }, + { + "name": "loaders", + "ordinal": 13, + "type_info": "VarcharArray" + }, + { + "name": "files", + "ordinal": 14, + "type_info": "Jsonb" + }, + { + "name": "hashes", + "ordinal": 15, + "type_info": "Jsonb" + }, + { + "name": "dependencies", + "ordinal": 16, + "type_info": "Jsonb" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + null, + null, + null, + null, + null + ], "parameters": { "Left": [ - "Int8" + "Int8Array" ] } }, - "query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1\n " + "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.date_published date_published, v.downloads downloads,\n v.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status,\n JSONB_AGG(DISTINCT jsonb_build_object('version', gv.version, 'created', gv.created)) filter (where gv.version is not null) game_versions,\n ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,\n JSONB_AGG(DISTINCT jsonb_build_object('id', f.id, 'url', f.url, 'filename', f.filename, 'primary', f.is_primary, 'size', f.size, 'file_type', f.file_type)) filter (where f.id is not null) files,\n JSONB_AGG(DISTINCT jsonb_build_object('algorithm', h.algorithm, 'hash', encode(h.hash, 'escape'), 'file_id', h.file_id)) filter (where h.hash is not null) hashes,\n JSONB_AGG(DISTINCT jsonb_build_object('project_id', d.mod_dependency_id, 'version_id', d.dependency_id, 'dependency_type', d.dependency_type,'file_name', dependency_file_name)) filter (where d.dependency_type is not null) dependencies\n FROM versions v\n LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id\n LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n WHERE v.id = ANY($1)\n GROUP BY v.id\n ORDER BY v.date_published ASC;\n " }, "a90bb6904e1b790c0e29e060dac5ba4c2a6087e07c1197dc1f59f0aff31944c9": { "describe": { @@ -4611,18 +4301,6 @@ }, "query": "\n SELECT COUNT(DISTINCT n.id) notifs_count, ARRAY_AGG(mf.mod_id) followed_projects FROM notifications n\n LEFT OUTER JOIN mod_follows mf ON mf.follower_id = $1\n WHERE user_id = $1 AND read = FALSE\n " }, - "aa59f79136ef87dd4121d5f367f5dbdbca80e936c1b986ec99c09c3e95daa756": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM threads_members\n WHERE user_id = $1\n " - }, "aaec611bae08eac41c163367dc508208178170de91165095405f1b41e47f5e7f": { "describe": { "columns": [ @@ -4684,26 +4362,6 @@ }, "query": "\n SELECT EXISTS(SELECT 1 FROM mods WHERE slug = LOWER($1))\n " }, - "ac2d17b7d7147b14f072c15ffa214c14f32f27ffa6a3c2b2a5f80f3ad49ca5e9": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT id FROM users\n WHERE LOWER(username) = LOWER($1)\n " - }, "acbafe265c4b7a1c95b0494a0a03c8bd2cd778ae561ef5a662fa931ca26cf603": { "describe": { "columns": [], @@ -4746,18 +4404,17 @@ }, "query": "\n DELETE FROM dependencies WHERE mod_dependency_id = NULL AND dependency_id = NULL AND dependency_file_name = NULL\n " }, - "af3f99ebd31aab8c911c9843dd8be814f302b41c9f7712944af14ab291fc0bdc": { + "ae99bfaea7f127d24b714302c9b1d6894d06485b3c62a8921e6e82086a425ad4": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Int8", - "Text" + "Int8" ] } }, - "query": "\n UPDATE users\n SET github_id = $1\n WHERE kratos_id = $2\n " + "query": "\n DELETE FROM mod_follows\n WHERE mod_id = $1\n " }, "b0c29c51bd3ae5b93d487471a98ee9bbb43a4df468ba781852b137dd315b9608": { "describe": { @@ -4774,18 +4431,6 @@ }, "query": "\n INSERT INTO threads_messages (\n id, author_id, body, thread_id\n )\n VALUES (\n $1, $2, $3, $4\n )\n " }, - "b0e3d1c70b87bb54819e3fac04b684a9b857aeedb4dcb7cb400c2af0dbb12922": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM teams\n WHERE id = $1\n " - }, "b0eb0c8e2c0f84bd7a7ba527b1dc6653e3d478a7be5e0a64d6965e8827c1e720": { "describe": { "columns": [], @@ -4833,111 +4478,7 @@ }, "query": "\n UPDATE users\n SET balance = balance - $1\n WHERE id = $2\n " }, - "b4396b519f31b9ccf4a3cde67fea204365bc2ea3cbe81705af25ae32b06ef579": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "name", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "kratos_id", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "email", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "github_id", - "ordinal": 4, - "type_info": "Int8" - }, - { - "name": "avatar_url", - "ordinal": 5, - "type_info": "Varchar" - }, - { - "name": "username", - "ordinal": 6, - "type_info": "Varchar" - }, - { - "name": "bio", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "created", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "role", - "ordinal": 9, - "type_info": "Varchar" - }, - { - "name": "badges", - "ordinal": 10, - "type_info": "Int8" - }, - { - "name": "balance", - "ordinal": 11, - "type_info": "Numeric" - }, - { - "name": "payout_wallet", - "ordinal": 12, - "type_info": "Varchar" - }, - { - "name": "payout_wallet_type", - "ordinal": 13, - "type_info": "Varchar" - }, - { - "name": "payout_address", - "ordinal": 14, - "type_info": "Varchar" - } - ], - "nullable": [ - false, - true, - true, - true, - true, - true, - false, - true, - false, - false, - false, - false, - true, - true, - true - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT u.id, u.name, u.kratos_id, u.email, u.github_id,\n u.avatar_url, u.username, u.bio,\n u.created, u.role, u.badges,\n u.balance, u.payout_wallet, u.payout_wallet_type,\n u.payout_address\n FROM users u\n WHERE u.kratos_id = $1\n " - }, - "b69a6f42965b3e7103fcbf46e39528466926789ff31e9ed2591bb175527ec169": { + "b3345991457853c3f4c49dd68239bb23c3502d5c46008eb1b50233546a6ffa5d": { "describe": { "columns": [], "nullable": [], @@ -4947,20 +4488,7 @@ ] } }, - "query": "\n DELETE FROM users\n WHERE id = $1\n " - }, - "b7b2b5b99340c7601de53cc33dc56af054b50b2fe4d1d212901c958115a42baa": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - } - }, - "query": "\n UPDATE versions\n SET author_id = $1\n WHERE (author_id = $2)\n " + "query": "\n UPDATE payouts_values\n SET mod_id = NULL\n WHERE (mod_id = $1)\n " }, "b903ac4e686ef85ba28d698c668da07860e7f276b261d8f2cebb74e73b094970": { "describe": { @@ -5094,25 +4622,29 @@ }, "query": "SELECT EXISTS(SELECT 1 FROM reports WHERE thread_id = $1 AND reporter = $2)" }, - "bec1612d4929d143bc5d6860a57cc036c5ab23e69d750ca5791c620297953c50": { + "bedb0bdf803671138449d3e46e6dc5c63f9d01ea93e447ee69c99d3f29c89ab3": { "describe": { - "columns": [ - { - "name": "team_id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], + "columns": [], + "nullable": [], "parameters": { "Left": [ - "Int8" + "Int8", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Timestamptz", + "Int8", + "Int8", + "Int8", + "Varchar", + "Int8", + "Varchar" ] } }, - "query": "\n SELECT team_id FROM mods WHERE id = $1\n " + "query": "\n INSERT INTO users (\n id, username, name, email,\n avatar_url, bio, created,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7,\n $8, $9, $10, $11, $12, $13\n )\n " }, "bee1abe8313d17a56d93b06a31240e338c3973bc7a7374799ced3df5e38d3134": { "describe": { @@ -5222,18 +4754,6 @@ }, "query": "\n SELECT id FROM game_versions\n WHERE version = $1\n " }, - "c201a7a7198fe2a083fc556b408b8b700e81759f4aa5966a4a3874a46aafb6b2": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM mod_follows\n WHERE follower_id = $1\n " - }, "c2564faa5f5a7d8aa485f4becde16ebf54d16f2dc41a70471e3b4fc896f11fd1": { "describe": { "columns": [], @@ -5247,6 +4767,30 @@ }, "query": "\n UPDATE versions\n SET version_type = $1\n WHERE (id = $2)\n " }, + "c3391aed338110205a170ba3032e54be0f2b753b5550d87d7b5ba3e17a57a202": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM reports\n WHERE mod_id = $1\n " + }, + "c3397fe8a9435d8c64283c8ae780a58b9f98e8c97c30e57d9c703619a6180917": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM teams\n WHERE id = $1\n " + }, "c44e260a1f7712b14ac521fd301fea1b3f92238da62aeaf819997aecc365be43": { "describe": { "columns": [ @@ -5382,6 +4926,26 @@ }, "query": "\n INSERT INTO mod_follows (follower_id, mod_id)\n VALUES ($1, $2)\n " }, + "c56dd77e35bf5372cd35ca981d248738b55f39d74428ed7d0c5ca2957a656eb6": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT id FROM users WHERE microsoft_id = $1" + }, "c5d44333c62223bd3e68185d1fb3f95152fafec593da8d06c9b2b665218a02be": { "describe": { "columns": [], @@ -5395,47 +4959,6 @@ }, "query": "\n UPDATE mods\n SET client_side = $1\n WHERE (id = $2)\n " }, - "c89b70c1428f32d4247497e9a89f83155d724100741e292282dd4f3679b0c2bc": { - "describe": { - "columns": [ - { - "name": "url", - "ordinal": 0, - "type_info": "Varchar" - }, - { - "name": "id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "version_id", - "ordinal": 2, - "type_info": "Int8" - }, - { - "name": "project_id", - "ordinal": 3, - "type_info": "Int8" - } - ], - "nullable": [ - false, - false, - false, - false - ], - "parameters": { - "Left": [ - "TextArray", - "Bytea", - "Text", - "TextArray" - ] - } - }, - "query": "\n SELECT f.url url, f.id id, f.version_id version_id, v.mod_id project_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ALL($4)\n ORDER BY v.date_published ASC\n " - }, "c8a27a122160a0896914c786deef9e8193eb240501d30d5ffb4129e2103efd3d": { "describe": { "columns": [], @@ -5448,25 +4971,98 @@ }, "query": "\n UPDATE versions\n SET status = requested_status\n WHERE status = $1 AND date_published < CURRENT_DATE AND requested_status IS NOT NULL\n " }, - "c9d63ed46799db7c30a7e917d97a5d4b2b78b0234cce49e136fa57526b38c1ca": { + "c94faba99d486b11509fff59465b7cc71983551b035e936ce4d9776510afb514": { "describe": { "columns": [ { - "name": "exists", + "name": "id", "ordinal": 0, - "type_info": "Bool" + "type_info": "Int8" + }, + { + "name": "user_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "session", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "created", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "last_login", + "ordinal": 4, + "type_info": "Timestamptz" + }, + { + "name": "expires", + "ordinal": 5, + "type_info": "Timestamptz" + }, + { + "name": "refresh_expires", + "ordinal": 6, + "type_info": "Timestamptz" + }, + { + "name": "os", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "platform", + "ordinal": 8, + "type_info": "Varchar" + }, + { + "name": "city", + "ordinal": 9, + "type_info": "Varchar" + }, + { + "name": "country", + "ordinal": 10, + "type_info": "Varchar" + }, + { + "name": "ip", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "user_agent", + "ordinal": 12, + "type_info": "Varchar" } ], "nullable": [ - null + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + false, + false ], "parameters": { "Left": [ - "Int8" + "Int8Array", + "TextArray" ] } }, - "query": "\n SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1)\n " + "query": "\n SELECT id, user_id, session, created, last_login, expires, refresh_expires, os, platform,\n city, country, ip, user_agent\n FROM sessions\n WHERE id = ANY($1) OR session = ANY($2)\n ORDER BY created DESC\n " }, "cb57ae673f1a7e50cc319efddb9bdc82e2251596bcf85aea52e8def343e423b8": { "describe": { @@ -5482,18 +5078,6 @@ }, "query": "\n INSERT INTO hashes (file_id, algorithm, hash)\n VALUES ($1, $2, $3)\n " }, - "cb597bf191d1ffe14634a9e7dc5089262497862eb4ee02091ee27c7a7606417a": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM reports\n WHERE mod_id = $1\n " - }, "cc58fd2c0aca4576a0fda37e7b2f183a6cda482f24e3cca7b2c6e31cb8d0d728": { "describe": { "columns": [ @@ -5558,55 +5142,6 @@ }, "query": "SELECT EXISTS(SELECT 1 FROM files WHERE id=$1)" }, - "ccdbb0f1206dfe7963777f0f8edd40b57b870460fed97bf547eb6bc20a1359e6": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Text" - ] - } - }, - "query": "\n UPDATE team_members\n SET user_id = $1\n WHERE (user_id = $2 AND role = $3)\n " - }, - "cd9de86f32e20de76fc566746172b4e41486685df61896a636fa3b5028828558": { - "describe": { - "columns": [ - { - "name": "hash", - "ordinal": 0, - "type_info": "Bytea" - }, - { - "name": "algorithm", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "version_id", - "ordinal": 2, - "type_info": "Int8" - } - ], - "nullable": [ - false, - false, - false - ], - "parameters": { - "Left": [ - "TextArray", - "ByteaArray", - "Text", - "TextArray" - ] - } - }, - "query": "\n SELECT h.hash hash, h.algorithm algorithm, f.version_id version_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ALL($4)\n " - }, "cdd7f8f95c308d9474e214d584c03be0466214da1e157f6bc577b76dbef7df86": { "describe": { "columns": [], @@ -5632,35 +5167,6 @@ }, "query": "\n INSERT INTO mods_categories (joining_mod_id, joining_category_id, is_additional)\n VALUES ($1, $2, FALSE)\n " }, - "ce0acbb8af13478aa37d65d3da2c9f634fa7d27ff826805c8d51f372500c3ddb": { - "describe": { - "columns": [ - { - "name": "hash", - "ordinal": 0, - "type_info": "Bytea" - }, - { - "name": "mod_id", - "ordinal": 1, - "type_info": "Int8" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Left": [ - "TextArray", - "ByteaArray", - "Text", - "TextArray" - ] - } - }, - "query": "\n SELECT h.hash, v.mod_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ALL($4)\n " - }, "cef01012769dcd499a0d16ce65ffc1e94bce362a7246b6a0a38d133afb90d3b6": { "describe": { "columns": [], @@ -5707,18 +5213,6 @@ }, "query": "\n SELECT v.id version_id, v.mod_id project_id, h.hash hash FROM hashes h\n INNER JOIN files f on h.file_id = f.id\n INNER JOIN versions v on f.version_id = v.id\n WHERE h.algorithm = 'sha1' AND h.hash = ANY($1)\n " }, - "d12bc07adb4dc8147d0ddccd72a4f23ed38cd31d7db3d36ebbe2c9b627130f0b": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM team_members\n WHERE team_id = $1\n " - }, "d1566672369ea22cb1f638f073f8e3fb467b354351ae71c67941323749ec9bcd": { "describe": { "columns": [ @@ -5740,110 +5234,6 @@ }, "query": "\n SELECT f.id id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n WHERE h.algorithm = $2 AND h.hash = $1\n " }, - "d1b58b57d019cf95ce2a0f4b92b34b3bdd7d27aa50dba72242458369eaa7e750": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "kratos_id", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "name", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "email", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "github_id", - "ordinal": 4, - "type_info": "Int8" - }, - { - "name": "avatar_url", - "ordinal": 5, - "type_info": "Varchar" - }, - { - "name": "username", - "ordinal": 6, - "type_info": "Varchar" - }, - { - "name": "bio", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "created", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "role", - "ordinal": 9, - "type_info": "Varchar" - }, - { - "name": "badges", - "ordinal": 10, - "type_info": "Int8" - }, - { - "name": "balance", - "ordinal": 11, - "type_info": "Numeric" - }, - { - "name": "payout_wallet", - "ordinal": 12, - "type_info": "Varchar" - }, - { - "name": "payout_wallet_type", - "ordinal": 13, - "type_info": "Varchar" - }, - { - "name": "payout_address", - "ordinal": 14, - "type_info": "Varchar" - } - ], - "nullable": [ - false, - true, - true, - true, - true, - true, - false, - true, - false, - false, - false, - false, - true, - true, - true - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT u.id, u.kratos_id, u.name, u.email, u.github_id,\n u.avatar_url, u.username, u.bio,\n u.created, u.role, u.badges,\n u.balance, u.payout_wallet, u.payout_wallet_type,\n u.payout_address\n FROM users u\n WHERE LOWER(u.username) = LOWER($1)\n " - }, "d203b99bd23d16224348e4fae44296aa0e1ea6d6a3fac26908303069b36a8dd0": { "describe": { "columns": [], @@ -5883,25 +5273,6 @@ }, "query": "\n UPDATE mods\n SET moderation_message = $1\n WHERE (id = $2)\n " }, - "d3756551348a3a4340cb460cf486229c4e3845d17dd765ff0bf7c1b9409c1770": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Timestamptz" - ] - } - }, - "query": "\n INSERT INTO users (\n id, kratos_id, username, name, email,\n avatar_url, bio, created\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8\n )\n " - }, "d3991923355b2e0ed7bbe6c85d9158754d7e7d28f5ac75ee5b4e782dbc5c38a9": { "describe": { "columns": [], @@ -5928,29 +5299,6 @@ }, "query": "\n INSERT INTO mods_categories (joining_mod_id, joining_category_id, is_additional)\n VALUES ($1, $2, TRUE)\n " }, - "d5eab086617843df850be11beaeb1a09fb1dcb19ca8a2a237a2bb73f2f5cad5f": { - "describe": { - "columns": [ - { - "name": "version_id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "TextArray", - "Bytea", - "Text", - "TextArray" - ] - } - }, - "query": "\n SELECT f.version_id version_id\n FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v on f.version_id = v.id AND v.status != ALL($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ALL($4)\n ORDER BY v.date_published ASC\n " - }, "d6453e50041b5521fa9e919a9162e533bb9426f8c584d98474c6ad414db715c8": { "describe": { "columns": [ @@ -6030,50 +5378,17 @@ }, "query": "\n DELETE FROM files\n WHERE files.version_id = $1\n " }, - "da83d33451ec3240956d989237d0c14bec19c2cc19363207d1536d7b48aa5baa": { + "d93a8727fa8c7af79529670bdeab27100a2cdeeb605c85d0f30fd4962e731157": { "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "version_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "filename", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "version_number", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "project_id", - "ordinal": 4, - "type_info": "Int8" - } - ], - "nullable": [ - false, - false, - false, - false, - false - ], + "columns": [], + "nullable": [], "parameters": { "Left": [ - "Bytea", - "Text" + "Int8" ] } }, - "query": "\n SELECT f.id id, f.version_id version_id, f.filename filename, v.version_number version_number, v.mod_id project_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id\n WHERE h.algorithm = $2 AND h.hash = $1\n ORDER BY v.date_published ASC\n " + "query": "\n DELETE FROM team_members\n WHERE team_id = $1\n " }, "dc6aa2e7bfd5d5004620ddd4cd6a47ecc56159e1489054e0652d56df802fb5e5": { "describe": { @@ -6202,6 +5517,19 @@ }, "query": "\n SELECT v.id id, v.mod_id mod_id, file_type FROM files f\n INNER JOIN versions v ON v.id = f.version_id\n WHERE f.url = $1\n " }, + "debb47a2718f79684c8776da7f289b8d178c302bb5a69562b963b8d008973b8d": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + } + }, + "query": "\n UPDATE threads_messages\n SET body = '{\"type\": \"deleted\"}', author_id = $2\n WHERE author_id = $1\n " + }, "df871bd959ba97f105ac575f34d8d2a39cbc44a07e0339750a0e477e6fd582ed": { "describe": { "columns": [], @@ -6224,144 +5552,6 @@ }, "query": "\n INSERT INTO versions (\n id, mod_id, author_id, name, version_number,\n changelog, date_published, downloads,\n version_type, featured, status\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8,\n $9, $10, $11\n )\n " }, - "e04e0d7add07dc7ae16496badcadd3789be22c80a04a01fbeda3f8dfca01f4b2": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT id FROM mods\n WHERE slug = LOWER($1)\n " - }, - "e0b0ad020deafc078ce7af9d7cb26245e00e4feaf8fcae12829113c7c04792cf": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "name", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "email", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "kratos_id", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "avatar_url", - "ordinal": 4, - "type_info": "Varchar" - }, - { - "name": "username", - "ordinal": 5, - "type_info": "Varchar" - }, - { - "name": "bio", - "ordinal": 6, - "type_info": "Varchar" - }, - { - "name": "created", - "ordinal": 7, - "type_info": "Timestamptz" - }, - { - "name": "role", - "ordinal": 8, - "type_info": "Varchar" - }, - { - "name": "badges", - "ordinal": 9, - "type_info": "Int8" - }, - { - "name": "balance", - "ordinal": 10, - "type_info": "Numeric" - }, - { - "name": "payout_wallet", - "ordinal": 11, - "type_info": "Varchar" - }, - { - "name": "payout_wallet_type", - "ordinal": 12, - "type_info": "Varchar" - }, - { - "name": "payout_address", - "ordinal": 13, - "type_info": "Varchar" - } - ], - "nullable": [ - false, - true, - true, - true, - true, - false, - true, - false, - false, - false, - false, - true, - true, - true - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT u.id, u.name, u.email, u.kratos_id,\n u.avatar_url, u.username, u.bio,\n u.created, u.role, u.badges,\n u.balance, u.payout_wallet, u.payout_wallet_type,\n u.payout_address\n FROM users u\n WHERE u.github_id = $1\n " - }, - "e29da865af4a0a110275b9756394546a3bb88bff40e18c66029651f515caed98": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT f.id id FROM files f\n WHERE f.version_id = $1\n " - }, "e3235e872f98eb85d3eb4a2518fb9dc88049ce62362bfd02623e9b49ac2e9fed": { "describe": { "columns": [ @@ -6380,26 +5570,6 @@ }, "query": "\n SELECT name FROM report_types\n " }, - "e35fa345b43725309b976efffbc8f9e20a62a5e90a86a82a77b55c39c168d2de": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT id FROM versions\n WHERE mod_id = $1\n " - }, "e3cc1fd070b97c4cc36bdb2f33080d4e0d7f3c3d81312d9d28a8c3c8213ad54b": { "describe": { "columns": [], @@ -6497,19 +5667,6 @@ }, "query": "SELECT EXISTS(SELECT 1 FROM team_members WHERE id=$1)" }, - "e876f64db82d618dce53b108509d67a1108aa747d16892499481fe9f8b95200b": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Varchar", - "Int8" - ] - } - }, - "query": "\n UPDATE users\n SET stripe_customer_id = $1\n WHERE (id = $2)\n " - }, "e8ad94314ec2972c3102041b1bf06872c8e4c8a55156a17334a0e317fe41b784": { "describe": { "columns": [ @@ -6670,26 +5827,6 @@ }, "query": "\n SELECT id, name, scope, user_id, expires_at FROM pats\n WHERE id = $1 AND user_id = $2\n " }, - "ebef881a0dae70e990814e567ed3de9565bb29b772782bc974c953af195fd6d7": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT n.id FROM notifications n\n WHERE n.user_id = $1\n " - }, "ed1d5d9433bc7f4a360431ecfdd9430c5e58cd6d1c623c187d8661200400b1a4": { "describe": { "columns": [], @@ -6715,11 +5852,11 @@ }, "query": "\n UPDATE mods\n SET follows = follows + 1\n WHERE id = $1\n " }, - "ed46b2a7665471eedf72b1be8868d1e64f5fbf4a568d4c0b8b469ca388fd70e6": { + "ed47f363296ef7f8b3a8bedfd8108ca692811be1b9dce4a89ad151a6932e44c5": { "describe": { "columns": [ { - "name": "project_id", + "name": "id", "ordinal": 0, "type_info": "Int8" } @@ -6729,26 +5866,31 @@ ], "parameters": { "Left": [ - "TextArray", - "Bytea", - "Text", - "TextArray" + "Int8" ] } }, - "query": "\n SELECT v.mod_id project_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ALL($4)\n ORDER BY v.date_published ASC\n " + "query": "\n SELECT id\n FROM sessions\n WHERE user_id = $1\n ORDER BY created DESC\n " }, - "ed5c72e789353869837e0653914c86d5d1002a4227d022567e02f280684d71a7": { + "ee2bca5618c3974147a4541bac1b2d8ca2c4a930769c11e10f6a97e3cac6ee2e": { "describe": { - "columns": [], - "nullable": [], + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], "parameters": { "Left": [ "Int8" ] } }, - "query": "\n DELETE FROM dependencies WHERE mod_dependency_id = $1\n " + "query": "SELECT id FROM users WHERE discord_id = $1" }, "ef59f99fc0ab66ff5779d0e71c4a2134e2f26eed002ff9ea5626ea3e23518594": { "describe": { @@ -6770,6 +5912,26 @@ }, "query": "\n SELECT name FROM project_types pt\n INNER JOIN mods ON mods.project_type = pt.id\n WHERE mods.id = $1\n " }, + "f1525930830e17b5ee8feb796d9950dd3741131965f050840fa75423b5a54f01": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Varchar", + "Int8", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar" + ] + } + }, + "query": "\n INSERT INTO sessions (\n id, session, user_id, os, platform,\n city, country, ip, user_agent\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9\n )\n " + }, "f17a109913015a7a5ab847bb2e73794d6261a08d450de24b450222755e520881": { "describe": { "columns": [ @@ -6791,18 +5953,6 @@ }, "query": "\n SELECT id FROM reports\n WHERE closed = FALSE AND reporter = $1\n ORDER BY created ASC\n LIMIT $2;\n " }, - "f22e9aee090f9952cf795a3540c03b0a5036dab0b740847d05e03d4565756283": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM team_members\n WHERE user_id = $1\n " - }, "f34bbe639ad21801258dc8beaab9877229a451761be07f85a1dd04d027832329": { "describe": { "columns": [ @@ -6823,26 +5973,6 @@ }, "query": "\n SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)\n " }, - "f3a8ad4a802dde0eb9304078e0368066e7d48121dfe73a63b2911b0998840a79": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT id FROM users\n WHERE LOWER(username) = LOWER($1)\n " - }, "f3d7eb1b62f0b978787dba1132308d070d28911d6ddc380cedfa16e7baa3243a": { "describe": { "columns": [ @@ -6910,18 +6040,17 @@ }, "query": "\n UPDATE users\n SET bio = $1\n WHERE (id = $2)\n " }, - "f6eae06931e9cde0f18e7031bc93c33fa689de4d9676c1a8a3fc14a182d5fb08": { + "f793e96499ff35f8dc2e420484c2a0cdb54f25ffa27caa081691779ab896a709": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Timestamptz", - "Text" + "Int8" ] } }, - "query": "\n UPDATE users\n SET midas_expires = $1, is_overdue = FALSE\n WHERE (stripe_customer_id = $2)\n " + "query": "\n DELETE FROM mods\n WHERE id = $1\n " }, "f85fc13148aafc03a4df68eaa389945e9dc6472a759525a48cfb23d31181535c": { "describe": { diff --git a/src/auth/checks.rs b/src/auth/checks.rs new file mode 100644 index 000000000..98a675411 --- /dev/null +++ b/src/auth/checks.rs @@ -0,0 +1,194 @@ +use crate::database; +use crate::database::models::project_item::QueryProject; +use crate::database::models::version_item::QueryVersion; +use crate::database::{models, Project, Version}; +use crate::models::users::User; +use crate::routes::ApiError; +use actix_web::web; +use sqlx::PgPool; + +pub async fn is_authorized( + project_data: &Project, + user_option: &Option, + pool: &web::Data, +) -> Result { + let mut authorized = !project_data.status.is_hidden(); + + if let Some(user) = &user_option { + if !authorized { + if user.role.is_mod() { + authorized = true; + } else { + let user_id: models::ids::UserId = user.id.into(); + + let project_exists = sqlx::query!( + "SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)", + project_data.team_id as database::models::ids::TeamId, + user_id as database::models::ids::UserId, + ) + .fetch_one(&***pool) + .await? + .exists; + + authorized = project_exists.unwrap_or(false); + } + } + } + + Ok(authorized) +} + +pub async fn filter_authorized_projects( + projects: Vec, + user_option: &Option, + pool: &web::Data, +) -> Result, ApiError> { + let mut return_projects = Vec::new(); + let mut check_projects = Vec::new(); + + for project in projects { + if !project.inner.status.is_hidden() + || user_option + .as_ref() + .map(|x| x.role.is_mod()) + .unwrap_or(false) + { + return_projects.push(project.into()); + } else if user_option.is_some() { + check_projects.push(project); + } + } + + if !check_projects.is_empty() { + if let Some(user) = user_option { + let user_id: models::ids::UserId = user.id.into(); + + use futures::TryStreamExt; + + sqlx::query!( + " + SELECT m.id id, m.team_id team_id FROM team_members tm + INNER JOIN mods m ON m.team_id = tm.team_id + WHERE tm.team_id = ANY($1) AND tm.user_id = $2 + ", + &check_projects + .iter() + .map(|x| x.inner.team_id.0) + .collect::>(), + user_id as database::models::ids::UserId, + ) + .fetch_many(&***pool) + .try_for_each(|e| { + if let Some(row) = e.right() { + check_projects.retain(|x| { + let bool = x.inner.id.0 == row.id && x.inner.team_id.0 == row.team_id; + + if bool { + return_projects.push(x.clone().into()); + } + + !bool + }); + } + + futures::future::ready(Ok(())) + }) + .await?; + } + } + + Ok(return_projects) +} + +pub async fn is_authorized_version( + version_data: &Version, + user_option: &Option, + pool: &web::Data, +) -> Result { + let mut authorized = !version_data.status.is_hidden(); + + if let Some(user) = &user_option { + if !authorized { + if user.role.is_mod() { + authorized = true; + } else { + let user_id: models::ids::UserId = user.id.into(); + + let version_exists = sqlx::query!( + "SELECT EXISTS(SELECT 1 FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 WHERE m.id = $1)", + version_data.project_id as database::models::ids::ProjectId, + user_id as database::models::ids::UserId, + ) + .fetch_one(&***pool) + .await? + .exists; + + authorized = version_exists.unwrap_or(false); + } + } + } + + Ok(authorized) +} + +pub async fn filter_authorized_versions( + versions: Vec, + user_option: &Option, + pool: &web::Data, +) -> Result, ApiError> { + let mut return_versions = Vec::new(); + let mut check_versions = Vec::new(); + + for version in versions { + if !version.inner.status.is_hidden() + || user_option + .as_ref() + .map(|x| x.role.is_mod()) + .unwrap_or(false) + { + return_versions.push(version.into()); + } else if user_option.is_some() { + check_versions.push(version); + } + } + + if !check_versions.is_empty() { + if let Some(user) = user_option { + let user_id: models::ids::UserId = user.id.into(); + + use futures::TryStreamExt; + + sqlx::query!( + " + SELECT m.id FROM mods m + INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 + WHERE m.id = ANY($1) + ", + &check_versions + .iter() + .map(|x| x.inner.project_id.0) + .collect::>(), + user_id as database::models::ids::UserId, + ) + .fetch_many(&***pool) + .try_for_each(|e| { + if let Some(row) = e.right() { + check_versions.retain(|x| { + let bool = x.inner.project_id.0 == row.id; + + if bool { + return_versions.push(x.clone().into()); + } + + !bool + }); + } + + futures::future::ready(Ok(())) + }) + .await?; + } + } + + Ok(return_versions) +} diff --git a/src/auth/flows.rs b/src/auth/flows.rs new file mode 100644 index 000000000..454cf9918 --- /dev/null +++ b/src/auth/flows.rs @@ -0,0 +1,889 @@ +use crate::database::models::{generate_state_id, StateId}; +use crate::models::ids::base62_impl::{parse_base62, to_base62}; +use std::collections::HashMap; +use std::sync::Arc; + +use crate::parse_strings_from_var; + +use actix_web::web::{scope, Data, Query, ServiceConfig}; +use actix_web::{get, HttpRequest, HttpResponse}; +use chrono::Utc; +use reqwest::header::AUTHORIZATION; +use rust_decimal::Decimal; + +use crate::auth::session::issue_session; +use crate::auth::AuthenticationError; +use crate::file_hosting::FileHost; +use crate::models::users::{Badges, Role}; +use crate::util::ext::{get_image_content_type, get_image_ext}; +use serde::{Deserialize, Serialize}; +use sqlx::postgres::PgPool; + +pub fn config(cfg: &mut ServiceConfig) { + cfg.service(scope("auth").service(auth_callback).service(init)); +} + +#[derive(Serialize, Deserialize, Default, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum AuthProvider { + #[default] + GitHub, + Discord, + Microsoft, + GitLab, + Google, + Steam, +} + +#[derive(Debug)] +pub struct TempUser { + pub id: String, + pub username: String, + pub email: Option, + + pub avatar_url: Option, + pub bio: Option, + pub name: Option, +} + +impl AuthProvider { + pub fn get_redirect_url(&self, state: StateId) -> Result { + let state = to_base62(state.0 as u64); + let self_addr = dotenvy::var("SELF_ADDR")?; + let raw_redirect_uri = format!("{}/v2/auth/callback", self_addr); + let redirect_uri = urlencoding::encode(&raw_redirect_uri); + + Ok(match self { + AuthProvider::GitHub => { + let client_id = dotenvy::var("GITHUB_CLIENT_ID")?; + + format!( + "https://github.com/login/oauth/authorize?client_id={}&state={}&scope=read%3Auser%20user%3Aemail&redirect_uri={}", + client_id, + state, + redirect_uri, + ) + } + AuthProvider::Discord => { + let client_id = dotenvy::var("DISCORD_CLIENT_ID")?; + + format!("https://discord.com/api/oauth2/authorize?client_id={}&state={}&response_type=code&scope=identify%20email&redirect_uri={}", client_id, state, redirect_uri) + } + AuthProvider::Microsoft => { + let client_id = dotenvy::var("MICROSOFT_CLIENT_ID")?; + + format!("https://login.live.com/oauth20_authorize.srf?client_id={}&response_type=code&scope=user.read&state={}&prompt=select_account&redirect_uri={}", client_id, state, redirect_uri) + } + AuthProvider::GitLab => { + let client_id = dotenvy::var("GITLAB_CLIENT_ID")?; + + format!( + "https://gitlab.com/oauth/authorize?client_id={}&state={}&scope=read_user+profile+email&response_type=code&redirect_uri={}", + client_id, + state, + redirect_uri, + ) + } + AuthProvider::Google => { + let client_id = dotenvy::var("GOOGLE_CLIENT_ID")?; + + format!( + "https://accounts.google.com/o/oauth2/v2/auth?client_id={}&state={}&scope={}&response_type=code&redirect_uri={}", + client_id, + state, + urlencoding::encode("https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"), + redirect_uri, + ) + } + AuthProvider::Steam => { + format!( + "https://steamcommunity.com/openid/login?openid.ns={}&openid.mode={}&openid.return_to={}{}{}&openid.realm={}&openid.identity={}&openid.claimed_id={}", + urlencoding::encode("http://specs.openid.net/auth/2.0"), + "checkid_setup", + redirect_uri, urlencoding::encode("?state="), state, + self_addr, + "http://specs.openid.net/auth/2.0/identifier_select", + "http://specs.openid.net/auth/2.0/identifier_select", + ) + } + }) + } + + pub async fn get_token( + &self, + query: HashMap, + ) -> Result { + let redirect_uri = format!("{}/v2/auth/callback", dotenvy::var("SELF_ADDR")?); + + #[derive(Deserialize)] + struct AccessToken { + pub access_token: String, + } + + let res = match self { + AuthProvider::GitHub => { + let code = query + .get("code") + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + let client_id = dotenvy::var("GITHUB_CLIENT_ID")?; + let client_secret = dotenvy::var("GITHUB_CLIENT_SECRET")?; + + let url = format!( + "https://github.com/login/oauth/access_token?client_id={}&client_secret={}&code={}&redirect_uri={}", + client_id, client_secret, code, redirect_uri + ); + + let token: AccessToken = reqwest::Client::new() + .post(&url) + .header(reqwest::header::ACCEPT, "application/json") + .send() + .await? + .json() + .await?; + + token.access_token + } + AuthProvider::Discord => { + let code = query + .get("code") + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + let client_id = dotenvy::var("DISCORD_CLIENT_ID")?; + let client_secret = dotenvy::var("DISCORD_CLIENT_SECRET")?; + + let mut map = HashMap::new(); + map.insert("client_id", &*client_id); + map.insert("client_secret", &*client_secret); + map.insert("code", code); + map.insert("grant_type", "authorization_code"); + map.insert("redirect_uri", &redirect_uri); + + let token: AccessToken = reqwest::Client::new() + .post("https://discord.com/api/v10/oauth2/token") + .header(reqwest::header::ACCEPT, "application/json") + .form(&map) + .send() + .await? + .json() + .await?; + + token.access_token + } + AuthProvider::Microsoft => { + let code = query + .get("code") + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + let client_id = dotenvy::var("MICROSOFT_CLIENT_ID")?; + let client_secret = dotenvy::var("MICROSOFT_CLIENT_SECRET")?; + + let mut map = HashMap::new(); + map.insert("client_id", &*client_id); + map.insert("client_secret", &*client_secret); + map.insert("code", code); + map.insert("grant_type", "authorization_code"); + map.insert("redirect_uri", &redirect_uri); + + let token: AccessToken = reqwest::Client::new() + .post("https://login.live.com/oauth20_token.srf") + .header(reqwest::header::ACCEPT, "application/json") + .form(&map) + .send() + .await? + .json() + .await?; + + token.access_token + } + AuthProvider::GitLab => { + let code = query + .get("code") + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + let client_id = dotenvy::var("GITLAB_CLIENT_ID")?; + let client_secret = dotenvy::var("GITLAB_CLIENT_SECRET")?; + + let mut map = HashMap::new(); + map.insert("client_id", &*client_id); + map.insert("client_secret", &*client_secret); + map.insert("code", code); + map.insert("grant_type", "authorization_code"); + map.insert("redirect_uri", &redirect_uri); + + let token: AccessToken = reqwest::Client::new() + .post("https://gitlab.com/oauth/token") + .header(reqwest::header::ACCEPT, "application/json") + .form(&map) + .send() + .await? + .json() + .await?; + + token.access_token + } + AuthProvider::Google => { + let code = query + .get("code") + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + let client_id = dotenvy::var("GOOGLE_CLIENT_ID")?; + let client_secret = dotenvy::var("GOOGLE_CLIENT_SECRET")?; + + let mut map = HashMap::new(); + map.insert("client_id", &*client_id); + map.insert("client_secret", &*client_secret); + map.insert("code", code); + map.insert("grant_type", "authorization_code"); + map.insert("redirect_uri", &redirect_uri); + + let token: AccessToken = reqwest::Client::new() + .post("https://oauth2.googleapis.com/token") + .header(reqwest::header::ACCEPT, "application/json") + .form(&map) + .send() + .await? + .json() + .await?; + + token.access_token + } + AuthProvider::Steam => { + let mut form = HashMap::new(); + + let signed = query + .get("openid.signed") + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + form.insert( + "openid.assoc_handle".to_string(), + &**query + .get("openid.assoc_handle") + .ok_or_else(|| AuthenticationError::InvalidCredentials)?, + ); + form.insert("openid.signed".to_string(), &**signed); + form.insert( + "openid.sig".to_string(), + &**query + .get("openid.sig") + .ok_or_else(|| AuthenticationError::InvalidCredentials)?, + ); + form.insert("openid.ns".to_string(), "http://specs.openid.net/auth/2.0"); + form.insert("openid.mode".to_string(), "check_authentication"); + + for val in signed.split(',') { + if let Some(arr_val) = query.get(&format!("openid.{}", val)) { + form.insert(format!("openid.{}", val), &**arr_val); + } + } + + let res = reqwest::Client::new() + .post("https://steamcommunity.com/openid/login") + .header("Accept-language", "en") + .form(&form) + .send() + .await? + .text() + .await?; + + if res.contains("is_valid:true") { + let identity = query + .get("openid.identity") + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + + identity + .rsplit('/') + .next() + .ok_or_else(|| AuthenticationError::InvalidCredentials)? + .to_string() + } else { + return Err(AuthenticationError::InvalidCredentials); + } + } + }; + + Ok(res) + } + + pub async fn get_user(&self, token: &str) -> Result { + let res = match self { + AuthProvider::GitHub => { + let response = reqwest::Client::new() + .get("https://api.github.com/user") + .header(reqwest::header::USER_AGENT, "Modrinth") + .header(AUTHORIZATION, format!("token {token}")) + .send() + .await?; + + if token.starts_with("gho_") { + let client_id = response + .headers() + .get("x-oauth-client-id") + .and_then(|x| x.to_str().ok()); + + if client_id != Some(&*dotenvy::var("GITHUB_CLIENT_ID").unwrap()) { + return Err(AuthenticationError::InvalidClientId); + } + } + + #[derive(Serialize, Deserialize, Debug)] + pub struct GitHubUser { + pub login: String, + pub id: u64, + pub avatar_url: String, + pub name: Option, + pub email: Option, + pub bio: Option, + } + + let github_user: GitHubUser = response.json().await?; + + TempUser { + id: github_user.id.to_string(), + username: github_user.login, + email: github_user.email, + avatar_url: Some(github_user.avatar_url), + bio: github_user.bio, + name: github_user.name, + } + } + AuthProvider::Discord => { + #[derive(Serialize, Deserialize, Debug)] + pub struct DiscordUser { + pub username: String, + pub id: String, + pub avatar: Option, + pub global_name: Option, + pub email: Option, + } + + let discord_user: DiscordUser = reqwest::Client::new() + .get("https://discord.com/api/v10/users/@me") + .header(reqwest::header::USER_AGENT, "Modrinth") + .header(AUTHORIZATION, format!("Bearer {token}")) + .send() + .await? + .json() + .await?; + + let id = discord_user.id.clone(); + TempUser { + id: discord_user.id, + username: discord_user.username, + email: discord_user.email, + avatar_url: discord_user + .avatar + .map(|x| format!("https://cdn.discordapp.com/avatars/{}/{}.webp", id, x)), + bio: None, + name: discord_user.global_name, + } + } + AuthProvider::Microsoft => { + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + pub struct MicrosoftUser { + pub id: String, + pub display_name: Option, + pub mail: Option, + pub user_principal_name: String, + } + + let microsoft_user: MicrosoftUser = reqwest::Client::new() + .get("https://graph.microsoft.com/v1.0/me?$select=id,displayName,mail,userPrincipalName") + .header(reqwest::header::USER_AGENT, "Modrinth") + .header(AUTHORIZATION, format!("Bearer {token}")) + .send() + .await?.json().await?; + + TempUser { + id: microsoft_user.id, + username: microsoft_user + .user_principal_name + .split('@') + .next() + .unwrap_or_default() + .to_string(), + email: microsoft_user.mail, + avatar_url: None, + bio: None, + name: microsoft_user.display_name, + } + } + AuthProvider::GitLab => { + #[derive(Serialize, Deserialize, Debug)] + pub struct GitLabUser { + pub id: i32, + pub username: String, + pub email: Option, + pub avatar_url: Option, + pub name: Option, + pub bio: Option, + } + + let gitlab_user: GitLabUser = reqwest::Client::new() + .get("https://gitlab.com/api/v4/user") + .header(reqwest::header::USER_AGENT, "Modrinth") + .header(AUTHORIZATION, format!("Bearer {token}")) + .send() + .await? + .json() + .await?; + + TempUser { + id: gitlab_user.id.to_string(), + username: gitlab_user.username, + email: gitlab_user.email, + avatar_url: gitlab_user.avatar_url, + bio: gitlab_user.bio, + name: gitlab_user.name, + } + } + AuthProvider::Google => { + #[derive(Deserialize, Debug)] + pub struct GoogleUser { + pub id: String, + pub email: String, + pub name: Option, + pub bio: Option, + pub picture: Option, + } + + let google_user: GoogleUser = reqwest::Client::new() + .get("https://www.googleapis.com/userinfo/v2/me") + .header(reqwest::header::USER_AGENT, "Modrinth") + .header(AUTHORIZATION, format!("Bearer {token}")) + .send() + .await? + .json() + .await?; + + TempUser { + id: google_user.id, + username: google_user + .email + .split('@') + .next() + .unwrap_or_default() + .to_string(), + email: Some(google_user.email), + avatar_url: google_user.picture, + bio: None, + name: google_user.name, + } + } + AuthProvider::Steam => { + let api_key = dotenvy::var("STEAM_API_KEY")?; + + #[derive(Deserialize)] + struct SteamResponse { + response: Players, + } + + #[derive(Deserialize)] + struct Players { + players: Vec, + } + + #[derive(Deserialize)] + struct Player { + steamid: String, + personaname: String, + profileurl: String, + avatar: Option, + } + + let response: String = reqwest::get( + &format!( + "https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={}&steamids={}", + api_key, + token + ) + ) + .await? + .text() + .await?; + + let mut response: SteamResponse = serde_json::from_str(&response)?; + + if let Some(player) = response.response.players.pop() { + let username = player + .profileurl + .trim_matches('/') + .rsplit('/') + .next() + .unwrap_or(&player.steamid) + .to_string(); + TempUser { + id: player.steamid, + username, + email: None, + avatar_url: player.avatar, + bio: None, + name: Some(player.personaname), + } + } else { + return Err(AuthenticationError::InvalidCredentials); + } + } + }; + + Ok(res) + } + + pub async fn get_user_id<'a, 'b, E>( + &self, + id: &str, + executor: E, + ) -> Result, AuthenticationError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + Ok(match self { + AuthProvider::GitHub => { + let value = sqlx::query!( + "SELECT id FROM users WHERE github_id = $1", + id.parse::() + .map_err(|_| AuthenticationError::InvalidCredentials)? + ) + .fetch_optional(executor) + .await?; + + value.map(|x| crate::database::models::UserId(x.id)) + } + AuthProvider::Discord => { + let value = sqlx::query!( + "SELECT id FROM users WHERE discord_id = $1", + id.parse::() + .map_err(|_| AuthenticationError::InvalidCredentials)? + ) + .fetch_optional(executor) + .await?; + + value.map(|x| crate::database::models::UserId(x.id)) + } + AuthProvider::Microsoft => { + let value = sqlx::query!("SELECT id FROM users WHERE microsoft_id = $1", id) + .fetch_optional(executor) + .await?; + + value.map(|x| crate::database::models::UserId(x.id)) + } + AuthProvider::GitLab => { + let value = sqlx::query!( + "SELECT id FROM users WHERE gitlab_id = $1", + id.parse::() + .map_err(|_| AuthenticationError::InvalidCredentials)? + ) + .fetch_optional(executor) + .await?; + + value.map(|x| crate::database::models::UserId(x.id)) + } + AuthProvider::Google => { + let value = sqlx::query!("SELECT id FROM users WHERE google_id = $1", id) + .fetch_optional(executor) + .await?; + + value.map(|x| crate::database::models::UserId(x.id)) + } + AuthProvider::Steam => { + let value = sqlx::query!( + "SELECT id FROM users WHERE steam_id = $1", + id.parse::() + .map_err(|_| AuthenticationError::InvalidCredentials)? + ) + .fetch_optional(executor) + .await?; + + value.map(|x| crate::database::models::UserId(x.id)) + } + }) + } + + pub fn as_str(&self) -> &'static str { + match self { + AuthProvider::GitHub => "github", + AuthProvider::Discord => "discord", + AuthProvider::Microsoft => "microsoft", + AuthProvider::GitLab => "gitlab", + AuthProvider::Google => "google", + AuthProvider::Steam => "steam", + } + } + + pub fn from_str(string: &str) -> AuthProvider { + match string { + "github" => AuthProvider::GitHub, + "discord" => AuthProvider::Discord, + "microsoft" => AuthProvider::Microsoft, + "gitlab" => AuthProvider::GitLab, + "google" => AuthProvider::Google, + "steam" => AuthProvider::Steam, + _ => AuthProvider::GitHub, + } + } +} + +impl std::fmt::Display for AuthProvider { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + fmt.write_str(self.as_str()) + } +} + +#[derive(Serialize, Deserialize)] +pub struct AuthorizationInit { + pub url: String, + #[serde(default)] + pub provider: AuthProvider, +} +#[derive(Serialize, Deserialize)] +pub struct Authorization { + pub code: String, + pub state: String, +} + +// Init link takes us to GitHub API and calls back to callback endpoint with a code and state +// http://localhost:8000/auth/init?url=https://modrinth.com +#[get("init")] +pub async fn init( + Query(info): Query, // callback url + client: Data, +) -> Result { + let url = url::Url::parse(&info.url).map_err(|_| AuthenticationError::Url)?; + + let allowed_callback_urls = parse_strings_from_var("ALLOWED_CALLBACK_URLS").unwrap_or_default(); + let domain = url.host_str().ok_or(AuthenticationError::Url)?; + if !allowed_callback_urls.iter().any(|x| domain.ends_with(x)) && domain != "modrinth.com" { + return Err(AuthenticationError::Url); + } + + let mut transaction = client.begin().await?; + + let state = generate_state_id(&mut transaction).await?; + + sqlx::query!( + " + INSERT INTO states (id, url, provider) + VALUES ($1, $2, $3) + ", + state.0, + info.url, + info.provider.to_string() + ) + .execute(&mut *transaction) + .await?; + + transaction.commit().await?; + + let url = info.provider.get_redirect_url(state)?; + Ok(HttpResponse::TemporaryRedirect() + .append_header(("Location", &*url)) + .json(serde_json::json!({ "url": url }))) +} + +#[get("callback")] +pub async fn auth_callback( + req: HttpRequest, + Query(query): Query>, + client: Data, + file_host: Data>, + redis: Data, +) -> Result { + let mut transaction = client.begin().await?; + + let state = query + .get("state") + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + let state_id: u64 = parse_base62(state)?; + + let result_option = sqlx::query!( + " + SELECT url, expires, provider FROM states + WHERE id = $1 + ", + state_id as i64 + ) + .fetch_optional(&mut *transaction) + .await?; + + // Extract cookie header from request + if let Some(result) = result_option { + // Extract cookie header to get authenticated user from Minos + let duration: chrono::Duration = result.expires - Utc::now(); + if duration.num_seconds() < 0 { + return Err(AuthenticationError::InvalidCredentials); + } + sqlx::query!( + " + DELETE FROM states + WHERE id = $1 + ", + state_id as i64 + ) + .execute(&mut *transaction) + .await?; + + let provider = AuthProvider::from_str(&result.provider); + + let token = provider.get_token(query).await?; + let oauth_user = provider.get_user(&token).await?; + let user_id = if let Some(user_id) = provider + .get_user_id(&oauth_user.id, &mut *transaction) + .await? + { + user_id + } else { + let user_id = crate::database::models::generate_user_id(&mut transaction).await?; + + let mut username_increment: i32 = 0; + let mut username = None; + + while username.is_none() { + let test_username = format!( + "{}{}", + oauth_user.username, + if username_increment > 0 { + username_increment.to_string() + } else { + "".to_string() + } + ); + + let new_id = + crate::database::models::User::get(&test_username, &**client, &redis).await?; + + if new_id.is_none() { + username = Some(test_username); + } else { + username_increment += 1; + } + } + + let avatar_url = if let Some(avatar_url) = oauth_user.avatar_url { + let cdn_url = dotenvy::var("CDN_URL")?; + + let res = reqwest::get(&avatar_url).await?; + let headers = res.headers().clone(); + + let img_data = if let Some(content_type) = headers + .get(reqwest::header::CONTENT_TYPE) + .and_then(|ct| ct.to_str().ok()) + { + get_image_ext(content_type).map(|ext| (ext, content_type)) + } else if let Some(ext) = avatar_url.rsplit('.').next() { + get_image_content_type(ext).map(|content_type| (ext, content_type)) + } else { + None + }; + + if let Some((ext, content_type)) = img_data { + let bytes = res.bytes().await?; + let hash = sha1::Sha1::from(&bytes).hexdigest(); + + let upload_data = file_host + .upload_file( + content_type, + &format!( + "user/{}/{}.{}", + crate::models::users::UserId::from(user_id), + hash, + ext + ), + bytes, + ) + .await?; + + Some(format!("{}/{}", cdn_url, upload_data.file_name)) + } else { + None + } + } else { + None + }; + + if let Some(username) = username { + crate::database::models::User { + id: user_id, + github_id: if provider == AuthProvider::GitHub { + Some( + oauth_user + .id + .clone() + .parse() + .map_err(|_| AuthenticationError::InvalidCredentials)?, + ) + } else { + None + }, + discord_id: if provider == AuthProvider::Discord { + Some( + oauth_user + .id + .parse() + .map_err(|_| AuthenticationError::InvalidCredentials)?, + ) + } else { + None + }, + gitlab_id: if provider == AuthProvider::GitLab { + Some( + oauth_user + .id + .parse() + .map_err(|_| AuthenticationError::InvalidCredentials)?, + ) + } else { + None + }, + google_id: if provider == AuthProvider::Google { + Some(oauth_user.id.clone()) + } else { + None + }, + steam_id: if provider == AuthProvider::Steam { + Some( + oauth_user + .id + .parse() + .map_err(|_| AuthenticationError::InvalidCredentials)?, + ) + } else { + None + }, + microsoft_id: if provider == AuthProvider::Microsoft { + Some(oauth_user.id) + } else { + None + }, + username, + name: oauth_user.name, + email: oauth_user.email, + avatar_url, + bio: oauth_user.bio, + created: Utc::now(), + role: Role::Developer.to_string(), + badges: Badges::default(), + balance: Decimal::ZERO, + payout_wallet: None, + payout_wallet_type: None, + payout_address: None, + } + .insert(&mut transaction) + .await?; + + user_id + } else { + return Err(AuthenticationError::InvalidCredentials); + } + }; + + let session = issue_session(req, user_id, &mut transaction, &redis).await?; + transaction.commit().await?; + + let redirect_url = if result.url.contains('?') { + format!("{}&code={}", result.url, session.session) + } else { + format!("{}?code={}", result.url, session.session) + }; + + Ok(HttpResponse::TemporaryRedirect() + .append_header(("Location", &*redirect_url)) + .json(serde_json::json!({ "url": redirect_url }))) + } else { + Err(AuthenticationError::InvalidCredentials) + } +} diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 000000000..6e0a58ebc --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,81 @@ +pub mod checks; +pub mod flows; +pub mod pat; +mod session; +pub mod validate; + +pub use checks::{ + filter_authorized_projects, filter_authorized_versions, is_authorized, is_authorized_version, +}; +pub use flows::config; +pub use pat::{generate_pat, get_user_from_pat, PersonalAccessToken}; +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::http::StatusCode; +use actix_web::HttpResponse; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AuthenticationError { + #[error("Environment Error")] + Env(#[from] dotenvy::Error), + #[error("An unknown database error occurred: {0}")] + Sqlx(#[from] sqlx::Error), + #[error("Database Error: {0}")] + Database(#[from] crate::database::models::DatabaseError), + #[error("Error while parsing JSON: {0}")] + SerDe(#[from] serde_json::Error), + #[error("Error while communicating to external oauth provider")] + Reqwest(#[from] reqwest::Error), + #[error("Error while decoding PAT: {0}")] + Decoding(#[from] crate::models::ids::DecodingError), + #[error("Invalid Authentication Credentials")] + InvalidCredentials, + #[error("Authentication method was not valid")] + InvalidAuthMethod, + #[error("GitHub Token from incorrect Client ID")] + InvalidClientId, + #[error("Invalid callback URL specified")] + Url, + #[error("Error uploading user profile picture")] + FileHosting(#[from] FileHostingError), +} + +impl actix_web::ResponseError for AuthenticationError { + fn status_code(&self) -> StatusCode { + match self { + AuthenticationError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR, + AuthenticationError::Sqlx(..) => StatusCode::INTERNAL_SERVER_ERROR, + AuthenticationError::Database(..) => StatusCode::INTERNAL_SERVER_ERROR, + AuthenticationError::SerDe(..) => StatusCode::BAD_REQUEST, + AuthenticationError::Reqwest(..) => StatusCode::INTERNAL_SERVER_ERROR, + AuthenticationError::InvalidCredentials => StatusCode::UNAUTHORIZED, + AuthenticationError::Decoding(..) => StatusCode::BAD_REQUEST, + AuthenticationError::InvalidAuthMethod => StatusCode::UNAUTHORIZED, + AuthenticationError::InvalidClientId => StatusCode::UNAUTHORIZED, + AuthenticationError::Url => StatusCode::BAD_REQUEST, + AuthenticationError::FileHosting(..) => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).json(ApiError { + error: 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::InvalidAuthMethod => "invalid_auth_method", + AuthenticationError::InvalidClientId => "invalid_client_id", + AuthenticationError::Url => "url_error", + AuthenticationError::FileHosting(..) => "file_hosting", + }, + description: &self.to_string(), + }) + } +} diff --git a/src/util/pat.rs b/src/auth/pat.rs similarity index 90% rename from src/util/pat.rs rename to src/auth/pat.rs index a725f7331..70850c011 100644 --- a/src/util/pat.rs +++ b/src/auth/pat.rs @@ -1,11 +1,4 @@ -/*! -Current edition of Ory kratos does not support PAT access of data, so this module is how we allow for PAT authentication. - - -Just as a summary: Don't implement this flow in your application! -*/ - -use super::auth::AuthenticationError; +use crate::auth::AuthenticationError; use crate::database; use crate::database::models::{DatabaseError, UserId}; use crate::models::users::{self, Badges, RecipientType, RecipientWallet}; @@ -35,11 +28,11 @@ where let row = sqlx::query!( " SELECT pats.expires_at, - u.id, u.name, u.kratos_id, u.email, u.github_id, + u.id, u.name, u.email, u.avatar_url, u.username, u.bio, u.created, u.role, u.badges, - u.balance, u.payout_wallet, u.payout_wallet_type, - u.payout_address + u.balance, u.payout_wallet, u.payout_wallet_type, u.payout_address, + github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id FROM pats LEFT OUTER JOIN users u ON pats.user_id = u.id WHERE access_token = $1 ", @@ -54,9 +47,13 @@ where return Ok(Some(database::models::User { id: UserId(row.id), - kratos_id: row.kratos_id, name: row.name, github_id: row.github_id, + discord_id: row.discord_id, + gitlab_id: row.gitlab_id, + google_id: row.google_id, + steam_id: row.steam_id, + microsoft_id: row.microsoft_id, email: row.email, avatar_url: row.avatar_url, username: row.username, diff --git a/src/auth/session.rs b/src/auth/session.rs new file mode 100644 index 000000000..a2564982a --- /dev/null +++ b/src/auth/session.rs @@ -0,0 +1,80 @@ +use crate::auth::AuthenticationError; +use crate::database::models::session_item::{Session, SessionBuilder}; +use crate::database::models::UserId; +use crate::util::env::parse_var; +use actix_web::HttpRequest; +use rand::distributions::Alphanumeric; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use woothee::parser::Parser; + +pub async fn issue_session( + req: HttpRequest, + user_id: UserId, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + redis: &deadpool_redis::Pool, +) -> Result { + let conn_info = req.connection_info().clone(); + let ip_addr = if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) { + if let Some(header) = req.headers().get("CF-Connecting-IP") { + header.to_str().ok() + } else { + conn_info.peer_addr() + } + } else { + conn_info.peer_addr() + }; + + let country = req + .headers() + .get("cf-ipcountry") + .and_then(|x| x.to_str().ok()); + let city = req.headers().get("cf-ipcity").and_then(|x| x.to_str().ok()); + + let user_agent = req + .headers() + .get("user-agent") + .and_then(|x| x.to_str().ok()) + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + + let parser = Parser::new(); + let info = parser.parse(user_agent); + let os = if let Some(info) = info { + Some((info.os, info.name)) + } else { + None + }; + + let session = ChaCha20Rng::from_entropy() + .sample_iter(&Alphanumeric) + .take(60) + .map(char::from) + .collect::(); + + let session = format!("mra_{session}"); + + let id = SessionBuilder { + session, + user_id, + os: os.map(|x| x.0.to_string()), + platform: os.map(|x| x.1.to_string()), + city: city.map(|x| x.to_string()), + country: country.map(|x| x.to_string()), + ip: ip_addr + .ok_or_else(|| AuthenticationError::InvalidCredentials)? + .to_string(), + user_agent: user_agent.to_string(), + } + .insert(transaction) + .await?; + + let session = Session::get_id(id, &mut *transaction, redis) + .await? + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + + Ok(session) +} + +// TODO: List user sessions route +// TODO: Delete User Session Route / logout +// TODO: Refresh session route diff --git a/src/auth/validate.rs b/src/auth/validate.rs new file mode 100644 index 000000000..a6af3c1d5 --- /dev/null +++ b/src/auth/validate.rs @@ -0,0 +1,108 @@ +use crate::auth::flows::AuthProvider; +use crate::auth::get_user_from_pat; +use crate::auth::AuthenticationError; +use crate::database::models::user_item; +use crate::models::users::{Role, User, UserId, UserPayoutData}; +use actix_web::http::header::HeaderMap; +use reqwest::header::{HeaderValue, AUTHORIZATION}; + +pub async fn get_user_from_headers<'a, E>( + headers: &HeaderMap, + executor: E, + redis: &deadpool_redis::Pool, +) -> Result +where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, +{ + let token: Option<&HeaderValue> = headers.get(AUTHORIZATION); + + // Fetch DB user record and minos user from headers + let db_user = get_user_record_from_bearer_token( + token + .ok_or_else(|| AuthenticationError::InvalidAuthMethod)? + .to_str() + .map_err(|_| AuthenticationError::InvalidCredentials)?, + executor, + redis, + ) + .await? + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + + let user = User { + id: UserId::from(db_user.id), + github_id: db_user.github_id.map(|x| x as u64), + // discord_id: minos_user.discord_id, + // google_id: minos_user.google_id, + // microsoft_id: minos_user.microsoft_id, + // apple_id: minos_user.apple_id, + // gitlab_id: minos_user.gitlab_id, + username: db_user.username, + name: db_user.name, + email: db_user.email, + avatar_url: db_user.avatar_url, + bio: db_user.bio, + created: db_user.created, + role: Role::from_string(&db_user.role), + badges: db_user.badges, + payout_data: Some(UserPayoutData { + balance: db_user.balance, + payout_wallet: db_user.payout_wallet, + payout_wallet_type: db_user.payout_wallet_type, + payout_address: db_user.payout_address, + }), + }; + Ok(user) +} + +pub async fn get_user_record_from_bearer_token<'a, 'b, E>( + token: &str, + executor: E, + redis: &deadpool_redis::Pool, +) -> Result, AuthenticationError> +where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, +{ + let token: &str = token.trim_start_matches("Bearer "); + + let possible_user = match token.split_once('_') { + Some(("modrinth", _)) => get_user_from_pat(token, executor).await?, + Some(("mra", _)) => { + let session = + crate::database::models::session_item::Session::get(token, executor, redis) + .await? + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + + user_item::User::get_id(session.user_id, executor, redis).await? + } + Some(("github", _)) | Some(("gho", _)) | Some(("ghp", _)) => { + let user = AuthProvider::GitHub.get_user(token).await?; + let id = AuthProvider::GitHub.get_user_id(&user.id, executor).await?; + + user_item::User::get_id( + id.ok_or_else(|| AuthenticationError::InvalidCredentials)?, + executor, + redis, + ) + .await? + } + _ => return Err(AuthenticationError::InvalidAuthMethod), + }; + Ok(possible_user) +} + +pub async fn check_is_moderator_from_headers<'a, 'b, E>( + headers: &HeaderMap, + executor: E, + redis: &deadpool_redis::Pool, +) -> Result +where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, +{ + let user = get_user_from_headers(headers, executor, redis).await?; + + if user.role.is_mod() { + Ok(user) + } else { + Err(AuthenticationError::InvalidCredentials) + } +} diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index 98a4329f3..a4b0618fa 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -2,7 +2,7 @@ use super::DatabaseError; use crate::models::ids::base62_impl::to_base62; use crate::models::ids::random_base62_rng; use censor::Censor; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use sqlx::sqlx_macros::Type; const ID_RETRY_COUNT: usize = 20; @@ -129,35 +129,43 @@ generate_ids!( ThreadMessageId ); -#[derive(Copy, Clone, Debug, PartialEq, Eq, Type, Deserialize)] +generate_ids!( + pub generate_session_id, + SessionId, + 8, + "SELECT EXISTS(SELECT 1 FROM sessions WHERE id=$1)", + SessionId +); + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Type, Serialize, Deserialize)] #[sqlx(transparent)] pub struct UserId(pub i64); -#[derive(Copy, Clone, Debug, Type, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Type, Eq, PartialEq, Serialize, Deserialize)] #[sqlx(transparent)] pub struct TeamId(pub i64); -#[derive(Copy, Clone, Debug, Type)] +#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)] #[sqlx(transparent)] pub struct TeamMemberId(pub i64); -#[derive(Copy, Clone, Debug, Type, PartialEq, Eq, Deserialize, Hash)] +#[derive(Copy, Clone, Debug, Type, PartialEq, Eq, Hash, Serialize, Deserialize)] #[sqlx(transparent)] pub struct ProjectId(pub i64); -#[derive(Copy, Clone, Debug, Type)] +#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)] #[sqlx(transparent)] pub struct ProjectTypeId(pub i32); #[derive(Copy, Clone, Debug, Type)] #[sqlx(transparent)] pub struct StatusId(pub i32); -#[derive(Copy, Clone, Debug, Type)] +#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)] #[sqlx(transparent)] pub struct SideTypeId(pub i32); -#[derive(Copy, Clone, Debug, Type, Deserialize)] +#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)] #[sqlx(transparent)] pub struct DonationPlatformId(pub i32); -#[derive(Copy, Clone, Debug, Type, PartialEq, Eq, Hash, Deserialize)] +#[derive(Copy, Clone, Debug, Type, PartialEq, Eq, Hash, Serialize, Deserialize)] #[sqlx(transparent)] pub struct VersionId(pub i64); #[derive(Copy, Clone, Debug, Type, Deserialize)] @@ -177,7 +185,7 @@ pub struct ReportId(pub i64); #[sqlx(transparent)] pub struct ReportTypeId(pub i32); -#[derive(Copy, Clone, Debug, Type, Hash, Eq, PartialEq, Deserialize)] +#[derive(Copy, Clone, Debug, Type, Hash, Eq, PartialEq, Deserialize, Serialize)] #[sqlx(transparent)] pub struct FileId(pub i64); @@ -196,13 +204,17 @@ pub struct NotificationId(pub i64); #[sqlx(transparent)] pub struct NotificationActionId(pub i32); -#[derive(Copy, Clone, Debug, Type, Deserialize, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq)] #[sqlx(transparent)] pub struct ThreadId(pub i64); #[derive(Copy, Clone, Debug, Type, Deserialize)] #[sqlx(transparent)] pub struct ThreadMessageId(pub i64); +#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)] +#[sqlx(transparent)] +pub struct SessionId(pub i64); + use crate::models::ids; impl From for ProjectId { @@ -285,3 +297,8 @@ impl From for ids::ThreadMessageId { ids::ThreadMessageId(id.0 as u64) } } +impl From for ids::SessionId { + fn from(id: SessionId) -> Self { + ids::SessionId(id.0 as u64) + } +} diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs index a952d22de..fab0bc5f4 100644 --- a/src/database/models/mod.rs +++ b/src/database/models/mod.rs @@ -5,6 +5,7 @@ pub mod ids; pub mod notification_item; pub mod project_item; pub mod report_item; +pub mod session_item; pub mod team_item; pub mod thread_item; pub mod user_item; @@ -21,11 +22,13 @@ pub use version_item::Version; #[derive(Error, Debug)] pub enum DatabaseError { #[error("Error while interacting with the database: {0}")] - Database(#[from] sqlx::error::Error), + Database(#[from] sqlx::Error), #[error("Error while trying to generate random ID")] RandomId, - #[error("A database request failed")] - Other(String), - #[error("Error while parsing JSON: {0}")] - Json(#[from] serde_json::Error), + #[error("Error while interacting with the cache: {0}")] + CacheError(#[from] redis::RedisError), + #[error("Redis Pool Error: {0}")] + RedisPool(#[from] deadpool_redis::PoolError), + #[error("Error while serializing with the cache: {0}")] + SerdeCacheError(#[from] serde_json::Error), } diff --git a/src/database/models/project_item.rs b/src/database/models/project_item.rs index 73b68f370..57c76a2c8 100644 --- a/src/database/models/project_item.rs +++ b/src/database/models/project_item.rs @@ -1,9 +1,18 @@ use super::ids::*; +use crate::database::models; +use crate::database::models::DatabaseError; +use crate::models::ids::base62_impl::{parse_base62, to_base62}; use crate::models::projects::{MonetizationStatus, ProjectStatus}; use chrono::{DateTime, Utc}; -use serde::Deserialize; +use redis::cmd; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Deserialize)] +const PROJECTS_NAMESPACE: &str = "projects"; +const PROJECTS_SLUGS_NAMESPACE: &str = "projects_slugs"; +const PROJECTS_DEPENDENCIES_NAMESPACE: &str = "projects_dependencies"; +const DEFAULT_EXPIRY: i64 = 1800; // 30 minutes + +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct DonationUrl { pub platform_id: DonationPlatformId, pub platform_short: String, @@ -37,7 +46,7 @@ impl DonationUrl { } } -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct GalleryItem { pub image_url: String, pub featured: bool, @@ -109,7 +118,7 @@ impl ProjectBuilder { pub async fn insert( self, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - ) -> Result { + ) -> Result { let project_struct = Project { id: self.project_id, project_type: self.project_type_id, @@ -144,8 +153,6 @@ impl ProjectBuilder { moderation_message_body: None, webhook_sent: false, color: self.color, - loaders: vec![], - game_versions: vec![], thread_id: Some(self.thread_id), monetization_status: self.monetization_status, }; @@ -190,13 +197,10 @@ impl ProjectBuilder { .await?; } - Project::update_game_versions(self.project_id, &mut *transaction).await?; - Project::update_loaders(self.project_id, &mut *transaction).await?; - Ok(self.project_id) } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Project { pub id: ProjectId, pub project_type: ProjectTypeId, @@ -227,8 +231,6 @@ pub struct Project { pub moderation_message_body: Option, pub webhook_sent: bool, pub color: Option, - pub loaders: Vec, - pub game_versions: Vec, pub thread_id: Option, pub monetization_status: MonetizationStatus, } @@ -237,7 +239,7 @@ impl Project { pub async fn insert( &self, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - ) -> Result<(), sqlx::error::Error> { + ) -> Result<(), DatabaseError> { sqlx::query!( " INSERT INTO mods ( @@ -285,402 +287,303 @@ impl Project { Ok(()) } - pub async fn get<'a, 'b, E>( - id: ProjectId, - executor: E, - ) -> Result, sqlx::error::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { - Project::get_many(&[id], executor) - .await - .map(|x| x.into_iter().next()) - } - - pub async fn get_many<'a, E>( - project_ids: &[ProjectId], - exec: E, - ) -> Result, sqlx::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { - use futures::stream::TryStreamExt; - - let project_ids_parsed: Vec = project_ids.iter().map(|x| x.0).collect(); - let projects = sqlx::query!( - " - SELECT id, project_type, title, description, downloads, follows, - icon_url, body, published, - updated, approved, queued, status, requested_status, - issues_url, source_url, wiki_url, discord_url, license_url, - team_id, client_side, server_side, license, slug, - moderation_message, moderation_message_body, - webhook_sent, color, loaders, game_versions, thread_id, monetization_status - FROM mods - WHERE id = ANY($1) - ", - &project_ids_parsed - ) - .fetch_many(exec) - .try_filter_map(|e| async { - Ok(e.right().map(|m| Project { - id: ProjectId(m.id), - project_type: ProjectTypeId(m.project_type), - team_id: TeamId(m.team_id), - title: m.title, - description: m.description, - downloads: m.downloads, - body_url: None, - icon_url: m.icon_url, - published: m.published, - updated: m.updated, - issues_url: m.issues_url, - source_url: m.source_url, - wiki_url: m.wiki_url, - license_url: m.license_url, - discord_url: m.discord_url, - client_side: SideTypeId(m.client_side), - status: ProjectStatus::from_str(&m.status), - requested_status: m.requested_status.map(|x| ProjectStatus::from_str(&x)), - server_side: SideTypeId(m.server_side), - license: m.license, - slug: m.slug, - body: m.body, - follows: m.follows, - moderation_message: m.moderation_message, - moderation_message_body: m.moderation_message_body, - approved: m.approved, - webhook_sent: m.webhook_sent, - color: m.color.map(|x| x as u32), - loaders: m.loaders, - game_versions: m.game_versions, - queued: m.queued, - thread_id: m.thread_id.map(ThreadId), - monetization_status: MonetizationStatus::from_str(&m.monetization_status), - })) - }) - .try_collect::>() - .await?; - - Ok(projects) - } - - pub async fn remove_full( + pub async fn remove( id: ProjectId, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - ) -> Result, sqlx::error::Error> { - let result = sqlx::query!( - " - SELECT team_id FROM mods WHERE id = $1 - ", - id as ProjectId, - ) - .fetch_optional(&mut *transaction) - .await?; + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> { + let project = Self::get_id(id, &mut *transaction, redis).await?; - let team_id: TeamId = if let Some(id) = result { - TeamId(id.team_id) - } else { - return Ok(None); - }; + if let Some(project) = project { + Project::clear_cache(id, project.inner.slug, Some(true), redis).await?; - let thread_id = sqlx::query!( - " - SELECT thread_id FROM mods - WHERE id = $1 - ", - id as ProjectId - ) - .fetch_optional(&mut *transaction) - .await?; + sqlx::query!( + " + DELETE FROM mod_follows + WHERE mod_id = $1 + ", + id as ProjectId + ) + .execute(&mut *transaction) + .await?; - sqlx::query!( - " - DELETE FROM mod_follows - WHERE mod_id = $1 - ", - id as ProjectId - ) - .execute(&mut *transaction) - .await?; + sqlx::query!( + " + DELETE FROM mods_gallery + WHERE mod_id = $1 + ", + id as ProjectId + ) + .execute(&mut *transaction) + .await?; - sqlx::query!( - " - DELETE FROM mods_gallery - WHERE mod_id = $1 - ", - id as ProjectId - ) - .execute(&mut *transaction) - .await?; + sqlx::query!( + " + DELETE FROM mod_follows + WHERE mod_id = $1 + ", + id as ProjectId, + ) + .execute(&mut *transaction) + .await?; - sqlx::query!( - " - DELETE FROM mod_follows - WHERE mod_id = $1 - ", - id as ProjectId, - ) - .execute(&mut *transaction) - .await?; + sqlx::query!( + " + DELETE FROM reports + WHERE mod_id = $1 + ", + id as ProjectId, + ) + .execute(&mut *transaction) + .await?; - sqlx::query!( - " - DELETE FROM reports - WHERE mod_id = $1 - ", - id as ProjectId, - ) - .execute(&mut *transaction) - .await?; + sqlx::query!( + " + DELETE FROM mods_categories + WHERE joining_mod_id = $1 + ", + id as ProjectId, + ) + .execute(&mut *transaction) + .await?; - sqlx::query!( - " - DELETE FROM mods_categories - WHERE joining_mod_id = $1 - ", - id as ProjectId, - ) - .execute(&mut *transaction) - .await?; + sqlx::query!( + " + DELETE FROM mods_donations + WHERE joining_mod_id = $1 + ", + id as ProjectId, + ) + .execute(&mut *transaction) + .await?; - sqlx::query!( - " - DELETE FROM mods_donations - WHERE joining_mod_id = $1 - ", - id as ProjectId, - ) - .execute(&mut *transaction) - .await?; - - use futures::TryStreamExt; - let versions: Vec = sqlx::query!( - " - SELECT id FROM versions - WHERE mod_id = $1 - ", - id as ProjectId, - ) - .fetch_many(&mut *transaction) - .try_filter_map(|e| async { Ok(e.right().map(|c| VersionId(c.id))) }) - .try_collect::>() - .await?; - - for version in versions { - super::Version::remove_full(version, transaction).await?; - } - - sqlx::query!( - " - DELETE FROM dependencies WHERE mod_dependency_id = $1 - ", - id as ProjectId, - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - " - UPDATE payouts_values - SET mod_id = NULL - WHERE (mod_id = $1) - ", - id as ProjectId, - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - " - DELETE FROM mods - WHERE id = $1 - ", - id as ProjectId, - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - " - DELETE FROM team_members - WHERE team_id = $1 - ", - team_id as TeamId, - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - " - DELETE FROM teams - WHERE id = $1 - ", - team_id as TeamId, - ) - .execute(&mut *transaction) - .await?; - - if let Some(thread_id) = thread_id { - if let Some(id) = thread_id.thread_id { - crate::database::models::Thread::remove_full(ThreadId(id), transaction).await?; + for version in project.versions { + super::Version::remove_full(version, redis, transaction).await?; } - } - Ok(Some(())) - } + sqlx::query!( + " + DELETE FROM dependencies WHERE mod_dependency_id = $1 + ", + id as ProjectId, + ) + .execute(&mut *transaction) + .await?; - pub async fn get_full_from_slug<'a, 'b, E>( - slug: &str, - executor: E, - ) -> Result, sqlx::error::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { - let id = sqlx::query!( - " - SELECT id FROM mods - WHERE slug = LOWER($1) - ", - slug - ) - .fetch_optional(executor) - .await?; + sqlx::query!( + " + UPDATE payouts_values + SET mod_id = NULL + WHERE (mod_id = $1) + ", + id as ProjectId, + ) + .execute(&mut *transaction) + .await?; - if let Some(project_id) = id { - Project::get_full(ProjectId(project_id.id), executor).await + sqlx::query!( + " + DELETE FROM mods + WHERE id = $1 + ", + id as ProjectId, + ) + .execute(&mut *transaction) + .await?; + + models::TeamMember::clear_cache(project.inner.team_id, redis).await?; + + sqlx::query!( + " + DELETE FROM team_members + WHERE team_id = $1 + ", + project.inner.team_id as TeamId, + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + DELETE FROM teams + WHERE id = $1 + ", + project.inner.team_id as TeamId, + ) + .execute(&mut *transaction) + .await?; + + if let Some(thread_id) = project.inner.thread_id { + models::Thread::remove_full(thread_id, transaction).await?; + } + + Ok(Some(())) } else { Ok(None) } } - pub async fn get_from_slug<'a, 'b, E>( - slug: &str, + pub async fn get<'a, 'b, E>( + string: &str, executor: E, - ) -> Result, sqlx::error::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { - let id = sqlx::query!( - " - SELECT id FROM mods - WHERE slug = LOWER($1) - ", - slug - ) - .fetch_optional(executor) - .await?; - - if let Some(project_id) = id { - Project::get(ProjectId(project_id.id), executor).await - } else { - Ok(None) - } - } - - pub async fn get_from_slug_or_project_id<'a, 'b, E>( - slug_or_project_id: &str, - executor: E, - ) -> Result, sqlx::error::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { - let id_option = crate::models::ids::base62_impl::parse_base62(slug_or_project_id).ok(); - - if let Some(id) = id_option { - let mut project = Project::get(ProjectId(id as i64), executor).await?; - - if project.is_none() { - project = Project::get_from_slug(slug_or_project_id, executor).await?; - } - - Ok(project) - } else { - let project = Project::get_from_slug(slug_or_project_id, executor).await?; - - Ok(project) - } - } - - pub async fn get_full_from_slug_or_project_id<'a, 'b, E>( - slug_or_project_id: &str, - executor: E, - ) -> Result, sqlx::error::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { - let id_option = crate::models::ids::base62_impl::parse_base62(slug_or_project_id).ok(); - - if let Some(id) = id_option { - let mut project = Project::get_full(ProjectId(id as i64), executor).await?; - - if project.is_none() { - project = Project::get_full_from_slug(slug_or_project_id, executor).await?; - } - - Ok(project) - } else { - let project = Project::get_full_from_slug(slug_or_project_id, executor).await?; - Ok(project) - } - } - - pub async fn get_full<'a, 'b, E>( - id: ProjectId, - executor: E, - ) -> Result, sqlx::error::Error> + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - Project::get_many_full(&[id], executor) + Project::get_many(&[string], executor, redis) .await .map(|x| x.into_iter().next()) } - pub async fn get_many_full<'a, E>( + pub async fn get_id<'a, 'b, E>( + id: ProjectId, + executor: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + Project::get_many(&[crate::models::ids::ProjectId::from(id)], executor, redis) + .await + .map(|x| x.into_iter().next()) + } + + pub async fn get_many_ids<'a, E>( project_ids: &[ProjectId], exec: E, - ) -> Result, sqlx::Error> + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let ids = project_ids + .iter() + .map(|x| crate::models::ids::ProjectId::from(*x)) + .collect::>(); + Project::get_many(&ids, exec, redis).await + } + + pub async fn get_many<'a, E, T: ToString>( + project_strings: &[T], + exec: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { use futures::TryStreamExt; - let project_ids_parsed: Vec = project_ids.iter().map(|x| x.0).collect(); - sqlx::query!( - " - SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows, - m.icon_url icon_url, m.body body, m.published published, - m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status, - m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url, - m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body, - cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.webhook_sent, m.color, - m.loaders loaders, m.game_versions game_versions, m.thread_id thread_id, m.monetization_status monetization_status, - ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories, - ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories, - JSONB_AGG(DISTINCT jsonb_build_object('id', v.id, 'date_published', v.date_published)) filter (where v.id is not null) versions, - JSONB_AGG(DISTINCT jsonb_build_object('image_url', mg.image_url, 'featured', mg.featured, 'title', mg.title, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering)) filter (where mg.image_url is not null) gallery, - JSONB_AGG(DISTINCT jsonb_build_object('platform_id', md.joining_platform_id, 'platform_short', dp.short, 'platform_name', dp.name,'url', md.url)) filter (where md.joining_platform_id is not null) donations - FROM mods m - INNER JOIN project_types pt ON pt.id = m.project_type - INNER JOIN side_types cs ON m.client_side = cs.id - INNER JOIN side_types ss ON m.server_side = ss.id - LEFT JOIN mods_donations md ON md.joining_mod_id = m.id - LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id - LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id - LEFT JOIN categories c ON mc.joining_category_id = c.id - LEFT JOIN versions v ON v.mod_id = m.id AND v.status = ANY($2) - LEFT JOIN mods_gallery mg ON mg.mod_id = m.id - WHERE m.id = ANY($1) - GROUP BY pt.id, cs.id, ss.id, m.id; - ", - &project_ids_parsed, - &*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_listed()).map(|x| x.to_string()).collect::>() - ) - .fetch_many(exec) - .try_filter_map(|e| async { - Ok(e.right().map(|m| { - let id = m.id; + if project_strings.is_empty() { + return Ok(Vec::new()); + } + + let mut redis = redis.get().await?; + + let mut found_projects = Vec::new(); + let mut remaining_strings = project_strings + .iter() + .map(|x| x.to_string()) + .collect::>(); + + let mut project_ids = project_strings + .iter() + .flat_map(|x| parse_base62(&x.to_string()).map(|x| x as i64)) + .collect::>(); + + project_ids.append( + &mut cmd("MGET") + .arg( + project_strings + .iter() + .map(|x| { + format!( + "{}:{}", + PROJECTS_SLUGS_NAMESPACE, + x.to_string().to_lowercase() + ) + }) + .collect::>(), + ) + .query_async::<_, Vec>>(&mut redis) + .await? + .into_iter() + .flatten() + .collect(), + ); + + if !project_ids.is_empty() { + let projects = cmd("MGET") + .arg( + project_ids + .iter() + .map(|x| format!("{}:{}", PROJECTS_NAMESPACE, x)) + .collect::>(), + ) + .query_async::<_, Vec>>(&mut redis) + .await?; + + for project in projects { + if let Some(project) = + project.and_then(|x| serde_json::from_str::(&x).ok()) + { + remaining_strings.retain(|x| { + &to_base62(project.inner.id.0 as u64) != x + && project.inner.slug.as_ref() != Some(x) + }); + found_projects.push(project); + continue; + } + } + } + + if !remaining_strings.is_empty() { + let project_ids_parsed: Vec = remaining_strings + .iter() + .flat_map(|x| parse_base62(&x.to_string()).ok()) + .map(|x| x as i64) + .collect(); + let db_projects: Vec = sqlx::query!( + " + SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows, + m.icon_url icon_url, m.body body, m.published published, + m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status, + m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url, + m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body, + cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.webhook_sent, m.color, + m.thread_id thread_id, m.monetization_status monetization_status, + ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders, + JSONB_AGG(DISTINCT jsonb_build_object('id', gv.version, 'created', gv.created)) filter (where gv.version is not null) game_versions, + ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories, + ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories, + JSONB_AGG(DISTINCT jsonb_build_object('id', v.id, 'date_published', v.date_published)) filter (where v.id is not null) versions, + JSONB_AGG(DISTINCT jsonb_build_object('image_url', mg.image_url, 'featured', mg.featured, 'title', mg.title, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering)) filter (where mg.image_url is not null) gallery, + JSONB_AGG(DISTINCT jsonb_build_object('platform_id', md.joining_platform_id, 'platform_short', dp.short, 'platform_name', dp.name,'url', md.url)) filter (where md.joining_platform_id is not null) donations + FROM mods m + INNER JOIN project_types pt ON pt.id = m.project_type + INNER JOIN side_types cs ON m.client_side = cs.id + INNER JOIN side_types ss ON m.server_side = ss.id + LEFT JOIN mods_gallery mg ON mg.mod_id = m.id + LEFT JOIN mods_donations md ON md.joining_mod_id = m.id + LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id + LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id + LEFT JOIN categories c ON mc.joining_category_id = c.id + LEFT JOIN versions v ON v.mod_id = m.id AND v.status = ANY($3) + LEFT JOIN loaders_versions lv ON v.id = lv.version_id + LEFT JOIN loaders l ON lv.loader_id = l.id + LEFT JOIN game_versions_versions gvv ON v.id = gvv.joining_version_id + LEFT JOIN game_versions gv ON gvv.game_version_id = gv.id + WHERE m.id = ANY($1) OR m.slug = ANY($2) + GROUP BY pt.id, cs.id, ss.id, m.id; + ", + &project_ids_parsed, + &remaining_strings.into_iter().map(|x| x.to_string().to_lowercase()).collect::>(), + &*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_listed()).map(|x| x.to_string()).collect::>() + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|m| { + let id = m.id; QueryProject { inner: Project { @@ -716,8 +619,6 @@ impl Project { approved: m.approved, webhook_sent: m.webhook_sent, color: m.color.map(|x| x as u32), - loaders: m.loaders, - game_versions: m.game_versions, queued: m.queued, thread_id: m.thread_id.map(ThreadId), monetization_status: MonetizationStatus::from_str( @@ -728,94 +629,179 @@ impl Project { categories: m.categories.unwrap_or_default(), additional_categories: m.additional_categories.unwrap_or_default(), versions: { - #[derive(Deserialize)] - struct Version { - pub id: VersionId, - pub date_published: DateTime, + #[derive(Deserialize)] + struct Version { + pub id: VersionId, + pub date_published: DateTime, + } + + let mut versions: Vec = serde_json::from_value( + m.versions.unwrap_or_default(), + ) + .ok() + .unwrap_or_default(); + + versions.sort_by(|a, b| a.date_published.cmp(&b.date_published)); + + versions.into_iter().map(|x| x.id).collect() + }, + gallery_items: { + let mut gallery: Vec = serde_json::from_value( + m.gallery.unwrap_or_default(), + ).ok().unwrap_or_default(); + + gallery.sort_by(|a, b| a.ordering.cmp(&b.ordering)); + + gallery + }, + donation_urls: serde_json::from_value( + m.donations.unwrap_or_default(), + ).ok().unwrap_or_default(), + client_side: crate::models::projects::SideType::from_str(&m.client_side_type), + server_side: crate::models::projects::SideType::from_str(&m.server_side_type), + loaders: m.loaders.unwrap_or_default(), + game_versions: { + #[derive(Deserialize)] + struct GameVersion { + pub id: String, + pub created: DateTime, + } + + let mut game_versions: Vec = serde_json::from_value( + m.game_versions.unwrap_or_default(), + ) + .ok() + .unwrap_or_default(); + + game_versions.sort_by(|a, b| a.created.cmp(&b.created)); + + game_versions.into_iter().map(|x| x.id).collect() } + }})) + }) + .try_collect::>() + .await?; - let mut versions: Vec = serde_json::from_value( - m.versions.unwrap_or_default(), - ) - .ok() - .unwrap_or_default(); + for project in db_projects { + cmd("SET") + .arg(format!("{}:{}", PROJECTS_NAMESPACE, project.inner.id.0)) + .arg(serde_json::to_string(&project)?) + .arg("EX") + .arg(DEFAULT_EXPIRY) + .query_async::<_, ()>(&mut redis) + .await?; - versions.sort_by(|a, b| a.date_published.cmp(&b.date_published)); + if let Some(slug) = &project.inner.slug { + cmd("SET") + .arg(format!( + "{}:{}", + PROJECTS_SLUGS_NAMESPACE, + slug.to_lowercase() + )) + .arg(project.inner.id.0) + .arg("EX") + .arg(DEFAULT_EXPIRY) + .query_async::<_, ()>(&mut redis) + .await?; + } + found_projects.push(project); + } + } - versions.into_iter().map(|x| x.id).collect() - }, - gallery_items: { - let mut gallery: Vec = serde_json::from_value( - m.gallery.unwrap_or_default(), - ).ok().unwrap_or_default(); - - gallery.sort_by(|a, b| a.ordering.cmp(&b.ordering)); - - gallery - }, - donation_urls: serde_json::from_value( - m.donations.unwrap_or_default(), - ).ok().unwrap_or_default(), - client_side: crate::models::projects::SideType::from_str(&m.client_side_type), - server_side: crate::models::projects::SideType::from_str(&m.server_side_type), - }})) - }) - .try_collect::>() - .await + Ok(found_projects) } - pub async fn update_game_versions( + pub async fn get_dependencies<'a, E>( id: ProjectId, - transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - ) -> Result<(), sqlx::error::Error> { - sqlx::query!( - " - UPDATE mods - SET game_versions = ( - SELECT COALESCE(ARRAY_AGG(DISTINCT gv.version) filter (where gv.version is not null), array[]::varchar[]) - FROM versions v - INNER JOIN game_versions_versions gvv ON v.id = gvv.joining_version_id - INNER JOIN game_versions gv on gvv.game_version_id = gv.id - WHERE v.mod_id = mods.id AND v.status != ALL($2) - ) - WHERE id = $1 - ", - id as ProjectId, - &*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::>() - ) - .execute(&mut *transaction) + exec: E, + redis: &deadpool_redis::Pool, + ) -> Result, Option, Option)>, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + type Dependencies = Vec<(Option, Option, Option)>; + + use futures::stream::TryStreamExt; + + let mut redis = redis.get().await?; + + let dependencies = cmd("GET") + .arg(format!("{}:{}", PROJECTS_DEPENDENCIES_NAMESPACE, id.0)) + .query_async::<_, Option>(&mut redis) .await?; - Ok(()) + if let Some(dependencies) = + dependencies.and_then(|x| serde_json::from_str::(&x).ok()) + { + return Ok(dependencies); + } + + let dependencies: Dependencies = sqlx::query!( + " + SELECT d.dependency_id, COALESCE(vd.mod_id, 0) mod_id, d.mod_dependency_id + FROM versions v + INNER JOIN dependencies d ON d.dependent_id = v.id + LEFT JOIN versions vd ON d.dependency_id = vd.id + WHERE v.mod_id = $1 + ", + id as ProjectId + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|x| { + ( + x.dependency_id.map(VersionId), + if x.mod_id == Some(0) { + None + } else { + x.mod_id.map(ProjectId) + }, + x.mod_dependency_id.map(ProjectId), + ) + })) + }) + .try_collect::() + .await?; + + cmd("SET") + .arg(format!("{}:{}", PROJECTS_DEPENDENCIES_NAMESPACE, id.0)) + .arg(serde_json::to_string(&dependencies)?) + .arg("EX") + .arg(DEFAULT_EXPIRY) + .query_async::<_, ()>(&mut redis) + .await?; + + Ok(dependencies) } - pub async fn update_loaders( + pub async fn clear_cache( id: ProjectId, - transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - ) -> Result<(), sqlx::error::Error> { - sqlx::query!( - " - UPDATE mods - SET loaders = ( - SELECT COALESCE(ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null), array[]::varchar[]) - FROM versions v - INNER JOIN loaders_versions lv ON lv.version_id = v.id - INNER JOIN loaders l on lv.loader_id = l.id - WHERE v.mod_id = mods.id AND v.status != ALL($2) - ) - WHERE id = $1 - ", - id as ProjectId, - &*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::>() - ) - .execute(&mut *transaction) - .await?; + slug: Option, + clear_dependencies: Option, + redis: &deadpool_redis::Pool, + ) -> Result<(), DatabaseError> { + let mut redis = redis.get().await?; + let mut cmd = cmd("DEL"); + + cmd.arg(format!("{}:{}", PROJECTS_NAMESPACE, id.0)); + if let Some(slug) = slug { + cmd.arg(format!( + "{}:{}", + PROJECTS_SLUGS_NAMESPACE, + slug.to_lowercase() + )); + } + if clear_dependencies.unwrap_or(false) { + cmd.arg(format!("{}:{}", PROJECTS_DEPENDENCIES_NAMESPACE, id.0)); + } + + cmd.query_async::<_, ()>(&mut redis).await?; Ok(()) } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct QueryProject { pub inner: Project, pub project_type: String, @@ -826,4 +812,6 @@ pub struct QueryProject { pub gallery_items: Vec, pub client_side: crate::models::projects::SideType, pub server_side: crate::models::projects::SideType, + pub loaders: Vec, + pub game_versions: Vec, } diff --git a/src/database/models/session_item.rs b/src/database/models/session_item.rs new file mode 100644 index 000000000..78b3f757b --- /dev/null +++ b/src/database/models/session_item.rs @@ -0,0 +1,312 @@ +use super::ids::*; +use crate::database::models::DatabaseError; +use crate::models::ids::base62_impl::{parse_base62, to_base62}; +use chrono::{DateTime, Utc}; +use redis::cmd; +use serde::{Deserialize, Serialize}; + +const SESSIONS_NAMESPACE: &str = "sessions"; +const SESSIONS_IDS_NAMESPACE: &str = "sessions_ids"; +const SESSIONS_USERS_NAMESPACE: &str = "sessions_users"; +const DEFAULT_EXPIRY: i64 = 1800; // 30 minutes + +// TODO: Manage sessions cache + clear cache when needed + +pub struct SessionBuilder { + pub session: String, + pub user_id: UserId, + + pub os: Option, + pub platform: Option, + + pub city: Option, + pub country: Option, + + pub ip: String, + pub user_agent: String, +} + +impl SessionBuilder { + pub async fn insert( + &self, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + let id = generate_session_id(&mut *transaction).await?; + + sqlx::query!( + " + INSERT INTO sessions ( + id, session, user_id, os, platform, + city, country, ip, user_agent + ) + VALUES ( + $1, $2, $3, $4, $5, + $6, $7, $8, $9 + ) + ", + id as SessionId, + self.session, + self.user_id as UserId, + self.os, + self.platform, + self.city, + self.country, + self.ip, + self.user_agent, + ) + .execute(&mut *transaction) + .await?; + + Ok(id) + } +} + +#[derive(Deserialize, Serialize)] +pub struct Session { + pub id: SessionId, + pub session: String, + pub user_id: UserId, + + pub created: DateTime, + pub last_login: DateTime, + pub expires: DateTime, + pub refresh_expires: DateTime, + + pub os: Option, + pub platform: Option, + pub user_agent: String, + + pub city: Option, + pub country: Option, + pub ip: String, +} + +impl Session { + pub async fn get<'a, E, T: ToString>( + id: T, + exec: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + Self::get_many(&[id], exec, redis) + .await + .map(|x| x.into_iter().next()) + } + + pub async fn get_id<'a, 'b, E>( + id: SessionId, + executor: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + Session::get_many(&[crate::models::ids::SessionId::from(id)], executor, redis) + .await + .map(|x| x.into_iter().next()) + } + + pub async fn get_many_ids<'a, E>( + user_ids: &[SessionId], + exec: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let ids = user_ids + .iter() + .map(|x| crate::models::ids::SessionId::from(*x)) + .collect::>(); + Session::get_many(&ids, exec, redis).await + } + + pub async fn get_many<'a, E, T: ToString>( + session_strings: &[T], + exec: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + use futures::TryStreamExt; + + if session_strings.is_empty() { + return Ok(Vec::new()); + } + + let mut redis = redis.get().await?; + + let mut found_sessions = Vec::new(); + let mut remaining_strings = session_strings + .iter() + .map(|x| x.to_string()) + .collect::>(); + + let mut session_ids = session_strings + .iter() + .flat_map(|x| parse_base62(&x.to_string()).map(|x| x as i64)) + .collect::>(); + + session_ids.append( + &mut cmd("MGET") + .arg( + session_strings + .iter() + .map(|x| format!("{}:{}", SESSIONS_IDS_NAMESPACE, x.to_string())) + .collect::>(), + ) + .query_async::<_, Vec>>(&mut redis) + .await? + .into_iter() + .flatten() + .collect(), + ); + + if !session_ids.is_empty() { + let sessions = cmd("MGET") + .arg( + session_ids + .iter() + .map(|x| format!("{}:{}", SESSIONS_NAMESPACE, x)) + .collect::>(), + ) + .query_async::<_, Vec>>(&mut redis) + .await?; + + for session in sessions { + if let Some(session) = + session.and_then(|x| serde_json::from_str::(&x).ok()) + { + remaining_strings + .retain(|x| &to_base62(session.id.0 as u64) != x && &session.session != x); + found_sessions.push(session); + continue; + } + } + } + + if !remaining_strings.is_empty() { + let session_ids_parsed: Vec = session_strings + .iter() + .flat_map(|x| parse_base62(&x.to_string()).ok()) + .map(|x| x as i64) + .collect(); + let db_sessions: Vec = sqlx::query!( + " + SELECT id, user_id, session, created, last_login, expires, refresh_expires, os, platform, + city, country, ip, user_agent + FROM sessions + WHERE id = ANY($1) OR session = ANY($2) + ORDER BY created DESC + ", + &session_ids_parsed, + &session_strings.into_iter().map(|x| x.to_string()).collect::>(), + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|x| Session { + id: SessionId(x.id), + session: x.session, + user_id: UserId(x.user_id), + created: x.created, + last_login: x.last_login, + expires: x.expires, + refresh_expires: x.refresh_expires, + os: x.os, + platform: x.platform, + city: x.city, + country: x.country, + ip: x.ip, + user_agent: x.user_agent, + })) + }) + .try_collect::>() + .await?; + + for session in db_sessions { + cmd("SET") + .arg(format!("{}:{}", SESSIONS_NAMESPACE, session.id.0)) + .arg(serde_json::to_string(&session)?) + .arg("EX") + .arg(DEFAULT_EXPIRY) + .query_async::<_, ()>(&mut redis) + .await?; + + cmd("SET") + .arg(format!("{}:{}", SESSIONS_IDS_NAMESPACE, session.session)) + .arg(session.id.0) + .arg("EX") + .arg(DEFAULT_EXPIRY) + .query_async::<_, ()>(&mut redis) + .await?; + found_sessions.push(session); + } + } + + Ok(found_sessions) + } + + pub async fn get_user_sessions<'a, E>( + user_id: UserId, + exec: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let mut redis = redis.get().await?; + let res = cmd("GET") + .arg(format!("{}:{}", SESSIONS_USERS_NAMESPACE, user_id.0)) + .query_async::<_, Option>>(&mut redis) + .await?; + + if let Some(res) = res { + return Ok(res.into_iter().map(SessionId).collect()); + } + + use futures::TryStreamExt; + let db_sessions: Vec = sqlx::query!( + " + SELECT id + FROM sessions + WHERE user_id = $1 + ORDER BY created DESC + ", + user_id.0, + ) + .fetch_many(exec) + .try_filter_map(|e| async { Ok(e.right().map(|x| SessionId(x.id))) }) + .try_collect::>() + .await?; + + cmd("SET") + .arg(format!("{}:{}", SESSIONS_USERS_NAMESPACE, user_id.0)) + .arg(serde_json::to_string(&db_sessions)?) + .arg("EX") + .arg(DEFAULT_EXPIRY) + .query_async::<_, ()>(&mut redis) + .await?; + + Ok(db_sessions) + } + + pub async fn remove( + id: SessionId, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + // redis: &deadpool_redis::Pool, + ) -> Result, sqlx::error::Error> { + sqlx::query!( + " + DELETE FROM sessions WHERE id = $1 + ", + id as SessionId, + ) + .execute(&mut *transaction) + .await?; + + Ok(Some(())) + } +} diff --git a/src/database/models/team_item.rs b/src/database/models/team_item.rs index ef1d1156a..740bb39be 100644 --- a/src/database/models/team_item.rs +++ b/src/database/models/team_item.rs @@ -1,8 +1,14 @@ use super::ids::*; use crate::database::models::User; use crate::models::teams::Permissions; -use crate::models::users::{Badges, RecipientType, RecipientWallet}; +use crate::models::users::Badges; +use itertools::Itertools; +use redis::cmd; use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; + +const TEAMS_NAMESPACE: &str = "teams"; +const DEFAULT_EXPIRY: i64 = 1800; pub struct TeamBuilder { pub members: Vec, @@ -90,6 +96,7 @@ pub struct TeamMember { } /// A member of a team +#[derive(Deserialize, Serialize)] pub struct QueryTeamMember { pub id: TeamMemberId, pub team_id: TeamId, @@ -107,81 +114,139 @@ impl TeamMember { pub async fn get_from_team_full<'a, 'b, E>( id: TeamId, executor: E, + redis: &deadpool_redis::Pool, ) -> Result, super::DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { - Self::get_from_team_full_many(&[id], executor).await + Self::get_from_team_full_many(&[id], executor, redis).await } pub async fn get_from_team_full_many<'a, E>( team_ids: &[TeamId], exec: E, + redis: &deadpool_redis::Pool, ) -> Result, super::DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { + if team_ids.is_empty() { + return Ok(Vec::new()); + } + use futures::stream::TryStreamExt; - let team_ids_parsed: Vec = team_ids.iter().map(|x| x.0).collect(); + let mut team_ids_parsed: Vec = team_ids.iter().map(|x| x.0).collect(); - let teams = sqlx::query!( - " - SELECT tm.id id, tm.team_id team_id, tm.role member_role, tm.permissions permissions, tm.accepted accepted, tm.payouts_split payouts_split, tm.ordering, - u.id user_id, u.name user_name, u.email email, u.kratos_id kratos_id, u.github_id github_id, - u.avatar_url avatar_url, u.username username, u.bio bio, - u.created created, u.role user_role, u.badges badges, u.balance balance, - u.payout_wallet payout_wallet, u.payout_wallet_type payout_wallet_type, - u.payout_address payout_address - FROM team_members tm - INNER JOIN users u ON u.id = tm.user_id - WHERE tm.team_id = ANY($1) - ORDER BY tm.team_id, tm.ordering - ", - &team_ids_parsed - ) - .fetch_many(exec) - .try_filter_map(|e| async { - if let Some(m) = e.right() { + let mut redis = redis.get().await?; - Ok(Some(Ok(QueryTeamMember { - id: TeamMemberId(m.id), - team_id: TeamId(m.team_id), - role: m.member_role, - permissions: Permissions::from_bits(m.permissions as u64).unwrap_or_default(), - accepted: m.accepted, - user: User { - id: UserId(m.user_id), - github_id: m.github_id, - kratos_id: m.kratos_id, - name: m.user_name, - email: m.email, - avatar_url: m.avatar_url, - username: m.username, - bio: m.bio, - created: m.created, - role: m.user_role, - badges: Badges::from_bits(m.badges as u64).unwrap_or_default(), - balance: m.balance, - payout_wallet: m.payout_wallet.map(|x| RecipientWallet::from_string(&x)), - payout_wallet_type: m.payout_wallet_type.map(|x| RecipientType::from_string(&x)), - payout_address: m.payout_address, - }, - payouts_split: m.payouts_split, - ordering: m.ordering, - }))) - } else { - Ok(None) - } - }) - .try_collect::>>() - .await?; + let mut found_teams = Vec::new(); - let team_members = teams - .into_iter() - .collect::, super::DatabaseError>>()?; + let teams = cmd("MGET") + .arg( + team_ids_parsed + .iter() + .map(|x| format!("{}:{}", TEAMS_NAMESPACE, x)) + .collect::>(), + ) + .query_async::<_, Vec>>(&mut redis) + .await?; - Ok(team_members) + for team_raw in teams { + if let Some(mut team) = team_raw + .clone() + .and_then(|x| serde_json::from_str::>(&x).ok()) + { + if let Some(team_id) = team.first().map(|x| x.team_id) { + team_ids_parsed.retain(|x| &team_id.0 != x); + } + + found_teams.append(&mut team); + continue; + } + } + + if !team_ids_parsed.is_empty() { + let teams: Vec = sqlx::query!( + " + SELECT tm.id id, tm.team_id team_id, tm.role member_role, tm.permissions permissions, tm.accepted accepted, tm.payouts_split payouts_split, tm.ordering, + u.id user_id, u.name user_name, + u.avatar_url avatar_url, u.username username, u.bio bio, + u.created created, u.role user_role, u.badges badges + FROM team_members tm + INNER JOIN users u ON u.id = tm.user_id + WHERE tm.team_id = ANY($1) + ORDER BY tm.team_id, tm.ordering + ", + &team_ids_parsed + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|m| + QueryTeamMember { + id: TeamMemberId(m.id), + team_id: TeamId(m.team_id), + role: m.member_role, + permissions: Permissions::from_bits(m.permissions as u64).unwrap_or_default(), + accepted: m.accepted, + user: User { + id: UserId(m.user_id), + github_id: None, + discord_id: None, + gitlab_id: None, + google_id: None, + steam_id: None, + name: m.user_name, + email: None, + avatar_url: m.avatar_url, + username: m.username, + bio: m.bio, + created: m.created, + role: m.user_role, + badges: Badges::from_bits(m.badges as u64).unwrap_or_default(), + balance: Decimal::ZERO, + payout_wallet: None, + payout_wallet_type: None, + payout_address: None, + microsoft_id: None, + }, + payouts_split: m.payouts_split, + ordering: m.ordering, + } + )) + }) + .try_collect::>() + .await?; + + for (id, members) in &teams.into_iter().group_by(|x| x.team_id) { + let mut members = members.collect::>(); + + cmd("SET") + .arg(format!("{}:{}", TEAMS_NAMESPACE, id.0)) + .arg(serde_json::to_string(&members)?) + .arg("EX") + .arg(DEFAULT_EXPIRY) + .query_async::<_, ()>(&mut redis) + .await?; + + found_teams.append(&mut members); + } + } + + Ok(found_teams) + } + + pub async fn clear_cache( + id: TeamId, + redis: &deadpool_redis::Pool, + ) -> Result<(), super::DatabaseError> { + let mut redis = redis.get().await?; + cmd("DEL") + .arg(format!("{}:{}", TEAMS_NAMESPACE, id.0)) + .query_async::<_, ()>(&mut redis) + .await?; + + Ok(()) } /// Gets a team member from a user id and team id. Does not return pending members. diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs index 97e561f48..71b8ad2f2 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -1,12 +1,28 @@ use super::ids::{ProjectId, UserId}; +use crate::database::models::DatabaseError; +use crate::models::ids::base62_impl::{parse_base62, to_base62}; use crate::models::users::{Badges, RecipientType, RecipientWallet}; use chrono::{DateTime, Utc}; +use redis::cmd; use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; +const USERS_NAMESPACE: &str = "users"; +const USER_USERNAMES_NAMESPACE: &str = "users_usernames"; +// const USERS_PROJECTS_NAMESPACE: &str = "users_projects"; +const DEFAULT_EXPIRY: i64 = 1800; // 30 minutes + +#[derive(Deserialize, Serialize)] pub struct User { pub id: UserId, - pub kratos_id: Option, // None if legacy user unconnected to Minos/Kratos + pub github_id: Option, + pub discord_id: Option, + pub gitlab_id: Option, + pub google_id: Option, + pub steam_id: Option, + pub microsoft_id: Option, + pub username: String, pub name: Option, pub email: Option, @@ -29,22 +45,29 @@ impl User { sqlx::query!( " INSERT INTO users ( - id, kratos_id, username, name, email, - avatar_url, bio, created + id, username, name, email, + avatar_url, bio, created, + github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id ) VALUES ( $1, $2, $3, $4, $5, - $6, $7, $8 + $6, $7, + $8, $9, $10, $11, $12, $13 ) ", self.id as UserId, - self.kratos_id, &self.username, self.name.as_ref(), self.email.as_ref(), self.avatar_url.as_ref(), self.bio.as_ref(), self.created, + self.github_id, + self.discord_id, + self.gitlab_id, + self.google_id, + self.steam_id, + self.microsoft_id, ) .execute(&mut *transaction) .await?; @@ -52,199 +75,192 @@ impl User { Ok(()) } - pub async fn get<'a, 'b, E>(id: UserId, executor: E) -> Result, sqlx::error::Error> + pub async fn get<'a, 'b, E>( + string: &str, + executor: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - Self::get_many(&[id], executor) + User::get_many(&[string], executor, redis) .await .map(|x| x.into_iter().next()) } - pub async fn get_from_github_id<'a, 'b, E>( - github_id: u64, + pub async fn get_id<'a, 'b, E>( + id: UserId, executor: E, - ) -> Result, sqlx::error::Error> + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - let result = sqlx::query!( - " - SELECT u.id, u.name, u.email, u.kratos_id, - u.avatar_url, u.username, u.bio, - u.created, u.role, u.badges, - u.balance, u.payout_wallet, u.payout_wallet_type, - u.payout_address - FROM users u - WHERE u.github_id = $1 - ", - github_id as i64, - ) - .fetch_optional(executor) - .await?; - - if let Some(row) = result { - Ok(Some(User { - id: UserId(row.id), - github_id: Some(github_id as i64), - name: row.name, - email: row.email, - kratos_id: row.kratos_id, - avatar_url: row.avatar_url, - username: row.username, - bio: row.bio, - created: row.created, - role: row.role, - badges: Badges::from_bits(row.badges as u64).unwrap_or_default(), - balance: row.balance, - payout_wallet: row.payout_wallet.map(|x| RecipientWallet::from_string(&x)), - payout_wallet_type: row - .payout_wallet_type - .map(|x| RecipientType::from_string(&x)), - payout_address: row.payout_address, - })) - } else { - Ok(None) - } + User::get_many(&[crate::models::ids::UserId::from(id)], executor, redis) + .await + .map(|x| x.into_iter().next()) } - pub async fn get_from_minos_kratos_id<'a, 'b, E>( - kratos_id: String, - executor: E, - ) -> Result, sqlx::error::Error> + pub async fn get_many_ids<'a, E>( + user_ids: &[UserId], + exec: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - let result = sqlx::query!( - " - SELECT u.id, u.name, u.kratos_id, u.email, u.github_id, - u.avatar_url, u.username, u.bio, - u.created, u.role, u.badges, - u.balance, u.payout_wallet, u.payout_wallet_type, - u.payout_address - FROM users u - WHERE u.kratos_id = $1 - ", - kratos_id as String, - ) - .fetch_optional(executor) - .await?; - - if let Some(row) = result { - Ok(Some(User { - id: UserId(row.id), - kratos_id: row.kratos_id, - github_id: row.github_id, - name: row.name, - email: row.email, - avatar_url: row.avatar_url, - username: row.username, - bio: row.bio, - created: row.created, - role: row.role, - badges: Badges::from_bits(row.badges as u64).unwrap_or_default(), - balance: row.balance, - payout_wallet: row.payout_wallet.map(|x| RecipientWallet::from_string(&x)), - payout_wallet_type: row - .payout_wallet_type - .map(|x| RecipientType::from_string(&x)), - payout_address: row.payout_address, - })) - } else { - Ok(None) - } + let ids = user_ids + .iter() + .map(|x| crate::models::ids::UserId::from(*x)) + .collect::>(); + User::get_many(&ids, exec, redis).await } - pub async fn get_from_username<'a, 'b, E>( - username: String, - executor: E, - ) -> Result, sqlx::error::Error> + pub async fn get_many<'a, E, T: ToString>( + users_strings: &[T], + exec: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - let result = sqlx::query!( - " - SELECT u.id, u.kratos_id, u.name, u.email, u.github_id, - u.avatar_url, u.username, u.bio, - u.created, u.role, u.badges, - u.balance, u.payout_wallet, u.payout_wallet_type, - u.payout_address - FROM users u - WHERE LOWER(u.username) = LOWER($1) - ", - username - ) - .fetch_optional(executor) - .await?; + use futures::TryStreamExt; - if let Some(row) = result { - Ok(Some(User { - id: UserId(row.id), - kratos_id: row.kratos_id, - github_id: row.github_id, - name: row.name, - email: row.email, - avatar_url: row.avatar_url, - username: row.username, - bio: row.bio, - created: row.created, - role: row.role, - badges: Badges::from_bits(row.badges as u64).unwrap_or_default(), - balance: row.balance, - payout_wallet: row.payout_wallet.map(|x| RecipientWallet::from_string(&x)), - payout_wallet_type: row - .payout_wallet_type - .map(|x| RecipientType::from_string(&x)), - payout_address: row.payout_address, - })) - } else { - Ok(None) + if users_strings.is_empty() { + return Ok(Vec::new()); } - } - pub async fn get_many<'a, E>(user_ids: &[UserId], exec: E) -> Result, sqlx::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { - use futures::stream::TryStreamExt; + let mut redis = redis.get().await?; - let user_ids_parsed: Vec = user_ids.iter().map(|x| x.0).collect(); - let users = sqlx::query!( - " - SELECT u.id, u.kratos_id, u.name, u.email, u.github_id, - u.avatar_url, u.username, u.bio, - u.created, u.role, u.badges, - u.balance, u.payout_wallet, u.payout_wallet_type, - u.payout_address - FROM users u - WHERE u.id = ANY($1) - ", - &user_ids_parsed - ) - .fetch_many(exec) - .try_filter_map(|e| async { - Ok(e.right().map(|u| User { - id: UserId(u.id), - kratos_id: u.kratos_id, - github_id: u.github_id, - name: u.name, - email: u.email, - avatar_url: u.avatar_url, - username: u.username, - bio: u.bio, - created: u.created, - role: u.role, - badges: Badges::from_bits(u.badges as u64).unwrap_or_default(), - balance: u.balance, - payout_wallet: u.payout_wallet.map(|x| RecipientWallet::from_string(&x)), - payout_wallet_type: u.payout_wallet_type.map(|x| RecipientType::from_string(&x)), - payout_address: u.payout_address, - })) - }) - .try_collect::>() - .await?; + let mut found_users = Vec::new(); + let mut remaining_strings = users_strings + .iter() + .map(|x| x.to_string()) + .collect::>(); - Ok(users) + let mut user_ids = users_strings + .iter() + .flat_map(|x| parse_base62(&x.to_string()).map(|x| x as i64)) + .collect::>(); + + user_ids.append( + &mut cmd("MGET") + .arg( + users_strings + .iter() + .map(|x| { + format!( + "{}:{}", + USER_USERNAMES_NAMESPACE, + x.to_string().to_lowercase() + ) + }) + .collect::>(), + ) + .query_async::<_, Vec>>(&mut redis) + .await? + .into_iter() + .flatten() + .collect(), + ); + + if !user_ids.is_empty() { + let users = cmd("MGET") + .arg( + user_ids + .iter() + .map(|x| format!("{}:{}", USERS_NAMESPACE, x)) + .collect::>(), + ) + .query_async::<_, Vec>>(&mut redis) + .await?; + + for user in users { + if let Some(user) = user.and_then(|x| serde_json::from_str::(&x).ok()) { + remaining_strings + .retain(|x| &to_base62(user.id.0 as u64) != x && &user.username != x); + found_users.push(user); + continue; + } + } + } + + if !remaining_strings.is_empty() { + let user_ids_parsed: Vec = remaining_strings + .iter() + .flat_map(|x| parse_base62(&x.to_string()).ok()) + .map(|x| x as i64) + .collect(); + let db_users: Vec = sqlx::query!( + " + SELECT id, name, email, + avatar_url, username, bio, + created, role, badges, + balance, payout_wallet, payout_wallet_type, payout_address, + github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id + FROM users + WHERE id = ANY($1) OR username = ANY($2) + ", + &user_ids_parsed, + &remaining_strings + .into_iter() + .map(|x| x.to_string().to_lowercase()) + .collect::>(), + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|u| User { + id: UserId(u.id), + github_id: u.github_id, + discord_id: u.discord_id, + gitlab_id: u.gitlab_id, + google_id: u.google_id, + steam_id: u.steam_id, + microsoft_id: u.microsoft_id, + name: u.name, + email: u.email, + avatar_url: u.avatar_url, + username: u.username, + bio: u.bio, + created: u.created, + role: u.role, + badges: Badges::from_bits(u.badges as u64).unwrap_or_default(), + balance: u.balance, + payout_wallet: u.payout_wallet.map(|x| RecipientWallet::from_string(&x)), + payout_wallet_type: u + .payout_wallet_type + .map(|x| RecipientType::from_string(&x)), + payout_address: u.payout_address, + })) + }) + .try_collect::>() + .await?; + + for user in db_users { + cmd("SET") + .arg(format!("{}:{}", USERS_NAMESPACE, user.id.0)) + .arg(serde_json::to_string(&user)?) + .arg("EX") + .arg(DEFAULT_EXPIRY) + .query_async::<_, ()>(&mut redis) + .await?; + + cmd("SET") + .arg(format!( + "{}:{}", + USER_USERNAMES_NAMESPACE, + user.username.to_lowercase() + )) + .arg(user.id.0) + .arg("EX") + .arg(DEFAULT_EXPIRY) + .query_async::<_, ()>(&mut redis) + .await?; + found_users.push(user); + } + } + + Ok(found_users) } pub async fn get_projects<'a, E>( @@ -273,321 +289,207 @@ impl User { Ok(projects) } - pub async fn remove( - id: UserId, - transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - ) -> Result, sqlx::error::Error> { - let deleted_user: UserId = crate::models::users::DELETED_USER.into(); + pub async fn clear_caches( + user_ids: &[(UserId, Option)], + redis: &deadpool_redis::Pool, + ) -> Result<(), DatabaseError> { + let mut redis = redis.get().await?; + let mut cmd = cmd("DEL"); - sqlx::query!( - " - UPDATE team_members - SET user_id = $1 - WHERE (user_id = $2 AND role = $3) - ", - deleted_user as UserId, - id as UserId, - crate::models::teams::OWNER_ROLE - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - " - UPDATE versions - SET author_id = $1 - WHERE (author_id = $2) - ", - deleted_user as UserId, - id as UserId, - ) - .execute(&mut *transaction) - .await?; - - use futures::TryStreamExt; - let notifications: Vec = sqlx::query!( - " - SELECT n.id FROM notifications n - WHERE n.user_id = $1 - ", - id as UserId, - ) - .fetch_many(&mut *transaction) - .try_filter_map(|e| async { Ok(e.right().map(|m| m.id)) }) - .try_collect::>() - .await?; - - sqlx::query!( - " - DELETE FROM notifications - WHERE user_id = $1 - ", - id as UserId, - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - " - DELETE FROM reports - WHERE user_id = $1 - ", - id as UserId, - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - " - DELETE FROM mod_follows - WHERE follower_id = $1 - ", - id as UserId, - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - " - DELETE FROM notifications_actions - WHERE notification_id = ANY($1) - ", - ¬ifications - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - " - DELETE FROM team_members - WHERE user_id = $1 - ", - id as UserId, - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - " - DELETE FROM payouts_values - WHERE user_id = $1 - ", - id as UserId, - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - " - DELETE FROM historical_payouts - WHERE user_id = $1 - ", - id as UserId, - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - " - DELETE FROM users - WHERE id = $1 - ", - id as UserId, - ) - .execute(&mut *transaction) - .await?; - - Ok(Some(())) - } - - pub async fn remove_full( - id: UserId, - transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - ) -> Result, sqlx::error::Error> { - use futures::TryStreamExt; - let projects: Vec = sqlx::query!( - " - SELECT m.id FROM mods m - INNER JOIN team_members tm ON tm.team_id = m.team_id - WHERE tm.user_id = $1 AND tm.role = $2 - ", - id as UserId, - crate::models::teams::OWNER_ROLE - ) - .fetch_many(&mut *transaction) - .try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) }) - .try_collect::>() - .await?; - - for project_id in projects { - let _result = - super::project_item::Project::remove_full(project_id, transaction).await?; - } - - let notifications: Vec = sqlx::query!( - " - SELECT n.id FROM notifications n - WHERE n.user_id = $1 - ", - id as UserId, - ) - .fetch_many(&mut *transaction) - .try_filter_map(|e| async { Ok(e.right().map(|m| m.id)) }) - .try_collect::>() - .await?; - - sqlx::query!( - " - DELETE FROM notifications - WHERE user_id = $1 - ", - id as UserId, - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - " - DELETE FROM notifications_actions - WHERE notification_id = ANY($1) - ", - ¬ifications - ) - .execute(&mut *transaction) - .await?; - - let deleted_user: UserId = crate::models::users::DELETED_USER.into(); - - sqlx::query!( - " - UPDATE versions - SET author_id = $1 - WHERE (author_id = $2) - ", - deleted_user as UserId, - id as UserId, - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - " - DELETE FROM team_members - WHERE user_id = $1 - ", - id as UserId, - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - r#" - UPDATE threads_messages - SET body = '{"type": "deleted"}', author_id = $2 - WHERE author_id = $1 - "#, - id as UserId, - deleted_user as UserId, - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - " - DELETE FROM threads_members - WHERE user_id = $1 - ", - id as UserId, - ) - .execute(&mut *transaction) - .await?; - - sqlx::query!( - " - DELETE FROM users - WHERE id = $1 - ", - id as UserId, - ) - .execute(&mut *transaction) - .await?; - - Ok(Some(())) - } - - pub async fn get_id_from_username_or_id<'a, 'b, E>( - username_or_id: &str, - executor: E, - ) -> Result, sqlx::error::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { - let id_option = crate::models::ids::base62_impl::parse_base62(username_or_id).ok(); - - if let Some(id) = id_option { - let id = UserId(id as i64); - - let mut user_id = sqlx::query!( - " - SELECT id FROM users - WHERE id = $1 - ", - id as UserId - ) - .fetch_optional(executor) - .await? - .map(|x| UserId(x.id)); - - if user_id.is_none() { - user_id = sqlx::query!( - " - SELECT id FROM users - WHERE LOWER(username) = LOWER($1) - ", - username_or_id - ) - .fetch_optional(executor) - .await? - .map(|x| UserId(x.id)); + for (id, username) in user_ids { + cmd.arg(format!("{}:{}", USERS_NAMESPACE, id.0)); + if let Some(username) = username { + cmd.arg(format!( + "{}:{}", + USER_USERNAMES_NAMESPACE, + username.to_lowercase() + )); } - - Ok(user_id) - } else { - let id = sqlx::query!( - " - SELECT id FROM users - WHERE LOWER(username) = LOWER($1) - ", - username_or_id - ) - .fetch_optional(executor) - .await?; - - Ok(id.map(|x| UserId(x.id))) } - } - pub async fn merge_minos_user<'a, 'b, E>( - &self, - kratos_id: &str, - executor: E, - ) -> Result<(), sqlx::error::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - // If the user exists, link the Minos user into the existing user rather tham create a new one - sqlx::query!( - " - UPDATE users - SET kratos_id = $1 - WHERE (id = $2) - ", - kratos_id, - self.id.0, - ) - .execute(executor) - .await?; + cmd.query_async::<_, ()>(&mut redis).await?; Ok(()) } + + pub async fn remove( + id: UserId, + full: bool, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> { + let user = Self::get_id(id, &mut *transaction, redis).await?; + + if let Some(delete_user) = user { + User::clear_caches(&[(id, Some(delete_user.username))], redis).await?; + + let deleted_user: UserId = crate::models::users::DELETED_USER.into(); + + if full { + let projects: Vec = sqlx::query!( + " + SELECT m.id FROM mods m + INNER JOIN team_members tm ON tm.team_id = m.team_id + WHERE tm.user_id = $1 AND tm.role = $2 + ", + id as UserId, + crate::models::teams::OWNER_ROLE + ) + .fetch_many(&mut *transaction) + .try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) }) + .try_collect::>() + .await?; + + for project_id in projects { + let _result = + super::project_item::Project::remove(project_id, transaction, redis) + .await?; + } + } else { + sqlx::query!( + " + UPDATE team_members + SET user_id = $1 + WHERE (user_id = $2 AND role = $3) + ", + deleted_user as UserId, + id as UserId, + crate::models::teams::OWNER_ROLE + ) + .execute(&mut *transaction) + .await?; + } + + sqlx::query!( + " + UPDATE versions + SET author_id = $1 + WHERE (author_id = $2) + ", + deleted_user as UserId, + id as UserId, + ) + .execute(&mut *transaction) + .await?; + + use futures::TryStreamExt; + let notifications: Vec = sqlx::query!( + " + SELECT n.id FROM notifications n + WHERE n.user_id = $1 + ", + id as UserId, + ) + .fetch_many(&mut *transaction) + .try_filter_map(|e| async { Ok(e.right().map(|m| m.id)) }) + .try_collect::>() + .await?; + + sqlx::query!( + " + DELETE FROM notifications + WHERE user_id = $1 + ", + id as UserId, + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + DELETE FROM notifications_actions + WHERE notification_id = ANY($1) + ", + ¬ifications + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + DELETE FROM reports + WHERE user_id = $1 + ", + id as UserId, + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + DELETE FROM mod_follows + WHERE follower_id = $1 + ", + id as UserId, + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + DELETE FROM team_members + WHERE user_id = $1 + ", + id as UserId, + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + DELETE FROM payouts_values + WHERE user_id = $1 + ", + id as UserId, + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + DELETE FROM historical_payouts + WHERE user_id = $1 + ", + id as UserId, + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + r#" + UPDATE threads_messages + SET body = '{"type": "deleted"}', author_id = $2 + WHERE author_id = $1 + "#, + id as UserId, + deleted_user as UserId, + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + DELETE FROM threads_members + WHERE user_id = $1 + ", + id as UserId, + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + DELETE FROM users + WHERE id = $1 + ", + id as UserId, + ) + .execute(&mut *transaction) + .await?; + + Ok(Some(())) + } else { + Ok(None) + } + } } diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index bbef408dd..11fb453b9 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -1,12 +1,20 @@ use super::ids::*; use super::DatabaseError; use crate::models::ids::base62_impl::parse_base62; -use crate::models::projects::{FileType, VersionStatus, VersionType}; +use crate::models::projects::{FileType, VersionStatus}; use chrono::{DateTime, Utc}; -use serde::Deserialize; +use itertools::Itertools; +use redis::cmd; +use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::collections::HashMap; +const VERSIONS_NAMESPACE: &str = "versions"; +// TODO: Cache version slugs call +// const VERSIONS_SLUGS_NAMESPACE: &str = "versions_slugs"; +const VERSION_FILES_NAMESPACE: &str = "versions_files"; +const DEFAULT_EXPIRY: i64 = 1800; // 30 minutes + pub struct VersionBuilder { pub version_id: VersionId, pub project_id: ProjectId, @@ -199,7 +207,7 @@ impl VersionBuilder { } } -#[derive(Clone)] +#[derive(Clone, Deserialize, Serialize)] pub struct Version { pub id: VersionId, pub project_id: ProjectId, @@ -254,20 +262,18 @@ impl Version { pub async fn remove_full( id: VersionId, + redis: &deadpool_redis::Pool, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - ) -> Result, sqlx::Error> { - let result = sqlx::query!( - " - SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1) - ", - id as VersionId, - ) - .fetch_one(&mut *transaction) - .await?; + ) -> Result, DatabaseError> { + let result = Self::get(id, &mut *transaction, redis).await?; - if !result.exists.unwrap_or(false) { + let result = if let Some(result) = result { + result + } else { return Ok(None); - } + }; + + Version::clear_cache(&result, redis).await?; sqlx::query!( " @@ -374,276 +380,383 @@ impl Version { .execute(&mut *transaction) .await?; - crate::database::models::Project::update_game_versions( - ProjectId(project_id.mod_id), - &mut *transaction, - ) - .await?; - crate::database::models::Project::update_loaders( - ProjectId(project_id.mod_id), - &mut *transaction, - ) - .await?; - Ok(Some(())) } - pub async fn get_project_versions<'a, E>( - project_id: ProjectId, - game_versions: Option>, - loaders: Option>, - version_type: Option, - limit: Option, - offset: Option, - exec: E, - ) -> Result, sqlx::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - use futures::stream::TryStreamExt; - - let vec = sqlx::query!( - " - SELECT DISTINCT ON(v.date_published, v.id) version_id, v.date_published FROM versions v - INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id - INNER JOIN game_versions gv on gvv.game_version_id = gv.id AND (cardinality($2::varchar[]) = 0 OR gv.version = ANY($2::varchar[])) - INNER JOIN loaders_versions lv ON lv.version_id = v.id - INNER JOIN loaders l on lv.loader_id = l.id AND (cardinality($3::varchar[]) = 0 OR l.loader = ANY($3::varchar[])) - WHERE v.mod_id = $1 AND ($4::varchar IS NULL OR v.version_type = $4) - ORDER BY v.date_published DESC, v.id - LIMIT $5 OFFSET $6 - ", - project_id as ProjectId, - &game_versions.unwrap_or_default(), - &loaders.unwrap_or_default(), - version_type.map(|x| x.as_str()), - limit.map(|x| x as i64), - offset.map(|x| x as i64), - ) - .fetch_many(exec) - .try_filter_map(|e| async { Ok(e.right().map(|v| VersionId(v.version_id))) }) - .try_collect::>() - .await?; - - Ok(vec) - } - - pub async fn get_projects_versions<'a, E>( - project_ids: Vec, - game_versions: Option>, - loaders: Option>, - version_type: Option, - limit: Option, - offset: Option, - exec: E, - ) -> Result>, sqlx::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - use futures::stream::TryStreamExt; - - let vec = sqlx::query!( - " - SELECT DISTINCT ON(v.date_published, v.id) version_id, v.mod_id, v.date_published FROM versions v - INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id - INNER JOIN game_versions gv on gvv.game_version_id = gv.id AND (cardinality($2::varchar[]) = 0 OR gv.version = ANY($2::varchar[])) - INNER JOIN loaders_versions lv ON lv.version_id = v.id - INNER JOIN loaders l on lv.loader_id = l.id AND (cardinality($3::varchar[]) = 0 OR l.loader = ANY($3::varchar[])) - WHERE v.mod_id = ANY($1) AND ($4::varchar IS NULL OR v.version_type = $4) - ORDER BY v.date_published, v.id ASC - LIMIT $5 OFFSET $6 - ", - &project_ids.into_iter().map(|x| x.0).collect::>(), - &game_versions.unwrap_or_default(), - &loaders.unwrap_or_default(), - version_type.map(|x| x.as_str()), - limit.map(|x| x as i64), - offset.map(|x| x as i64), - ) - .fetch_many(exec) - .try_filter_map(|e| async { Ok(e.right().map(|v| (ProjectId(v.mod_id), VersionId(v.version_id)))) }) - .try_collect::>() - .await?; - - let mut map: HashMap> = HashMap::new(); - - for (project_id, version_id) in vec { - if let Some(value) = map.get_mut(&project_id) { - value.push(version_id); - } else { - map.insert(project_id, vec![version_id]); - } - } - - Ok(map) - } - - pub async fn get_full<'a, 'b, E>( + pub async fn get<'a, 'b, E>( id: VersionId, executor: E, - ) -> Result, sqlx::error::Error> + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - Self::get_many_full(&[id], executor) + Self::get_many(&[id], executor, redis) .await .map(|x| x.into_iter().next()) } - pub async fn get_many_full<'a, E>( + pub async fn get_many<'a, E>( version_ids: &[VersionId], exec: E, - ) -> Result, sqlx::Error> + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + if version_ids.is_empty() { + return Ok(Vec::new()); + } + + use futures::stream::TryStreamExt; + + let mut version_ids_parsed: Vec = version_ids.iter().map(|x| x.0).collect(); + + let mut redis = redis.get().await?; + + let mut found_versions = Vec::new(); + + let versions = cmd("MGET") + .arg( + version_ids_parsed + .iter() + .map(|x| format!("{}:{}", VERSIONS_NAMESPACE, x)) + .collect::>(), + ) + .query_async::<_, Vec>>(&mut redis) + .await?; + + for version in versions { + if let Some(version) = + version.and_then(|x| serde_json::from_str::(&x).ok()) + { + version_ids_parsed.retain(|x| &version.inner.id.0 != x); + found_versions.push(version); + continue; + } + } + + if !version_ids_parsed.is_empty() { + let db_versions: Vec = sqlx::query!( + " + SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number, + v.changelog changelog, v.date_published date_published, v.downloads downloads, + v.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status, + JSONB_AGG(DISTINCT jsonb_build_object('version', gv.version, 'created', gv.created)) filter (where gv.version is not null) game_versions, + ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders, + JSONB_AGG(DISTINCT jsonb_build_object('id', f.id, 'url', f.url, 'filename', f.filename, 'primary', f.is_primary, 'size', f.size, 'file_type', f.file_type)) filter (where f.id is not null) files, + JSONB_AGG(DISTINCT jsonb_build_object('algorithm', h.algorithm, 'hash', encode(h.hash, 'escape'), 'file_id', h.file_id)) filter (where h.hash is not null) hashes, + JSONB_AGG(DISTINCT jsonb_build_object('project_id', d.mod_dependency_id, 'version_id', d.dependency_id, 'dependency_type', d.dependency_type,'file_name', dependency_file_name)) filter (where d.dependency_type is not null) dependencies + FROM versions v + LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id + LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id + LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id + LEFT OUTER JOIN loaders l on lv.loader_id = l.id + LEFT OUTER JOIN files f on v.id = f.version_id + LEFT OUTER JOIN hashes h on f.id = h.file_id + LEFT OUTER JOIN dependencies d on v.id = d.dependent_id + WHERE v.id = ANY($1) + GROUP BY v.id + ORDER BY v.date_published ASC; + ", + &version_ids_parsed + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|v| + QueryVersion { + inner: Version { + id: VersionId(v.id), + project_id: ProjectId(v.mod_id), + author_id: UserId(v.author_id), + name: v.version_name, + version_number: v.version_number, + changelog: v.changelog, + changelog_url: None, + date_published: v.date_published, + downloads: v.downloads, + version_type: v.version_type, + featured: v.featured, + status: VersionStatus::from_str(&v.status), + requested_status: v.requested_status + .map(|x| VersionStatus::from_str(&x)), + }, + files: { + #[derive(Deserialize)] + struct Hash { + pub file_id: FileId, + pub algorithm: String, + pub hash: String, + } + + #[derive(Deserialize)] + struct File { + pub id: FileId, + pub url: String, + pub filename: String, + pub primary: bool, + pub size: u32, + pub file_type: Option, + } + + let hashes: Vec = serde_json::from_value( + v.hashes.unwrap_or_default(), + ) + .ok() + .unwrap_or_default(); + + let files: Vec = serde_json::from_value( + v.files.unwrap_or_default(), + ) + .ok() + .unwrap_or_default(); + + let mut files = files.into_iter().map(|x| { + let mut file_hashes = HashMap::new(); + + for hash in &hashes { + if hash.file_id == x.id { + file_hashes.insert( + hash.algorithm.clone(), + hash.hash.clone(), + ); + } + } + + QueryFile { + id: x.id, + url: x.url, + filename: x.filename, + hashes: file_hashes, + primary: x.primary, + size: x.size, + file_type: x.file_type, + } + }).collect::>(); + + files.sort_by(|a, b| { + if a.primary { + Ordering::Less + } else if b.primary { + Ordering::Greater + } else { + a.filename.cmp(&b.filename) + } + }); + + files + }, + game_versions: { + #[derive(Deserialize)] + struct GameVersion { + pub version: String, + pub created: DateTime, + } + + let mut game_versions: Vec = serde_json::from_value( + v.game_versions.unwrap_or_default(), + ) + .ok() + .unwrap_or_default(); + + game_versions.sort_by(|a, b| a.created.cmp(&b.created)); + + game_versions.into_iter().map(|x| x.version).collect() + }, + loaders: v.loaders.unwrap_or_default(), + dependencies: serde_json::from_value( + v.dependencies.unwrap_or_default(), + ) + .ok() + .unwrap_or_default(), + } + )) + }) + .try_collect::>() + .await?; + + for version in db_versions { + cmd("SET") + .arg(format!("{}:{}", VERSIONS_NAMESPACE, version.inner.id.0)) + .arg(serde_json::to_string(&version)?) + .arg("EX") + .arg(DEFAULT_EXPIRY) + .query_async::<_, ()>(&mut redis) + .await?; + + found_versions.push(version); + } + } + + Ok(found_versions) + } + + pub async fn get_file_from_hash<'a, 'b, E>( + algo: String, + hash: String, + version_id: Option, + executor: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { - use futures::stream::TryStreamExt; - - let version_ids_parsed: Vec = version_ids.iter().map(|x| x.0).collect(); - sqlx::query!( - " - SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number, - v.changelog changelog, v.date_published date_published, v.downloads downloads, - v.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status, - JSONB_AGG(DISTINCT jsonb_build_object('version', gv.version, 'created', gv.created)) filter (where gv.version is not null) game_versions, - ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders, - JSONB_AGG(DISTINCT jsonb_build_object('id', f.id, 'url', f.url, 'filename', f.filename, 'primary', f.is_primary, 'size', f.size, 'file_type', f.file_type)) filter (where f.id is not null) files, - JSONB_AGG(DISTINCT jsonb_build_object('algorithm', h.algorithm, 'hash', encode(h.hash, 'escape'), 'file_id', h.file_id)) filter (where h.hash is not null) hashes, - JSONB_AGG(DISTINCT jsonb_build_object('project_id', d.mod_dependency_id, 'version_id', d.dependency_id, 'dependency_type', d.dependency_type,'file_name', dependency_file_name)) filter (where d.dependency_type is not null) dependencies - FROM versions v - LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id - LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id - LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id - LEFT OUTER JOIN loaders l on lv.loader_id = l.id - LEFT OUTER JOIN files f on v.id = f.version_id - LEFT OUTER JOIN hashes h on f.id = h.file_id - LEFT OUTER JOIN dependencies d on v.id = d.dependent_id - WHERE v.id = ANY($1) - GROUP BY v.id - ORDER BY v.date_published ASC; - ", - &version_ids_parsed - ) - .fetch_many(exec) - .try_filter_map(|e| async { - Ok(e.right().map(|v| - QueryVersion { - inner: Version { - id: VersionId(v.id), - project_id: ProjectId(v.mod_id), - author_id: UserId(v.author_id), - name: v.version_name, - version_number: v.version_number, - changelog: v.changelog, - changelog_url: None, - date_published: v.date_published, - downloads: v.downloads, - version_type: v.version_type, - featured: v.featured, - status: VersionStatus::from_str(&v.status), - requested_status: v.requested_status - .map(|x| VersionStatus::from_str(&x)), - }, - files: { - #[derive(Deserialize)] - struct Hash { - pub file_id: FileId, - pub algorithm: String, - pub hash: String, - } - - #[derive(Deserialize)] - struct File { - pub id: FileId, - pub url: String, - pub filename: String, - pub primary: bool, - pub size: u32, - pub file_type: Option, - } - - let hashes: Vec = serde_json::from_value( - v.hashes.unwrap_or_default(), - ) - .ok() - .unwrap_or_default(); - - let files: Vec = serde_json::from_value( - v.files.unwrap_or_default(), - ) - .ok() - .unwrap_or_default(); - - let mut files = files.into_iter().map(|x| { - let mut file_hashes = HashMap::new(); - - for hash in &hashes { - if hash.file_id == x.id { - file_hashes.insert( - hash.algorithm.clone(), - hash.hash.clone(), - ); - } - } - - QueryFile { - id: x.id, - url: x.url, - filename: x.filename, - hashes: file_hashes, - primary: x.primary, - size: x.size, - file_type: x.file_type, - } - }).collect::>(); - - files.sort_by(|a, b| { - if a.primary { - Ordering::Less - } else if b.primary { - Ordering::Greater - } else { - a.filename.cmp(&b.filename) - } - }); - - files - }, - game_versions: { - #[derive(Deserialize)] - struct GameVersion { - pub version: String, - pub created: DateTime, - } - - let mut game_versions: Vec = serde_json::from_value( - v.game_versions.unwrap_or_default(), - ) - .ok() - .unwrap_or_default(); - - game_versions.sort_by(|a, b| a.created.cmp(&b.created)); - - game_versions.into_iter().map(|x| x.version).collect() - }, - loaders: v.loaders.unwrap_or_default(), - dependencies: serde_json::from_value( - v.dependencies.unwrap_or_default(), - ) - .ok() - .unwrap_or_default(), - } - )) - }) - .try_collect::>() + Self::get_files_from_hash(algo, &[hash], executor, redis) .await + .map(|x| { + x.into_iter() + .find_or_first(|x| Some(x.version_id) == version_id) + }) } + pub async fn get_files_from_hash<'a, 'b, E>( + algorithm: String, + hashes: &[String], + executor: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + if hashes.is_empty() { + return Ok(Vec::new()); + } + + use futures::stream::TryStreamExt; + + let mut file_ids_parsed = hashes.to_vec(); + + let mut redis = redis.get().await?; + + let mut found_files = Vec::new(); + + let files = cmd("MGET") + .arg( + file_ids_parsed + .iter() + .map(|hash| format!("{}:{}_{}", VERSION_FILES_NAMESPACE, algorithm, hash)) + .collect::>(), + ) + .query_async::<_, Vec>>(&mut redis) + .await?; + + for file in files { + if let Some(mut file) = + file.and_then(|x| serde_json::from_str::>(&x).ok()) + { + file_ids_parsed.retain(|x| { + !file + .iter() + .any(|y| y.hashes.iter().any(|z| z.0 == &algorithm && z.1 == x)) + }); + found_files.append(&mut file); + continue; + } + } + + if !file_ids_parsed.is_empty() { + let db_files: Vec = sqlx::query!( + " + SELECT f.id, f.version_id, v.mod_id, f.url, f.filename, f.is_primary, f.size, f.file_type, + JSONB_AGG(DISTINCT jsonb_build_object('algorithm', h.algorithm, 'hash', encode(h.hash, 'escape'))) filter (where h.hash is not null) hashes + FROM files f + INNER JOIN versions v on v.id = f.version_id + INNER JOIN hashes h on h.file_id = f.id + WHERE h.algorithm = $1 AND h.hash = ANY($2) + GROUP BY f.id, v.mod_id, v.date_published + ORDER BY v.date_published + ", + algorithm, + &file_ids_parsed.into_iter().map(|x| x.as_bytes().to_vec()).collect::>(), + ) + .fetch_many(executor) + .try_filter_map(|e| async { + Ok(e.right().map(|f| { + #[derive(Deserialize)] + struct Hash { + pub algorithm: String, + pub hash: String, + } + + SingleFile { + id: FileId(f.id), + version_id: VersionId(f.version_id), + project_id: ProjectId(f.mod_id), + url: f.url, + filename: f.filename, + hashes: serde_json::from_value::>( + f.hashes.unwrap_or_default(), + ) + .ok() + .unwrap_or_default().into_iter().map(|x| (x.algorithm, x.hash)).collect(), + primary: f.is_primary, + size: f.size as u32, + file_type: f.file_type.map(|x| FileType::from_str(&x)), + } + } + )) + }) + .try_collect::>() + .await?; + + let mut save_files: HashMap> = HashMap::new(); + + for file in db_files { + for (algo, hash) in &file.hashes { + let key = format!("{}_{}", algo, hash); + + if let Some(files) = save_files.get_mut(&key) { + files.push(file.clone()); + } else { + save_files.insert(key, vec![file.clone()]); + } + } + } + + for (key, mut files) in save_files { + cmd("SET") + .arg(format!("{}:{}", VERSIONS_NAMESPACE, key)) + .arg(serde_json::to_string(&files)?) + .arg("EX") + .arg(DEFAULT_EXPIRY) + .query_async::<_, ()>(&mut redis) + .await?; + + found_files.append(&mut files); + } + } + + Ok(found_files) + } + + pub async fn clear_cache( + version: &QueryVersion, + redis: &deadpool_redis::Pool, + ) -> Result<(), DatabaseError> { + let mut redis = redis.get().await?; + + let mut cmd = cmd("DEL"); + + cmd.arg(format!("{}:{}", VERSIONS_NAMESPACE, version.inner.id.0)); + + for file in &version.files { + for (algo, hash) in &file.hashes { + cmd.arg(format!("{}:{}_{}", VERSION_FILES_NAMESPACE, algo, hash)); + } + } + + cmd.query_async::<_, ()>(&mut redis).await?; + + Ok(()) + } + + // TODO: Needs to be cached pub async fn get_full_from_id_slug<'a, 'b, E>( project_id_or_slug: &str, slug: &str, executor: E, - ) -> Result, sqlx::error::Error> + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { @@ -665,14 +778,14 @@ impl Version { .await?; if let Some(version_id) = id { - Version::get_full(VersionId(version_id.id), executor).await + Ok(Version::get(VersionId(version_id.id), executor, redis).await?) } else { Ok(None) } } } -#[derive(Clone)] +#[derive(Clone, Deserialize, Serialize)] pub struct QueryVersion { pub inner: Version, @@ -682,7 +795,7 @@ pub struct QueryVersion { pub dependencies: Vec, } -#[derive(Clone, Deserialize)] +#[derive(Clone, Deserialize, Serialize)] pub struct QueryDependency { pub project_id: Option, pub version_id: Option, @@ -690,7 +803,7 @@ pub struct QueryDependency { pub dependency_type: String, } -#[derive(Clone)] +#[derive(Clone, Deserialize, Serialize)] pub struct QueryFile { pub id: FileId, pub url: String, @@ -700,3 +813,16 @@ pub struct QueryFile { pub size: u32, pub file_type: Option, } + +#[derive(Clone, Deserialize, Serialize)] +pub struct SingleFile { + pub id: FileId, + pub version_id: VersionId, + pub project_id: ProjectId, + pub url: String, + pub filename: String, + pub hashes: HashMap, + pub primary: bool, + pub size: u32, + pub file_type: Option, +} diff --git a/src/health/mod.rs b/src/health/mod.rs deleted file mode 100644 index f28b79dc9..000000000 --- a/src/health/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod status; - -use lazy_static::lazy_static; -use std::sync::atomic::AtomicBool; - -lazy_static! { - pub static ref SEARCH_READY: AtomicBool = AtomicBool::new(false); -} diff --git a/src/health/status.rs b/src/health/status.rs deleted file mode 100644 index 043c7d971..000000000 --- a/src/health/status.rs +++ /dev/null @@ -1,14 +0,0 @@ -use actix_web::web; -use sqlx::PgPool; - -pub async fn test_database(postgres: web::Data) -> Result<(), sqlx::Error> { - let mut transaction = postgres.acquire().await?; - sqlx::query( - " - SELECT 1 - ", - ) - .execute(&mut transaction) - .await - .map(|_| ()) -} diff --git a/src/main.rs b/src/main.rs index 82a4c9ec8..9a52a00ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use crate::util::env::{parse_strings_from_var, parse_var}; use actix_cors::Cors; use actix_web::{web, App, HttpServer}; use chrono::{DateTime, Utc}; +use deadpool_redis::{Config, Runtime}; use env_logger::Env; use log::{error, info, warn}; use search::indexing::index_projects; @@ -15,9 +16,9 @@ use search::indexing::IndexingSettings; use std::sync::Arc; use tokio::sync::Mutex; +mod auth; mod database; mod file_hosting; -mod health; mod models; mod queue; mod ratelimit; @@ -74,6 +75,12 @@ async fn main() -> std::io::Result<()> { .await .expect("Database connection failed"); + // Redis connector + let redis_cfg = Config::from_url(dotenvy::var("REDIS_URL").expect("Redis URL not set")); + let redis_pool = redis_cfg + .create_pool(Some(Runtime::Tokio1)) + .expect("Redis connection failed"); + let storage_backend = dotenvy::var("STORAGE_BACKEND").unwrap_or_else(|_| "local".to_string()); let file_host: Arc = match storage_backend.as_str() { @@ -152,6 +159,7 @@ async fn main() -> std::io::Result<()> { // Changes statuses of scheduled projects/versions let pool_ref = pool.clone(); + // TODO: Clear cache when these are run scheduler.run(std::time::Duration::from_secs(60), move || { let pool_ref = pool_ref.clone(); info!("Releasing scheduled versions/projects!"); @@ -245,7 +253,7 @@ async fn main() -> std::io::Result<()> { } } - Ok::<(), crate::routes::ApiError>(()) + Ok::<(), routes::ApiError>(()) }; if let Err(e) = do_steps.await { @@ -342,6 +350,7 @@ async fn main() -> std::io::Result<()> { routes::ApiError::Validation(err.to_string()).into() }), ) + .app_data(web::Data::new(redis_pool.clone())) .app_data(web::Data::new(pool.clone())) .app_data(web::Data::new(file_host.clone())) .app_data(web::Data::new(search_config.clone())) @@ -352,6 +361,7 @@ async fn main() -> std::io::Result<()> { .configure(routes::root_config) .configure(routes::v2::config) .configure(routes::v3::config) + .configure(auth::config) .default_service(web::get().to(routes::not_found)) }) .bind(dotenvy::var("BIND_ADDR").unwrap())? @@ -387,9 +397,6 @@ fn check_env_vars() -> bool { failed |= check_var::("SITE_URL"); failed |= check_var::("CDN_URL"); - failed |= check_var::("MINOS_URL"); - failed |= check_var::("KRATOS_URL"); - failed |= check_var::("ORY_AUTH_BEARER"); failed |= check_var::("LABRINTH_ADMIN_KEY"); failed |= check_var::("RATE_LIMIT_IGNORE_KEY"); failed |= check_var::("DATABASE_URL"); @@ -398,6 +405,8 @@ fn check_env_vars() -> bool { failed |= check_var::("BIND_ADDR"); failed |= check_var::("SELF_ADDR"); + failed |= check_var::("REDIS_URL"); + failed |= check_var::("STORAGE_BACKEND"); let storage_backend = dotenvy::var("STORAGE_BACKEND").ok(); @@ -431,13 +440,11 @@ fn check_env_vars() -> bool { failed |= check_var::("VERSION_INDEX_INTERVAL"); failed |= check_var::("GITHUB_CLIENT_ID"); + failed |= check_var::("GITHUB_CLIENT_SECRET"); failed |= check_var::("ARIADNE_ADMIN_KEY"); failed |= check_var::("ARIADNE_URL"); - failed |= check_var::("STRIPE_TOKEN"); - failed |= check_var::("STRIPE_WEBHOOK_SECRET"); - failed |= check_var::("PAYPAL_API_URL"); failed |= check_var::("PAYPAL_CLIENT_ID"); failed |= check_var::("PAYPAL_CLIENT_SECRET"); diff --git a/src/models/ids.rs b/src/models/ids.rs index 119bdee3a..b5385bd59 100644 --- a/src/models/ids.rs +++ b/src/models/ids.rs @@ -3,6 +3,7 @@ use thiserror::Error; pub use super::notifications::NotificationId; pub use super::projects::{ProjectId, VersionId}; pub use super::reports::ReportId; +pub use super::sessions::SessionId; pub use super::teams::TeamId; pub use super::threads::ThreadId; pub use super::threads::ThreadMessageId; @@ -113,6 +114,7 @@ base62_id_impl!(ReportId, ReportId); base62_id_impl!(NotificationId, NotificationId); base62_id_impl!(ThreadId, ThreadId); base62_id_impl!(ThreadMessageId, ThreadMessageId); +base62_id_impl!(SessionId, SessionId); pub mod base62_impl { use serde::de::{self, Deserializer, Visitor}; diff --git a/src/models/mod.rs b/src/models/mod.rs index 54df8b61d..d3f30e60b 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -4,6 +4,7 @@ pub mod notifications; pub mod pack; pub mod projects; pub mod reports; +pub mod sessions; pub mod teams; pub mod threads; pub mod users; diff --git a/src/models/projects.rs b/src/models/projects.rs index 0095d8b80..4a95866aa 100644 --- a/src/models/projects.rs +++ b/src/models/projects.rs @@ -165,8 +165,8 @@ impl From for Project { followers: m.follows as u32, categories: data.categories, additional_categories: data.additional_categories, - game_versions: m.game_versions, - loaders: m.loaders, + game_versions: data.game_versions, + loaders: data.loaders, versions: data.versions.into_iter().map(|v| v.into()).collect(), icon_url: m.icon_url, issues_url: m.issues_url, @@ -449,7 +449,7 @@ impl MonetizationStatus { } /// A specific version of a project -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct Version { /// The ID of the version, encoded as a base62 string. pub id: VersionId, @@ -633,7 +633,7 @@ impl VersionStatus { } /// A single project file, with a url for the file and the file's hash -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct VersionFile { /// A map of hashes of the file. The key is the hashing algorithm /// and the value is the string version of the hash. @@ -749,6 +749,15 @@ impl FileType { FileType::Unknown => "unknown", } } + + pub fn from_str(string: &str) -> FileType { + match string { + "required-resource-pack" => FileType::RequiredResourcePack, + "optional-resource-pack" => FileType::OptionalResourcePack, + "unknown" => FileType::Unknown, + _ => FileType::Unknown, + } + } } /// A specific version of Minecraft diff --git a/src/models/reports.rs b/src/models/reports.rs index 5f39e9aa2..5bd8e254b 100644 --- a/src/models/reports.rs +++ b/src/models/reports.rs @@ -1,5 +1,6 @@ use super::ids::Base62Id; -use crate::models::ids::{ThreadId, UserId}; +use crate::database::models::report_item::QueryReport as DBReport; +use crate::models::ids::{ProjectId, ThreadId, UserId, VersionId}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -40,3 +41,33 @@ impl ItemType { } } } + +impl From for Report { + fn from(x: DBReport) -> Self { + let mut item_id = "".to_string(); + let mut item_type = ItemType::Unknown; + + if let Some(project_id) = x.project_id { + item_id = ProjectId::from(project_id).to_string(); + item_type = ItemType::Project; + } else if let Some(version_id) = x.version_id { + item_id = VersionId::from(version_id).to_string(); + item_type = ItemType::Version; + } else if let Some(user_id) = x.user_id { + item_id = UserId::from(user_id).to_string(); + item_type = ItemType::User; + } + + Report { + id: x.id.into(), + report_type: x.report_type, + item_id, + item_type, + reporter: x.reporter.into(), + body: x.body, + created: x.created, + closed: x.closed, + thread_id: x.thread_id.map(|x| x.into()), + } + } +} diff --git a/src/models/sessions.rs b/src/models/sessions.rs new file mode 100644 index 000000000..702a7acdf --- /dev/null +++ b/src/models/sessions.rs @@ -0,0 +1,29 @@ +use super::ids::Base62Id; +use crate::models::users::UserId; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "Base62Id")] +#[serde(into = "Base62Id")] +pub struct SessionId(pub u64); + +#[derive(Serialize, Deserialize, Clone)] +pub struct Session { + pub id: SessionId, + pub session: String, + pub user_id: UserId, + + pub created: DateTime, + pub last_login: DateTime, + pub expires: DateTime, + pub refresh_expires: DateTime, + + pub os: Option, + pub platform: Option, + pub user_agent: String, + + pub city: Option, + pub country: Option, + pub ip: String, +} diff --git a/src/models/threads.rs b/src/models/threads.rs index 22ba9a33e..0f6ad9801 100644 --- a/src/models/threads.rs +++ b/src/models/threads.rs @@ -82,3 +82,44 @@ impl ThreadType { } } } + +impl Thread { + pub fn from(data: crate::database::models::Thread, users: Vec, user: &User) -> Self { + let thread_type = data.type_; + + Thread { + id: data.id.into(), + type_: thread_type, + messages: data + .messages + .into_iter() + .filter(|x| { + if let MessageBody::Text { private, .. } = x.body { + !private || user.role.is_mod() + } else { + true + } + }) + .map(|x| ThreadMessage { + id: x.id.into(), + author_id: if users + .iter() + .find(|y| x.author_id == Some(y.id.into())) + .map(|x| x.role.is_mod() && !user.role.is_mod()) + .unwrap_or(false) + { + None + } else { + x.author_id.map(|x| x.into()) + }, + body: x.body, + created: x.created, + }) + .collect(), + members: users + .into_iter() + .filter(|x| !x.role.is_mod() || user.role.is_mod()) + .collect(), + } + } +} diff --git a/src/models/users.rs b/src/models/users.rs index e95ac0d28..b702b22b3 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -37,7 +37,6 @@ impl Default for Badges { #[derive(Serialize, Deserialize, Clone)] pub struct User { pub id: UserId, - pub kratos_id: Option, // None if legacy user unconnected to Minos/Kratos pub username: String, pub name: Option, pub email: Option, @@ -48,11 +47,11 @@ pub struct User { pub badges: Badges, pub payout_data: Option, pub github_id: Option, - pub discord_id: Option, - pub google_id: Option, - pub microsoft_id: Option, - pub apple_id: Option, - pub gitlab_id: Option, + // pub discord_id: Option, + // pub google_id: Option, + // pub microsoft_id: Option, + // pub apple_id: Option, + // pub gitlab_id: Option, } #[derive(Serialize, Deserialize, Clone)] @@ -136,7 +135,6 @@ impl From for User { fn from(data: DBUser) -> Self { Self { id: data.id.into(), - kratos_id: data.kratos_id, username: data.username, name: data.name, email: None, @@ -147,11 +145,11 @@ impl From for User { badges: data.badges, payout_data: None, github_id: None, - discord_id: None, - google_id: None, - microsoft_id: None, - apple_id: None, - gitlab_id: None, + // discord_id: None, + // google_id: None, + // microsoft_id: None, + // apple_id: None, + // gitlab_id: None, } } } diff --git a/src/routes/health.rs b/src/routes/health.rs deleted file mode 100644 index d4042c5a8..000000000 --- a/src/routes/health.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::health::status::test_database; -use crate::health::SEARCH_READY; -use actix_web::web::Data; -use actix_web::{get, HttpResponse}; -use serde_json::json; -use sqlx::PgPool; -use std::sync::atomic::Ordering; - -#[get("/health")] -pub async fn health_get(client: Data) -> HttpResponse { - // Check database connection: - let result = test_database(client).await; - if result.is_err() { - let data = json!({ - "ready": false, - "reason": "Database connection error" - }); - return HttpResponse::InternalServerError().json(data); - } - if !SEARCH_READY.load(Ordering::Acquire) { - let data = json!({ - "ready": false, - "reason": "Indexing is not finished" - }); - return HttpResponse::InternalServerError().json(data); - } - HttpResponse::Ok().json(json!({ - "ready": true, - "reason": "Everything is OK" - })) -} diff --git a/src/routes/maven.rs b/src/routes/maven.rs index d25d9dd06..708a7aa87 100644 --- a/src/routes/maven.rs +++ b/src/routes/maven.rs @@ -1,9 +1,9 @@ +use crate::auth::{get_user_from_headers, is_authorized_version}; use crate::database::models::project_item::QueryProject; use crate::database::models::version_item::{QueryFile, QueryVersion}; use crate::models::projects::{ProjectId, VersionId}; use crate::routes::ApiError; -use crate::util::auth::{get_user_from_headers, is_authorized_version}; -use crate::{database, util::auth::is_authorized}; +use crate::{auth::is_authorized, database}; use actix_web::{get, route, web, HttpRequest, HttpResponse}; use sqlx::PgPool; use std::collections::HashSet; @@ -66,10 +66,10 @@ pub async fn maven_metadata( req: HttpRequest, params: web::Path<(String,)>, pool: web::Data, + redis: web::Data, ) -> Result { let project_id = params.into_inner().0; - let project_data = - database::models::Project::get_from_slug_or_project_id(&project_id, &**pool).await?; + let project_data = database::models::Project::get(&project_id, &**pool, &redis).await?; let data = if let Some(data) = project_data { data @@ -77,9 +77,11 @@ pub async fn maven_metadata( return Ok(HttpResponse::NotFound().body("")); }; - let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); - if !is_authorized(&data, &user_option, &pool).await? { + if !is_authorized(&data.inner, &user_option, &pool).await? { return Ok(HttpResponse::NotFound().body("")); } @@ -90,7 +92,7 @@ pub async fn maven_metadata( WHERE mod_id = $1 AND status = ANY($2) ORDER BY date_published ASC ", - data.id as database::models::ids::ProjectId, + data.inner.id as database::models::ids::ProjectId, &*crate::models::projects::VersionStatus::iterator() .filter(|x| x.is_listed()) .map(|x| x.to_string()) @@ -118,7 +120,7 @@ pub async fn maven_metadata( new_versions.push(value); } - let project_id: ProjectId = data.id.into(); + let project_id: ProjectId = data.inner.id.into(); let respdata = Metadata { group_id: "maven.modrinth".to_string(), @@ -132,7 +134,7 @@ pub async fn maven_metadata( versions: Versions { versions: new_versions, }, - last_updated: data.updated.format("%Y%m%d%H%M%S").to_string(), + last_updated: data.inner.updated.format("%Y%m%d%H%M%S").to_string(), }, }; @@ -185,10 +187,10 @@ pub async fn version_file( req: HttpRequest, params: web::Path<(String, String, String)>, pool: web::Data, + redis: web::Data, ) -> Result { let (project_id, vnum, file) = params.into_inner(); - let project_data = - database::models::Project::get_full_from_slug_or_project_id(&project_id, &**pool).await?; + let project_data = database::models::Project::get(&project_id, &**pool, &redis).await?; let project = if let Some(data) = project_data { data @@ -196,7 +198,9 @@ pub async fn version_file( return Ok(HttpResponse::NotFound().body("")); }; - let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); if !is_authorized(&project.inner, &user_option, &pool).await? { return Ok(HttpResponse::NotFound().body("")); @@ -221,7 +225,7 @@ pub async fn version_file( }; let version = if let Some(version) = - database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool) + database::models::Version::get(database::models::ids::VersionId(vid.id), &**pool, &redis) .await? { version @@ -266,10 +270,10 @@ pub async fn version_file_sha1( req: HttpRequest, params: web::Path<(String, String, String)>, pool: web::Data, + redis: web::Data, ) -> Result { let (project_id, vnum, file) = params.into_inner(); - let project_data = - database::models::Project::get_full_from_slug_or_project_id(&project_id, &**pool).await?; + let project_data = database::models::Project::get(&project_id, &**pool, &redis).await?; let project = if let Some(data) = project_data { data @@ -277,7 +281,9 @@ pub async fn version_file_sha1( return Ok(HttpResponse::NotFound().body("")); }; - let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); if !is_authorized(&project.inner, &user_option, &pool).await? { return Ok(HttpResponse::NotFound().body("")); @@ -302,7 +308,7 @@ pub async fn version_file_sha1( }; let version = if let Some(version) = - database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool) + database::models::Version::get(database::models::ids::VersionId(vid.id), &**pool, &redis) .await? { version @@ -321,10 +327,10 @@ pub async fn version_file_sha512( req: HttpRequest, params: web::Path<(String, String, String)>, pool: web::Data, + redis: web::Data, ) -> Result { let (project_id, vnum, file) = params.into_inner(); - let project_data = - database::models::Project::get_full_from_slug_or_project_id(&project_id, &**pool).await?; + let project_data = database::models::Project::get(&project_id, &**pool, &redis).await?; let project = if let Some(data) = project_data { data @@ -332,7 +338,9 @@ pub async fn version_file_sha512( return Ok(HttpResponse::NotFound().body("")); }; - let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); if !is_authorized(&project.inner, &user_option, &pool).await? { return Ok(HttpResponse::NotFound().body("")); @@ -357,7 +365,7 @@ pub async fn version_file_sha512( }; let version = if let Some(version) = - database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool) + database::models::Version::get(database::models::ids::VersionId(vid.id), &**pool, &redis) .await? { version diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 96cf2dcbb..875c4b094 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -6,7 +6,6 @@ use futures::FutureExt; pub mod v2; pub mod v3; -mod health; mod index; mod maven; mod not_found; @@ -16,7 +15,6 @@ pub use self::not_found::not_found; pub fn root_config(cfg: &mut web::ServiceConfig) { cfg.service(index::index_get); - cfg.service(health::health_get); cfg.service(web::scope("maven").configure(maven::config)); cfg.service(web::scope("updates").configure(updates::config)); cfg.service( @@ -47,7 +45,7 @@ pub enum ApiError { #[error("Deserialization error: {0}")] Json(#[from] serde_json::Error), #[error("Authentication Error: {0}")] - Authentication(#[from] crate::util::auth::AuthenticationError), + Authentication(#[from] crate::auth::AuthenticationError), #[error("Authentication Error: {0}")] CustomAuthentication(String), #[error("Invalid Input: {0}")] @@ -60,8 +58,6 @@ pub enum ApiError { Indexing(#[from] crate::search::indexing::IndexingError), #[error("Ariadne Error: {0}")] Analytics(String), - #[error("Crypto Error: {0}")] - Crypto(String), #[error("Payments Error: {0}")] Payments(String), #[error("Discord Error: {0}")] @@ -88,7 +84,6 @@ impl actix_web::ResponseError for ApiError { ApiError::InvalidInput(..) => StatusCode::BAD_REQUEST, ApiError::Validation(..) => StatusCode::BAD_REQUEST, ApiError::Analytics(..) => StatusCode::FAILED_DEPENDENCY, - ApiError::Crypto(..) => StatusCode::FORBIDDEN, ApiError::Payments(..) => StatusCode::FAILED_DEPENDENCY, ApiError::DiscordError(..) => StatusCode::FAILED_DEPENDENCY, ApiError::Decoding(..) => StatusCode::BAD_REQUEST, @@ -112,7 +107,6 @@ impl actix_web::ResponseError for ApiError { ApiError::InvalidInput(..) => "invalid_input", ApiError::Validation(..) => "invalid_input", ApiError::Analytics(..) => "analytics_error", - ApiError::Crypto(..) => "crypto_error", ApiError::Payments(..) => "payments_error", ApiError::DiscordError(..) => "discord_error", ApiError::Decoding(..) => "decoding_error", diff --git a/src/routes/updates.rs b/src/routes/updates.rs index ac3d119f4..752dd6e6e 100644 --- a/src/routes/updates.rs +++ b/src/routes/updates.rs @@ -4,9 +4,9 @@ use actix_web::{get, web, HttpRequest, HttpResponse}; use serde::Serialize; use sqlx::PgPool; +use crate::auth::{filter_authorized_versions, get_user_from_headers, is_authorized}; use crate::database; use crate::models::projects::VersionType; -use crate::util::auth::{filter_authorized_versions, get_user_from_headers, is_authorized}; use super::ApiError; @@ -19,36 +19,36 @@ pub async fn forge_updates( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, ) -> Result { const ERROR: &str = "The specified project does not exist!"; let (id,) = info.into_inner(); - let project = database::models::Project::get_from_slug_or_project_id(&id, &**pool) + let project = database::models::Project::get(&id, &**pool, &redis) .await? .ok_or_else(|| ApiError::InvalidInput(ERROR.to_string()))?; - let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); - if !is_authorized(&project, &user_option, &pool).await? { + if !is_authorized(&project.inner, &user_option, &pool).await? { return Err(ApiError::InvalidInput(ERROR.to_string())); } - let version_ids = database::models::Version::get_project_versions( - project.id, - None, - Some(vec!["forge".to_string()]), - None, - None, - None, - &**pool, + let versions = database::models::Version::get_many(&project.versions, &**pool, &redis).await?; + + let mut versions = filter_authorized_versions( + versions + .into_iter() + .filter(|x| x.loaders.iter().any(|y| *y == "forge")) + .collect(), + &user_option, + &pool, ) .await?; - let versions = database::models::Version::get_many_full(&version_ids, &**pool).await?; - - let mut versions = filter_authorized_versions(versions, &user_option, &pool).await?; - versions.sort_by(|a, b| b.date_published.cmp(&a.date_published)); #[derive(Serialize)] diff --git a/src/routes/v2/admin.rs b/src/routes/v2/admin.rs index 3949feea4..de9c08757 100644 --- a/src/routes/v2/admin.rs +++ b/src/routes/v2/admin.rs @@ -1,12 +1,10 @@ -use crate::database::models::user_item; +use crate::database::models::{User, UserId}; use crate::models::ids::ProjectId; use crate::models::projects::MonetizationStatus; -use crate::models::users::User; use crate::routes::ApiError; -use crate::util::auth::{link_or_insert_new_user, MinosNewUser}; use crate::util::guards::admin_key_guard; use crate::DownloadQueue; -use actix_web::{get, patch, post, web, HttpResponse}; +use actix_web::{patch, post, web, HttpResponse}; use chrono::{DateTime, SecondsFormat, Utc}; use rust_decimal::Decimal; use serde::Deserialize; @@ -19,110 +17,10 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("admin") .service(count_download) - .service(add_minos_user) - .service(edit_github_id) - .service(edit_email) - .service(get_legacy_account) .service(process_payout), ); } -// Adds a Minos user to the database -// This is an internal endpoint, and should not be used by applications, only by the Minos backend -#[post("_minos-user-callback", guard = "admin_key_guard")] -pub async fn add_minos_user( - minos_user: web::Json, // getting directly from Kratos rather than Minos, so unparse - client: web::Data, -) -> Result { - let minos_new_user = minos_user.into_inner(); - let mut transaction = client.begin().await?; - link_or_insert_new_user(&mut transaction, minos_new_user).await?; - transaction.commit().await?; - Ok(HttpResponse::Ok().finish()) -} - -// Add or update a user's GitHub ID by their kratos id -// OIDC ids should be kept in Minos, but Github is duplicated in Labrinth for legacy support -// This should not be directly useable by applications, only by the Minos backend -// user id is passed in path, github id is passed in body -#[derive(Deserialize)] -pub struct EditGithubId { - github_id: Option, -} -#[post("_edit_github_id/{kratos_id}", guard = "admin_key_guard")] -pub async fn edit_github_id( - pool: web::Data, - kratos_id: web::Path, - github_id: web::Json, -) -> Result { - let github_id = github_id.into_inner().github_id; - // Parse error if github inner id not a number - let github_id = github_id - .as_ref() - .map(|x| x.parse::()) - .transpose() - .map_err(|_| ApiError::InvalidInput("Github id must be a number".to_string()))?; - - let mut transaction = pool.begin().await?; - sqlx::query!( - " - UPDATE users - SET github_id = $1 - WHERE kratos_id = $2 - ", - github_id, - kratos_id.into_inner() - ) - .execute(&mut transaction) - .await?; - transaction.commit().await?; - Ok(HttpResponse::Ok().finish()) -} - -// Update a user's email ID by their kratos id -// email ids should be kept in Minos, but email is duplicated in Labrinth for legacy support (and to avoid Minos calls) -// This should not be directly useable by applications, only by the Minos backend -// user id is passed in path, email is passed in body -#[derive(Deserialize)] -pub struct EditEmail { - email: String, -} -#[post("_edit_email/{kratos_id}", guard = "admin_key_guard")] -pub async fn edit_email( - pool: web::Data, - kratos_id: web::Path, - email: web::Json, -) -> Result { - let email = email.into_inner().email; - - let mut transaction = pool.begin().await?; - sqlx::query!( - " - UPDATE users - SET email = $1 - WHERE kratos_id = $2 - ", - email, - kratos_id.into_inner() - ) - .execute(&mut transaction) - .await?; - transaction.commit().await?; - Ok(HttpResponse::Ok().finish()) -} - -#[get("_legacy_account/{github_id}", guard = "admin_key_guard")] - -pub async fn get_legacy_account( - pool: web::Data, - github_id: web::Path, -) -> Result { - let github_id = github_id.into_inner(); - let user = user_item::User::get_from_github_id(github_id as u64, &**pool).await?; - let user: Option = user.map(|u| u.into()); - Ok(HttpResponse::Ok().json(user)) -} - #[derive(Deserialize)] pub struct DownloadBody { pub url: String, @@ -214,6 +112,7 @@ pub struct PayoutData { #[post("/_process_payout", guard = "admin_key_guard")] pub async fn process_payout( pool: web::Data, + redis: web::Data, data: web::Json, ) -> Result { let start: DateTime = DateTime::from_utc( @@ -409,6 +308,8 @@ pub async fn process_payout( let sum_splits: Decimal = project.team_members.iter().map(|x| x.1).sum(); let sum_tm_splits: Decimal = project.split_team_members.iter().map(|x| x.1).sum(); + let mut clear_cache_users = Vec::new(); + if sum_splits > Decimal::ZERO { for (user_id, split) in project.team_members { let payout: Decimal = data.amount @@ -445,6 +346,7 @@ pub async fn process_payout( ) .execute(&mut *transaction) .await?; + clear_cache_users.push(user_id); } } } @@ -481,9 +383,19 @@ pub async fn process_payout( ) .execute(&mut *transaction) .await?; + clear_cache_users.push(user_id); } } } + + User::clear_caches( + &clear_cache_users + .into_iter() + .map(|x| (UserId(x), None)) + .collect::>(), + &redis, + ) + .await?; } } diff --git a/src/routes/v2/auth.rs b/src/routes/v2/auth.rs deleted file mode 100644 index 2833b0e5b..000000000 --- a/src/routes/v2/auth.rs +++ /dev/null @@ -1,214 +0,0 @@ -/*! -This auth module is how we allow for authentication within the Modrinth sphere. -It uses a self-hosted Ory Kratos instance on the backend, powered by our Minos backend. - - Applications interacting with the authenticated API (a very small portion - notifications, private projects, editing/creating projects -and versions) should include the Ory authentication cookie in their requests. This cookie is set by the Ory Kratos instance and Minos provides function to access these. - -In addition, you can use a logged-in-account to generate a PAT. -This token can be passed in as a Bearer token in the Authorization header, as an alternative to a cookie. -This is useful for applications that don't have a frontend, or for applications that need to access the authenticated API on behalf of a user. - -Just as a summary: Don't implement this flow in your application! -*/ - -use crate::database::models::{self, generate_state_id}; -use crate::models::error::ApiError; -use crate::models::ids::base62_impl::{parse_base62, to_base62}; -use crate::models::ids::DecodingError; - -use crate::parse_strings_from_var; -use crate::util::auth::{get_minos_user_from_cookies, AuthenticationError}; - -use actix_web::http::StatusCode; -use actix_web::web::{scope, Data, Query, ServiceConfig}; -use actix_web::{get, HttpRequest, HttpResponse}; -use chrono::Utc; - -use serde::{Deserialize, Serialize}; -use sqlx::postgres::PgPool; -use thiserror::Error; - -pub fn config(cfg: &mut ServiceConfig) { - cfg.service(scope("auth").service(auth_callback).service(init)); -} - -#[derive(Error, Debug)] -pub enum AuthorizationError { - #[error("Environment Error")] - Env(#[from] dotenvy::Error), - #[error("An unknown database error occured: {0}")] - SqlxDatabase(#[from] sqlx::Error), - #[error("Database Error: {0}")] - Database(#[from] crate::database::models::DatabaseError), - #[error("Error while parsing JSON: {0}")] - SerDe(#[from] serde_json::Error), - #[error("Error with communicating to Minos")] - Minos(#[from] reqwest::Error), - #[error("Invalid Authentication credentials")] - InvalidCredentials, - #[error("Authentication Error: {0}")] - Authentication(#[from] crate::util::auth::AuthenticationError), - #[error("Error while decoding Base62")] - Decoding(#[from] DecodingError), - #[error("Invalid callback URL specified")] - Url, - #[error("User exists in Minos but not in Labrinth")] - DatabaseMismatch, -} -impl actix_web::ResponseError for AuthorizationError { - fn status_code(&self) -> StatusCode { - match self { - AuthorizationError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR, - AuthorizationError::SqlxDatabase(..) => StatusCode::INTERNAL_SERVER_ERROR, - AuthorizationError::Database(..) => StatusCode::INTERNAL_SERVER_ERROR, - AuthorizationError::SerDe(..) => StatusCode::BAD_REQUEST, - AuthorizationError::Minos(..) => StatusCode::INTERNAL_SERVER_ERROR, - AuthorizationError::InvalidCredentials => StatusCode::UNAUTHORIZED, - AuthorizationError::Decoding(..) => StatusCode::BAD_REQUEST, - AuthorizationError::Authentication(..) => StatusCode::UNAUTHORIZED, - AuthorizationError::Url => StatusCode::BAD_REQUEST, - AuthorizationError::DatabaseMismatch => StatusCode::INTERNAL_SERVER_ERROR, - } - } - - fn error_response(&self) -> HttpResponse { - HttpResponse::build(self.status_code()).json(ApiError { - error: match self { - AuthorizationError::Env(..) => "environment_error", - AuthorizationError::SqlxDatabase(..) => "database_error", - AuthorizationError::Database(..) => "database_error", - AuthorizationError::SerDe(..) => "invalid_input", - AuthorizationError::Minos(..) => "network_error", - AuthorizationError::InvalidCredentials => "invalid_credentials", - AuthorizationError::Decoding(..) => "decoding_error", - AuthorizationError::Authentication(..) => "authentication_error", - AuthorizationError::Url => "url_error", - AuthorizationError::DatabaseMismatch => "database_mismatch", - }, - description: &self.to_string(), - }) - } -} - -#[derive(Serialize, Deserialize)] -pub struct AuthorizationInit { - pub url: String, -} -#[derive(Serialize, Deserialize)] -pub struct StateResponse { - pub state: String, -} - -// Init link takes us to Minos API and calls back to callback endpoint with a code and state -//http://:8000/api/v1/auth/init?url=https%3A%2F%2Fmodrinth.com%2Fmods -#[get("init")] -pub async fn init( - Query(info): Query, // callback url - client: Data, -) -> Result { - let url = url::Url::parse(&info.url).map_err(|_| AuthorizationError::Url)?; - - let allowed_callback_urls = parse_strings_from_var("ALLOWED_CALLBACK_URLS").unwrap_or_default(); - let domain = url.host_str().ok_or(AuthorizationError::Url)?; // TODO: change back to .domain() (host_str is so we can use 127.0.0.1) - if !allowed_callback_urls.iter().any(|x| domain.ends_with(x)) && domain != "modrinth.com" { - return Err(AuthorizationError::Url); - } - - let mut transaction = client.begin().await?; - - let state = generate_state_id(&mut transaction).await?; - - sqlx::query!( - " - INSERT INTO states (id, url) - VALUES ($1, $2) - ", - state.0, - info.url - ) - .execute(&mut *transaction) - .await?; - - transaction.commit().await?; - - let kratos_url = dotenvy::var("KRATOS_URL")?; - let labrinth_url = dotenvy::var("SELF_ADDR")?; - let url = format!( - // Callback URL of initialization is /callback below. - "{kratos_url}/self-service/login/browser?return_to={labrinth_url}/v2/auth/callback?state={}", - to_base62(state.0 as u64) - ); - Ok(HttpResponse::TemporaryRedirect() - .append_header(("Location", &*url)) - .json(AuthorizationInit { url })) -} - -#[get("callback")] -pub async fn auth_callback( - req: HttpRequest, - Query(state): Query, - client: Data, -) -> Result { - let mut transaction = client.begin().await?; - let state_id: u64 = parse_base62(&state.state)?; - - let result_option = sqlx::query!( - " - SELECT url, expires FROM states - WHERE id = $1 - ", - state_id as i64 - ) - .fetch_optional(&mut *transaction) - .await?; - - // Extract cookie header from request - let cookie_header = req.headers().get("Cookie"); - if let Some(result) = result_option { - if let Some(cookie_header) = cookie_header { - // Extract cookie header to get authenticated user from Minos - let duration: chrono::Duration = result.expires - Utc::now(); - if duration.num_seconds() < 0 { - return Err(AuthorizationError::InvalidCredentials); - } - sqlx::query!( - " - DELETE FROM states - WHERE id = $1 - ", - state_id as i64 - ) - .execute(&mut *transaction) - .await?; - - // Attempt to create a minos user from the cookie header- if this fails, the user is invalid - let minos_user = get_minos_user_from_cookies( - cookie_header - .to_str() - .map_err(|_| AuthenticationError::InvalidCredentials)?, - ) - .await?; - let user_result = - models::User::get_from_minos_kratos_id(minos_user.id.clone(), &mut transaction) - .await?; - - // Cookies exist, but user does not exist in database, meaning they are invalid - if user_result.is_none() { - return Err(AuthorizationError::DatabaseMismatch); - } - transaction.commit().await?; - - // Cookie is attached now, so redirect to the original URL - // Do not re-append cookie header, as it is not needed, - // because all redirects are to various modrinth.com subdomains - Ok(HttpResponse::TemporaryRedirect() - .append_header(("Location", &*result.url)) - .json(AuthorizationInit { url: result.url })) - } else { - Err(AuthorizationError::InvalidCredentials) - } - } else { - Err(AuthorizationError::InvalidCredentials) - } -} diff --git a/src/routes/v2/midas.rs b/src/routes/v2/midas.rs deleted file mode 100644 index 0ddd474b8..000000000 --- a/src/routes/v2/midas.rs +++ /dev/null @@ -1,325 +0,0 @@ -use crate::models::users::UserId; -use crate::routes::ApiError; -use crate::util::auth::get_user_from_headers; -use actix_web::{post, web, HttpRequest, HttpResponse}; -use chrono::{DateTime, Duration, NaiveDateTime, Utc}; -use hmac::{Hmac, Mac, NewMac}; -use itertools::Itertools; -use serde::Deserialize; -use serde_json::{json, Value}; -use sqlx::PgPool; - -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("midas") - .service(init_checkout) - .service(init_customer_portal) - .service(handle_stripe_webhook), - ); -} - -#[derive(Deserialize)] -pub struct CheckoutData { - pub price_id: String, -} - -#[post("/_stripe-init-checkout")] -pub async fn init_checkout( - req: HttpRequest, - pool: web::Data, - data: web::Json, -) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; - - let client = reqwest::Client::new(); - - #[derive(Deserialize)] - struct Session { - url: Option, - } - - let session = client - .post("https://api.stripe.com/v1/checkout/sessions") - .header( - "Authorization", - format!("Bearer {}", dotenvy::var("STRIPE_TOKEN")?), - ) - .form(&[ - ("mode", "subscription"), - ("line_items[0][price]", &*data.price_id), - ("line_items[0][quantity]", "1"), - ("success_url", "https://modrinth.com/welcome-to-midas"), - ("cancel_url", "https://modrinth.com/midas"), - ("metadata[user_id]", &user.id.to_string()), - ]) - .send() - .await - .map_err(|_| ApiError::Payments("Error while creating checkout session!".to_string()))? - .json::() - .await - .map_err(|_| { - ApiError::Payments("Error while deserializing checkout response!".to_string()) - })?; - - Ok(HttpResponse::Ok().json(json!( - { - "url": session.url - } - ))) -} - -#[post("/_stripe-init-portal")] -pub async fn init_customer_portal( - req: HttpRequest, - pool: web::Data, -) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; - - let customer_id = sqlx::query!( - " - SELECT u.stripe_customer_id - FROM users u - WHERE u.id = $1 - ", - user.id.0 as i64, - ) - .fetch_optional(&**pool) - .await? - .and_then(|x| x.stripe_customer_id) - .ok_or_else(|| ApiError::InvalidInput("User is not linked to stripe account!".to_string()))?; - - let client = reqwest::Client::new(); - - #[derive(Deserialize)] - struct Session { - url: Option, - } - - let session = client - .post("https://api.stripe.com/v1/billing_portal/sessions") - .header( - "Authorization", - format!("Bearer {}", dotenvy::var("STRIPE_TOKEN")?), - ) - .form(&[ - ("customer", &*customer_id), - ("return_url", "https://modrinth.com/settings/billing"), - ]) - .send() - .await - .map_err(|_| ApiError::Payments("Error while creating billing session!".to_string()))? - .json::() - .await - .map_err(|_| { - ApiError::Payments("Error while deserializing billing response!".to_string()) - })?; - - Ok(HttpResponse::Ok().json(json!( - { - "url": session.url - } - ))) -} - -#[post("/_stripe-webook")] -pub async fn handle_stripe_webhook( - body: String, - req: HttpRequest, - pool: web::Data, -) -> Result { - if let Some(signature_raw) = req - .headers() - .get("Stripe-Signature") - .and_then(|x| x.to_str().ok()) - { - let mut timestamp = None; - let mut signature = None; - for val in signature_raw.split(',') { - let key_val = val.split('=').collect_vec(); - - if key_val.len() == 2 { - if key_val[0] == "v1" { - signature = hex::decode(key_val[1]).ok() - } else if key_val[0] == "t" { - timestamp = key_val[1].parse::().ok() - } - } - } - - if let Some(timestamp) = timestamp { - if let Some(signature) = signature { - type HmacSha256 = Hmac; - - let mut key = - HmacSha256::new_from_slice(dotenvy::var("STRIPE_WEBHOOK_SECRET")?.as_bytes()) - .map_err(|_| { - ApiError::Crypto( - "Unable to initialize HMAC instance due to invalid key length!" - .to_string(), - ) - })?; - - key.update(format!("{timestamp}.{body}").as_bytes()); - - key.verify(&signature).map_err(|_| { - ApiError::Crypto("Unable to verify webhook signature!".to_string()) - })?; - - if timestamp < (Utc::now() - Duration::minutes(5)).timestamp() - || timestamp > (Utc::now() + Duration::minutes(5)).timestamp() - { - return Err(ApiError::Crypto("Webhook signature expired!".to_string())); - } - } else { - return Err(ApiError::Crypto("Missing signature!".to_string())); - } - } else { - return Err(ApiError::Crypto("Missing timestamp!".to_string())); - } - } else { - return Err(ApiError::Crypto("Missing signature header!".to_string())); - } - - #[derive(Deserialize)] - struct StripeWebhookBody { - #[serde(rename = "type")] - type_: String, - data: StripeWebhookObject, - } - - #[derive(Deserialize)] - struct StripeWebhookObject { - object: Value, - } - - let webhook: StripeWebhookBody = serde_json::from_str(&body)?; - - #[derive(Deserialize)] - struct CheckoutSession { - customer: String, - metadata: SessionMetadata, - } - - #[derive(Deserialize)] - struct SessionMetadata { - user_id: UserId, - } - - #[derive(Deserialize)] - struct Invoice { - customer: String, - // paid: bool, - lines: InvoiceLineItems, - } - - #[derive(Deserialize)] - struct InvoiceLineItems { - pub data: Vec, - } - - #[derive(Deserialize)] - struct InvoiceLineItem { - period: Period, - } - - #[derive(Deserialize)] - struct Period { - // start: i64, - end: i64, - } - - #[derive(Deserialize)] - struct Subscription { - customer: String, - } - - let mut transaction = pool.begin().await?; - - // TODO: Currently hardcoded to midas-only. When we add more stuff should include price IDs - match &*webhook.type_ { - "checkout.session.completed" => { - let session: CheckoutSession = serde_json::from_value(webhook.data.object)?; - - sqlx::query!( - " - UPDATE users - SET stripe_customer_id = $1 - WHERE (id = $2) - ", - session.customer, - session.metadata.user_id.0 as i64, - ) - .execute(&mut *transaction) - .await?; - } - "invoice.paid" => { - let invoice: Invoice = serde_json::from_value(webhook.data.object)?; - - if let Some(item) = invoice.lines.data.first() { - let expires: DateTime = DateTime::from_utc( - NaiveDateTime::from_timestamp_opt(item.period.end, 0).unwrap_or_default(), - Utc, - ) + Duration::days(1); - - sqlx::query!( - " - UPDATE users - SET midas_expires = $1, is_overdue = FALSE - WHERE (stripe_customer_id = $2) - ", - expires, - invoice.customer, - ) - .execute(&mut *transaction) - .await?; - } - } - "invoice.payment_failed" => { - let invoice: Invoice = serde_json::from_value(webhook.data.object)?; - - let customer_id = sqlx::query!( - " - SELECT u.id - FROM users u - WHERE u.stripe_customer_id = $1 - ", - invoice.customer, - ) - .fetch_optional(&**pool) - .await? - .map(|x| x.id); - - if let Some(user_id) = customer_id { - sqlx::query!( - " - UPDATE users - SET is_overdue = TRUE - WHERE (id = $1) - ", - user_id, - ) - .execute(&mut *transaction) - .await?; - } - } - "customer.subscription.deleted" => { - let session: Subscription = serde_json::from_value(webhook.data.object)?; - - sqlx::query!( - " - UPDATE users - SET stripe_customer_id = NULL, midas_expires = NULL, is_overdue = NULL - WHERE (stripe_customer_id = $1) - ", - session.customer, - ) - .execute(&mut *transaction) - .await?; - } - _ => {} - }; - - transaction.commit().await?; - - Ok(HttpResponse::NoContent().body("")) -} diff --git a/src/routes/v2/mod.rs b/src/routes/v2/mod.rs index 1318e0e7d..787c7d1ce 100644 --- a/src/routes/v2/mod.rs +++ b/src/routes/v2/mod.rs @@ -1,6 +1,4 @@ mod admin; -mod auth; -mod midas; mod moderation; mod notifications; mod pats; @@ -22,20 +20,26 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service( actix_web::web::scope("v2") .configure(admin::config) - .configure(auth::config) - .configure(midas::config) + .configure(crate::auth::config) .configure(moderation::config) .configure(notifications::config) .configure(pats::config) .configure(project_creation::config) + // SHOULD CACHE .configure(projects::config) .configure(reports::config) + // should cache in future .configure(statistics::config) + // should cache in future .configure(tags::config) + // should cache .configure(teams::config) .configure(threads::config) + // should cache .configure(users::config) + // should cache in future .configure(version_file::config) + // SHOULD CACHE .configure(versions::config), ); } diff --git a/src/routes/v2/moderation.rs b/src/routes/v2/moderation.rs index d69205c69..99e431ef7 100644 --- a/src/routes/v2/moderation.rs +++ b/src/routes/v2/moderation.rs @@ -1,7 +1,7 @@ use super::ApiError; +use crate::auth::check_is_moderator_from_headers; use crate::database; use crate::models::projects::ProjectStatus; -use crate::util::auth::check_is_moderator_from_headers; use actix_web::{get, web, HttpRequest, HttpResponse}; use serde::Deserialize; use sqlx::PgPool; @@ -24,9 +24,10 @@ fn default_count() -> i16 { pub async fn get_projects( req: HttpRequest, pool: web::Data, + redis: web::Data, count: web::Query, ) -> Result { - check_is_moderator_from_headers(req.headers(), &**pool).await?; + check_is_moderator_from_headers(req.headers(), &**pool, &redis).await?; use futures::stream::TryStreamExt; @@ -45,7 +46,7 @@ pub async fn get_projects( .try_collect::>() .await?; - let projects: Vec<_> = database::Project::get_many_full(&project_ids, &**pool) + let projects: Vec<_> = database::Project::get_many_ids(&project_ids, &**pool, &redis) .await? .into_iter() .map(crate::models::projects::Project::from) diff --git a/src/routes/v2/notifications.rs b/src/routes/v2/notifications.rs index e47f3403b..dcaa5ed21 100644 --- a/src/routes/v2/notifications.rs +++ b/src/routes/v2/notifications.rs @@ -1,8 +1,8 @@ +use crate::auth::get_user_from_headers; use crate::database; use crate::models::ids::NotificationId; use crate::models::notifications::Notification; use crate::routes::ApiError; -use crate::util::auth::get_user_from_headers; use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; @@ -30,8 +30,9 @@ pub async fn notifications_get( req: HttpRequest, web::Query(ids): web::Query, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; use database::models::notification_item::Notification as DBNotification; use database::models::NotificationId as DBNotificationId; @@ -60,8 +61,9 @@ pub async fn notification_get( req: HttpRequest, info: web::Path<(NotificationId,)>, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let id = info.into_inner().0; @@ -84,8 +86,9 @@ pub async fn notification_read( req: HttpRequest, info: web::Path<(NotificationId,)>, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let id = info.into_inner().0; @@ -117,8 +120,9 @@ pub async fn notification_delete( req: HttpRequest, info: web::Path<(NotificationId,)>, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let id = info.into_inner().0; @@ -150,8 +154,9 @@ pub async fn notifications_read( req: HttpRequest, web::Query(ids): web::Query, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let notification_ids = serde_json::from_str::>(&ids.ids)? .into_iter() @@ -185,8 +190,9 @@ pub async fn notifications_delete( req: HttpRequest, web::Query(ids): web::Query, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let notification_ids = serde_json::from_str::>(&ids.ids)? .into_iter() diff --git a/src/routes/v2/pats.rs b/src/routes/v2/pats.rs index 04b4d875f..040dfc6c5 100644 --- a/src/routes/v2/pats.rs +++ b/src/routes/v2/pats.rs @@ -9,10 +9,10 @@ use crate::database; use crate::database::models::generate_pat_id; use crate::models::ids::base62_impl::{parse_base62, to_base62}; +use crate::auth::get_user_from_headers; +use crate::auth::{generate_pat, PersonalAccessToken}; use crate::models::users::UserId; use crate::routes::ApiError; -use crate::util::auth::get_user_from_headers; -use crate::util::pat::{generate_pat, PersonalAccessToken}; use actix_web::web::{self, Data, Query}; use actix_web::{delete, get, patch, post, HttpRequest, HttpResponse}; @@ -46,8 +46,13 @@ pub struct ModifyPersonalAccessToken { // Get all personal access tokens for the given user. Minos/Kratos cookie must be attached for it to work. // Does not return the actual access token, only the ID + metadata. #[get("pat")] -pub async fn get_pats(req: HttpRequest, pool: Data) -> Result { - let user: crate::models::users::User = get_user_from_headers(req.headers(), &**pool).await?; +pub async fn get_pats( + req: HttpRequest, + pool: Data, + redis: Data, +) -> Result { + let user: crate::models::users::User = + get_user_from_headers(req.headers(), &**pool, &redis).await?; let db_user_id: database::models::UserId = database::models::UserId::from(user.id); let pats = sqlx::query!( @@ -84,8 +89,10 @@ pub async fn create_pat( req: HttpRequest, Query(info): Query, // callback url pool: Data, + redis: web::Data, ) -> Result { - let user: crate::models::users::User = get_user_from_headers(req.headers(), &**pool).await?; + let user: crate::models::users::User = + get_user_from_headers(req.headers(), &**pool, &redis).await?; let db_user_id: database::models::UserId = database::models::UserId::from(user.id); let mut transaction: sqlx::Transaction = pool.begin().await?; @@ -135,8 +142,10 @@ pub async fn edit_pat( id: web::Path, Query(info): Query, // callback url pool: Data, + redis: web::Data, ) -> Result { - let user: crate::models::users::User = get_user_from_headers(req.headers(), &**pool).await?; + let user: crate::models::users::User = + get_user_from_headers(req.headers(), &**pool, &redis).await?; let pat_id = database::models::PatId(parse_base62(&id)? as i64); let db_user_id: database::models::UserId = database::models::UserId::from(user.id); @@ -198,8 +207,10 @@ pub async fn delete_pat( req: HttpRequest, id: web::Path, pool: Data, + redis: web::Data, ) -> Result { - let user: crate::models::users::User = get_user_from_headers(req.headers(), &**pool).await?; + let user: crate::models::users::User = + get_user_from_headers(req.headers(), &**pool, &redis).await?; let pat_id = database::models::PatId(parse_base62(&id)? as i64); let db_user_id: database::models::UserId = database::models::UserId::from(user.id); diff --git a/src/routes/v2/project_creation.rs b/src/routes/v2/project_creation.rs index 1e10ba29a..69f49c81e 100644 --- a/src/routes/v2/project_creation.rs +++ b/src/routes/v2/project_creation.rs @@ -1,4 +1,5 @@ use super::version_creation::InitialVersionData; +use crate::auth::{get_user_from_headers, AuthenticationError}; use crate::database::models; use crate::database::models::thread_item::ThreadBuilder; use crate::file_hosting::{FileHost, FileHostingError}; @@ -10,7 +11,6 @@ use crate::models::projects::{ use crate::models::threads::ThreadType; use crate::models::users::UserId; use crate::search::indexing::IndexingError; -use crate::util::auth::{get_user_from_headers_transaction, AuthenticationError}; use crate::util::routes::read_from_field; use crate::util::validate::validation_errors_to_string; use actix_multipart::{Field, Multipart}; @@ -270,6 +270,7 @@ pub async fn project_create( req: HttpRequest, mut payload: Multipart, client: Data, + redis: Data, file_host: Data>, ) -> Result { let mut transaction = client.begin().await?; @@ -282,6 +283,7 @@ pub async fn project_create( &***file_host, &mut uploaded_files, &client, + &redis, ) .await; @@ -336,12 +338,13 @@ async fn project_create_inner( file_host: &dyn FileHost, uploaded_files: &mut Vec, pool: &PgPool, + redis: &deadpool_redis::Pool, ) -> Result { // The base URL for files uploaded to backblaze let cdn_url = dotenvy::var("CDN_URL")?; // The currently logged in user - let current_user = get_user_from_headers_transaction(req.headers(), &mut *transaction).await?; + let current_user = get_user_from_headers(req.headers(), pool, redis).await?; let project_id: ProjectId = models::generate_project_id(transaction).await?.into(); diff --git a/src/routes/v2/projects.rs b/src/routes/v2/projects.rs index b76b5f6dc..0e24df09c 100644 --- a/src/routes/v2/projects.rs +++ b/src/routes/v2/projects.rs @@ -1,3 +1,4 @@ +use crate::auth::{filter_authorized_projects, get_user_from_headers, is_authorized}; use crate::database; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::thread_item::ThreadMessageBuilder; @@ -12,7 +13,6 @@ use crate::models::teams::Permissions; use crate::models::threads::MessageBody; use crate::routes::ApiError; use crate::search::{search_for_project, SearchConfig, SearchError}; -use crate::util::auth::{filter_authorized_projects, get_user_from_headers, is_authorized}; use crate::util::routes::read_from_payload; use crate::util::validate::validation_errors_to_string; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; @@ -74,6 +74,7 @@ pub struct RandomProjects { pub async fn random_projects_get( web::Query(count): web::Query, pool: web::Data, + redis: web::Data, ) -> Result { count .validate() @@ -94,7 +95,7 @@ pub async fn random_projects_get( .try_collect::>() .await?; - let projects_data = database::models::Project::get_many_full(&project_ids, &**pool) + let projects_data = database::models::Project::get_many_ids(&project_ids, &**pool, &redis) .await? .into_iter() .map(Project::from) @@ -113,16 +114,14 @@ pub async fn projects_get( req: HttpRequest, web::Query(ids): web::Query, pool: web::Data, + redis: web::Data, ) -> Result { - let project_ids: Vec = - serde_json::from_str::>(&ids.ids)? - .into_iter() - .map(|x| x.into()) - .collect(); + let ids = serde_json::from_str::>(&ids.ids)?; + let projects_data = database::models::Project::get_many(&ids, &**pool, &redis).await?; - let projects_data = database::models::Project::get_many_full(&project_ids, &**pool).await?; - - let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); let projects = filter_authorized_projects(projects_data, &user_option, &pool).await?; @@ -134,13 +133,15 @@ pub async fn project_get( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, ) -> Result { let string = info.into_inner().0; - let project_data = - database::models::Project::get_full_from_slug_or_project_id(&string, &**pool).await?; + let project_data = database::models::Project::get(&string, &**pool, &redis).await?; - let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); if let Some(data) = project_data { if is_authorized(&data.inner, &user_option, &pool).await? { @@ -155,52 +156,15 @@ pub async fn project_get( pub async fn project_get_check( info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, ) -> Result { let slug = info.into_inner().0; - let id_option = parse_base62(&slug).ok(); + let project_data = database::models::Project::get(&slug, &**pool, &redis).await?; - let id = if let Some(id) = id_option { - let id = sqlx::query!( - " - SELECT id FROM mods - WHERE id = $1 - ", - id as i64 - ) - .fetch_optional(&**pool) - .await?; - - if id.is_none() { - sqlx::query!( - " - SELECT id FROM mods - WHERE slug = LOWER($1) - ", - &slug - ) - .fetch_optional(&**pool) - .await? - .map(|x| x.id) - } else { - id.map(|x| x.id) - } - } else { - sqlx::query!( - " - SELECT id FROM mods - WHERE slug = LOWER($1) - ", - &slug - ) - .fetch_optional(&**pool) - .await? - .map(|x| x.id) - }; - - if let Some(id) = id { + if let Some(project) = project_data { Ok(HttpResponse::Ok().json(json! ({ - "id": models::ids::ProjectId(id as u64) + "id": models::ids::ProjectId::from(project.inner.id) }))) } else { Ok(HttpResponse::NotFound().body("")) @@ -218,52 +182,23 @@ pub async fn dependency_list( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, ) -> Result { let string = info.into_inner().0; - let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?; + let result = database::models::Project::get(&string, &**pool, &redis).await?; - let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); if let Some(project) = result { - if !is_authorized(&project, &user_option, &pool).await? { + if !is_authorized(&project.inner, &user_option, &pool).await? { return Ok(HttpResponse::NotFound().body("")); } - let id = project.id; - - use futures::stream::TryStreamExt; - - let dependencies = sqlx::query!( - " - SELECT d.dependency_id, COALESCE(vd.mod_id, 0) mod_id, d.mod_dependency_id - FROM versions v - INNER JOIN dependencies d ON d.dependent_id = v.id - LEFT JOIN versions vd ON d.dependency_id = vd.id - WHERE v.mod_id = $1 - ", - id as database::models::ProjectId - ) - .fetch_many(&**pool) - .try_filter_map(|e| async { - Ok(e.right().map(|x| { - ( - x.dependency_id.map(database::models::VersionId), - if x.mod_id == Some(0) { - None - } else { - x.mod_id.map(database::models::ProjectId) - }, - x.mod_dependency_id.map(database::models::ProjectId), - ) - })) - }) - .try_collect::, - Option, - Option, - )>>() - .await?; + let dependencies = + database::Project::get_dependencies(project.inner.id, &**pool, &redis).await?; let project_ids = dependencies .iter() @@ -285,8 +220,8 @@ pub async fn dependency_list( .filter_map(|x| x.0) .collect::>(); let (projects_result, versions_result) = futures::future::try_join( - database::Project::get_many_full(&project_ids, &**pool), - database::Version::get_many_full(&dep_version_ids, &**pool), + database::Project::get_many_ids(&project_ids, &**pool, &redis), + database::Version::get_many(&dep_version_ids, &**pool, &redis), ) .await?; @@ -417,16 +352,16 @@ pub async fn project_edit( pool: web::Data, config: web::Data, new_project: web::Json, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; new_project .validate() .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?; let string = info.into_inner().0; - let result = - database::models::Project::get_full_from_slug_or_project_id(&string, &**pool).await?; + let result = database::models::Project::get(&string, &**pool, &redis).await?; if let Some(project_item) = result { let id = project_item.inner.id; @@ -889,7 +824,7 @@ pub async fn project_edit( // Make sure the new slug is different from the old one // We are able to unwrap here because the slug is always set - if !slug.eq(&project_item.inner.slug.unwrap_or_default()) { + if !slug.eq(&project_item.inner.slug.clone().unwrap_or_default()) { let results = sqlx::query!( " SELECT EXISTS(SELECT 1 FROM mods WHERE slug = LOWER($1)) @@ -1151,6 +1086,14 @@ pub async fn project_edit( .await?; } + database::models::Project::clear_cache( + project_item.inner.id, + project_item.inner.slug, + None, + &redis, + ) + .await?; + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { @@ -1232,8 +1175,9 @@ pub async fn projects_edit( web::Query(ids): web::Query, pool: web::Data, bulk_edit_project: web::Json, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; bulk_edit_project .validate() @@ -1245,7 +1189,8 @@ pub async fn projects_edit( .map(|x| x.into()) .collect(); - let projects_data = database::models::Project::get_many_full(&project_ids, &**pool).await?; + let projects_data = + database::models::Project::get_many_ids(&project_ids, &**pool, &redis).await?; if let Some(id) = project_ids .iter() @@ -1262,7 +1207,7 @@ pub async fn projects_edit( .map(|x| x.inner.team_id) .collect::>(); let team_members = - database::models::TeamMember::get_from_team_full_many(&team_ids, &**pool).await?; + database::models::TeamMember::get_from_team_full_many(&team_ids, &**pool, &redis).await?; let categories = database::models::categories::Category::list(&**pool).await?; let donation_platforms = database::models::categories::DonationPlatform::list(&**pool).await?; @@ -1538,6 +1483,9 @@ pub async fn projects_edit( .execute(&mut *transaction) .await?; } + + database::models::Project::clear_cache(project.inner.id, project.inner.slug, None, &redis) + .await?; } transaction.commit().await?; @@ -1556,9 +1504,10 @@ pub async fn project_schedule( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, scheduling_data: web::Json, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; if scheduling_data.time < Utc::now() { return Err(ApiError::InvalidInput( @@ -1573,11 +1522,11 @@ pub async fn project_schedule( } let string = info.into_inner().0; - let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?; + let result = database::models::Project::get(&string, &**pool, &redis).await?; if let Some(project_item) = result { let team_member = database::models::TeamMember::get_from_user_id( - project_item.team_id, + project_item.inner.team_id, user.id.into(), &**pool, ) @@ -1601,11 +1550,19 @@ pub async fn project_schedule( ", ProjectStatus::Scheduled.as_str(), scheduling_data.time, - project_item.id as database::models::ids::ProjectId, + project_item.inner.id as database::models::ids::ProjectId, ) .execute(&**pool) .await?; + database::models::Project::clear_cache( + project_item.inner.id, + project_item.inner.slug, + None, + &redis, + ) + .await?; + Ok(HttpResponse::NoContent().body("")) } else { Ok(HttpResponse::NotFound().body("")) @@ -1623,15 +1580,16 @@ pub async fn project_icon_edit( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, file_host: web::Data>, mut payload: web::Payload, ) -> Result { if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) { let cdn_url = dotenvy::var("CDN_URL")?; - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let string = info.into_inner().0; - let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool) + let project_item = database::models::Project::get(&string, &**pool, &redis) .await? .ok_or_else(|| { ApiError::InvalidInput("The specified project does not exist!".to_string()) @@ -1639,7 +1597,7 @@ pub async fn project_icon_edit( if !user.role.is_mod() { let team_member = database::models::TeamMember::get_from_user_id( - project_item.team_id, + project_item.inner.team_id, user.id.into(), &**pool, ) @@ -1656,7 +1614,7 @@ pub async fn project_icon_edit( } } - if let Some(icon) = project_item.icon_url { + if let Some(icon) = project_item.inner.icon_url { let name = icon.split(&format!("{cdn_url}/")).nth(1); if let Some(icon_path) = name { @@ -1670,7 +1628,7 @@ pub async fn project_icon_edit( let color = crate::util::img::get_color_from_img(&bytes)?; let hash = sha1::Sha1::from(&bytes).hexdigest(); - let project_id: ProjectId = project_item.id.into(); + let project_id: ProjectId = project_item.inner.id.into(); let upload_data = file_host .upload_file( content_type, @@ -1689,11 +1647,19 @@ pub async fn project_icon_edit( ", format!("{}/{}", cdn_url, upload_data.file_name), color.map(|x| x as i32), - project_item.id as database::models::ids::ProjectId, + project_item.inner.id as database::models::ids::ProjectId, ) .execute(&mut *transaction) .await?; + database::models::Project::clear_cache( + project_item.inner.id, + project_item.inner.slug, + None, + &redis, + ) + .await?; + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) @@ -1710,12 +1676,13 @@ pub async fn delete_project_icon( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, file_host: web::Data>, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let string = info.into_inner().0; - let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool) + let project_item = database::models::Project::get(&string, &**pool, &redis) .await? .ok_or_else(|| { ApiError::InvalidInput("The specified project does not exist!".to_string()) @@ -1723,7 +1690,7 @@ pub async fn delete_project_icon( if !user.role.is_mod() { let team_member = database::models::TeamMember::get_from_user_id( - project_item.team_id, + project_item.inner.team_id, user.id.into(), &**pool, ) @@ -1741,7 +1708,7 @@ pub async fn delete_project_icon( } let cdn_url = dotenvy::var("CDN_URL")?; - if let Some(icon) = project_item.icon_url { + if let Some(icon) = project_item.inner.icon_url { let name = icon.split(&format!("{cdn_url}/")).nth(1); if let Some(icon_path) = name { @@ -1757,11 +1724,19 @@ pub async fn delete_project_icon( SET icon_url = NULL, color = NULL WHERE (id = $1) ", - project_item.id as database::models::ids::ProjectId, + project_item.inner.id as database::models::ids::ProjectId, ) .execute(&mut *transaction) .await?; + database::models::Project::clear_cache( + project_item.inner.id, + project_item.inner.slug, + None, + &redis, + ) + .await?; + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) @@ -1778,12 +1753,14 @@ pub struct GalleryCreateQuery { } #[post("{id}/gallery")] +#[allow(clippy::too_many_arguments)] pub async fn add_gallery_item( web::Query(ext): web::Query, req: HttpRequest, web::Query(item): web::Query, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, file_host: web::Data>, mut payload: web::Payload, ) -> Result { @@ -1792,15 +1769,14 @@ pub async fn add_gallery_item( .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?; let cdn_url = dotenvy::var("CDN_URL")?; - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let string = info.into_inner().0; - let project_item = - database::models::Project::get_full_from_slug_or_project_id(&string, &**pool) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("The specified project does not exist!".to_string()) - })?; + let project_item = database::models::Project::get(&string, &**pool, &redis) + .await? + .ok_or_else(|| { + ApiError::InvalidInput("The specified project does not exist!".to_string()) + })?; if project_item.gallery_items.len() > 64 { return Err(ApiError::CustomAuthentication( @@ -1880,6 +1856,14 @@ pub async fn add_gallery_item( .insert(project_item.inner.id, &mut transaction) .await?; + database::models::Project::clear_cache( + project_item.inner.id, + project_item.inner.slug, + None, + &redis, + ) + .await?; + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) @@ -1919,14 +1903,15 @@ pub async fn edit_gallery_item( web::Query(item): web::Query, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let string = info.into_inner().0; item.validate() .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?; - let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool) + let project_item = database::models::Project::get(&string, &**pool, &redis) .await? .ok_or_else(|| { ApiError::InvalidInput("The specified project does not exist!".to_string()) @@ -1934,7 +1919,7 @@ pub async fn edit_gallery_item( if !user.role.is_mod() { let team_member = database::models::TeamMember::get_from_user_id( - project_item.team_id, + project_item.inner.team_id, user.id.into(), &**pool, ) @@ -1979,7 +1964,7 @@ pub async fn edit_gallery_item( SET featured = $2 WHERE mod_id = $1 ", - project_item.id as database::models::ids::ProjectId, + project_item.inner.id as database::models::ids::ProjectId, false, ) .execute(&mut *transaction) @@ -2038,6 +2023,14 @@ pub async fn edit_gallery_item( .await?; } + database::models::Project::clear_cache( + project_item.inner.id, + project_item.inner.slug, + None, + &redis, + ) + .await?; + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) @@ -2054,12 +2047,13 @@ pub async fn delete_gallery_item( web::Query(item): web::Query, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, file_host: web::Data>, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let string = info.into_inner().0; - let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool) + let project_item = database::models::Project::get(&string, &**pool, &redis) .await? .ok_or_else(|| { ApiError::InvalidInput("The specified project does not exist!".to_string()) @@ -2067,7 +2061,7 @@ pub async fn delete_gallery_item( if !user.role.is_mod() { let team_member = database::models::TeamMember::get_from_user_id( - project_item.team_id, + project_item.inner.team_id, user.id.into(), &**pool, ) @@ -2121,6 +2115,14 @@ pub async fn delete_gallery_item( .execute(&mut *transaction) .await?; + database::models::Project::clear_cache( + project_item.inner.id, + project_item.inner.slug, + None, + &redis, + ) + .await?; + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) @@ -2131,12 +2133,13 @@ pub async fn project_delete( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, config: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let string = info.into_inner().0; - let project = database::models::Project::get_from_slug_or_project_id(&string, &**pool) + let project = database::models::Project::get(&string, &**pool, &redis) .await? .ok_or_else(|| { ApiError::InvalidInput("The specified project does not exist!".to_string()) @@ -2144,7 +2147,7 @@ pub async fn project_delete( if !user.role.is_admin() { let team_member = database::models::TeamMember::get_from_user_id_project( - project.id, + project.inner.id, user.id.into(), &**pool, ) @@ -2166,11 +2169,12 @@ pub async fn project_delete( let mut transaction = pool.begin().await?; - let result = database::models::Project::remove_full(project.id, &mut transaction).await?; + let result = + database::models::Project::remove(project.inner.id, &mut transaction, &redis).await?; transaction.commit().await?; - delete_from_index(project.id.into(), config).await?; + delete_from_index(project.inner.id.into(), config).await?; if result.is_some() { Ok(HttpResponse::NoContent().body("")) @@ -2184,20 +2188,21 @@ pub async fn project_follow( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let string = info.into_inner().0; - let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool) + let result = database::models::Project::get(&string, &**pool, &redis) .await? .ok_or_else(|| { ApiError::InvalidInput("The specified project does not exist!".to_string()) })?; let user_id: database::models::ids::UserId = user.id.into(); - let project_id: database::models::ids::ProjectId = result.id; + let project_id: database::models::ids::ProjectId = result.inner.id; - if !is_authorized(&result, &Some(user), &pool).await? { + if !is_authorized(&result.inner, &Some(user), &pool).await? { return Ok(HttpResponse::NotFound().body("")); } @@ -2253,18 +2258,19 @@ pub async fn project_unfollow( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let string = info.into_inner().0; - let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool) + let result = database::models::Project::get(&string, &**pool, &redis) .await? .ok_or_else(|| { ApiError::InvalidInput("The specified project does not exist!".to_string()) })?; let user_id: database::models::ids::UserId = user.id.into(); - let project_id = result.id; + let project_id = result.inner.id; let following = sqlx::query!( " diff --git a/src/routes/v2/reports.rs b/src/routes/v2/reports.rs index 63bdd8fa4..11b7d4b3b 100644 --- a/src/routes/v2/reports.rs +++ b/src/routes/v2/reports.rs @@ -1,11 +1,9 @@ +use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; use crate::database::models::thread_item::{ThreadBuilder, ThreadMessageBuilder}; use crate::models::ids::{base62_impl::parse_base62, ProjectId, UserId, VersionId}; use crate::models::reports::{ItemType, Report}; use crate::models::threads::{MessageBody, ThreadType}; use crate::routes::ApiError; -use crate::util::auth::{ - check_is_moderator_from_headers, get_user_from_headers, get_user_from_headers_transaction, -}; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use chrono::Utc; use futures::StreamExt; @@ -35,10 +33,11 @@ pub async fn report_create( req: HttpRequest, pool: web::Data, mut body: web::Payload, + redis: web::Data, ) -> Result { let mut transaction = pool.begin().await?; - let current_user = get_user_from_headers_transaction(req.headers(), &mut transaction).await?; + let current_user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let mut bytes = web::BytesMut::new(); while let Some(item) = body.next().await { @@ -179,9 +178,10 @@ fn default_all() -> bool { pub async fn reports( req: HttpRequest, pool: web::Data, + redis: web::Data, count: web::Query, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; use futures::stream::TryStreamExt; @@ -225,10 +225,10 @@ pub async fn reports( let query_reports = crate::database::models::report_item::Report::get_many(&report_ids, &**pool).await?; - let mut reports = Vec::new(); + let mut reports: Vec = Vec::new(); for x in query_reports { - reports.push(to_report(x)); + reports.push(x.into()); } Ok(HttpResponse::Ok().json(reports)) @@ -244,6 +244,7 @@ pub async fn reports_get( req: HttpRequest, web::Query(ids): web::Query, pool: web::Data, + redis: web::Data, ) -> Result { let report_ids: Vec = serde_json::from_str::>(&ids.ids)? @@ -254,12 +255,12 @@ pub async fn reports_get( let reports_data = crate::database::models::report_item::Report::get_many(&report_ids, &**pool).await?; - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let all_reports = reports_data .into_iter() .filter(|x| user.role.is_mod() || x.reporter == user.id.into()) - .map(to_report) + .map(|x| x.into()) .collect::>(); Ok(HttpResponse::Ok().json(all_reports)) @@ -269,9 +270,10 @@ pub async fn reports_get( pub async fn report_get( req: HttpRequest, pool: web::Data, + redis: web::Data, info: web::Path<(crate::models::reports::ReportId,)>, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let id = info.into_inner().0.into(); let report = crate::database::models::report_item::Report::get(id, &**pool).await?; @@ -281,7 +283,8 @@ pub async fn report_get( return Ok(HttpResponse::NotFound().body("")); } - Ok(HttpResponse::Ok().json(to_report(report))) + let report: Report = report.into(); + Ok(HttpResponse::Ok().json(report)) } else { Ok(HttpResponse::NotFound().body("")) } @@ -298,10 +301,11 @@ pub struct EditReport { pub async fn report_edit( req: HttpRequest, pool: web::Data, + redis: web::Data, info: web::Path<(crate::models::reports::ReportId,)>, edit_report: web::Json, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let id = info.into_inner().0.into(); let report = crate::database::models::report_item::Report::get(id, &**pool).await?; @@ -374,8 +378,9 @@ pub async fn report_delete( req: HttpRequest, pool: web::Data, info: web::Path<(crate::models::reports::ReportId,)>, + redis: web::Data, ) -> Result { - check_is_moderator_from_headers(req.headers(), &**pool).await?; + check_is_moderator_from_headers(req.headers(), &**pool, &redis).await?; let mut transaction = pool.begin().await?; let result = crate::database::models::report_item::Report::remove_full( @@ -391,31 +396,3 @@ pub async fn report_delete( Ok(HttpResponse::NotFound().body("")) } } - -fn to_report(x: crate::database::models::report_item::QueryReport) -> Report { - let mut item_id = "".to_string(); - let mut item_type = ItemType::Unknown; - - if let Some(project_id) = x.project_id { - item_id = ProjectId::from(project_id).to_string(); - item_type = ItemType::Project; - } else if let Some(version_id) = x.version_id { - item_id = VersionId::from(version_id).to_string(); - item_type = ItemType::Version; - } else if let Some(user_id) = x.user_id { - item_id = UserId::from(user_id).to_string(); - item_type = ItemType::User; - } - - Report { - id: x.id.into(), - report_type: x.report_type, - item_id, - item_type, - reporter: x.reporter.into(), - body: x.body, - created: x.created, - closed: x.closed, - thread_id: x.thread_id.map(|x| x.into()), - } -} diff --git a/src/routes/v2/teams.rs b/src/routes/v2/teams.rs index 027f13f80..732679061 100644 --- a/src/routes/v2/teams.rs +++ b/src/routes/v2/teams.rs @@ -1,3 +1,4 @@ +use crate::auth::{get_user_from_headers, is_authorized}; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::TeamMember; use crate::models::ids::ProjectId; @@ -5,7 +6,6 @@ use crate::models::notifications::NotificationBody; use crate::models::teams::{Permissions, TeamId}; use crate::models::users::UserId; use crate::routes::ApiError; -use crate::util::auth::{get_user_from_headers, is_authorized}; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -30,25 +30,27 @@ pub async fn team_members_get_project( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, ) -> Result { let string = info.into_inner().0; - let project_data = - crate::database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?; + let project_data = crate::database::models::Project::get(&string, &**pool, &redis).await?; if let Some(project) = project_data { - let current_user = get_user_from_headers(req.headers(), &**pool).await.ok(); + let current_user = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); - if !is_authorized(&project, ¤t_user, &pool).await? { + let members_data = + TeamMember::get_from_team_full(project.inner.team_id, &**pool, &redis).await?; + + if !is_authorized(&project.inner, ¤t_user, &pool).await? { return Ok(HttpResponse::NotFound().body("")); } - let members_data = TeamMember::get_from_team_full(project.team_id, &**pool).await?; - if let Some(user) = ¤t_user { - let team_member = - TeamMember::get_from_user_id(project.team_id, user.id.into(), &**pool) - .await - .map_err(ApiError::Database)?; + let team_member = members_data + .iter() + .find(|x| x.user.id == user.id.into() && x.accepted); if team_member.is_some() { let team_members: Vec<_> = members_data @@ -83,16 +85,19 @@ pub async fn team_members_get( req: HttpRequest, info: web::Path<(TeamId,)>, pool: web::Data, + redis: web::Data, ) -> Result { let id = info.into_inner().0; - let members_data = TeamMember::get_from_team_full(id.into(), &**pool).await?; + let members_data = TeamMember::get_from_team_full(id.into(), &**pool, &redis).await?; - let current_user = get_user_from_headers(req.headers(), &**pool).await.ok(); + let current_user = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); if let Some(user) = ¤t_user { - let team_member = TeamMember::get_from_user_id(id.into(), user.id.into(), &**pool) - .await - .map_err(ApiError::Database)?; + let team_member = members_data + .iter() + .find(|x| x.user.id == user.id.into() && x.accepted); if team_member.is_some() { let team_members: Vec<_> = members_data @@ -129,6 +134,7 @@ pub async fn teams_get( req: HttpRequest, web::Query(ids): web::Query, pool: web::Data, + redis: web::Data, ) -> Result { use itertools::Itertools; @@ -137,34 +143,39 @@ pub async fn teams_get( .map(|x| x.into()) .collect::>(); - let teams_data = TeamMember::get_from_team_full_many(&team_ids, &**pool).await?; + let teams_data = TeamMember::get_from_team_full_many(&team_ids, &**pool, &redis).await?; - let current_user = get_user_from_headers(req.headers(), &**pool).await.ok(); - let accepted = if let Some(user) = current_user { - TeamMember::get_from_user_id_many(&team_ids, user.id.into(), &**pool) - .await? - .into_iter() - .map(|m| m.team_id.0) - .collect() - } else { - std::collections::HashSet::new() - }; + let current_user = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); let teams_groups = teams_data.into_iter().group_by(|data| data.team_id.0); let mut teams: Vec> = vec![]; - for (id, member_data) in &teams_groups { - if accepted.contains(&id) { - let team_members = - member_data.map(|data| crate::models::teams::TeamMember::from(data, false)); + for (_, member_data) in &teams_groups { + let members = member_data.collect::>(); + + let team_member = if let Some(user) = ¤t_user { + members + .iter() + .find(|x| x.user.id == user.id.into() && x.accepted) + } else { + None + }; + + if team_member.is_some() { + let team_members = members + .into_iter() + .map(|data| crate::models::teams::TeamMember::from(data, false)); teams.push(team_members.collect()); continue; } - let team_members = member_data + let team_members = members + .into_iter() .filter(|x| x.accepted) .map(|data| crate::models::teams::TeamMember::from(data, true)); @@ -179,9 +190,10 @@ pub async fn join_team( req: HttpRequest, info: web::Path<(TeamId,)>, pool: web::Data, + redis: web::Data, ) -> Result { let team_id = info.into_inner().0.into(); - let current_user = get_user_from_headers(req.headers(), &**pool).await?; + let current_user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let member = TeamMember::get_from_user_id_pending(team_id, current_user.id.into(), &**pool).await?; @@ -207,6 +219,8 @@ pub async fn join_team( ) .await?; + TeamMember::clear_cache(team_id, &redis).await?; + transaction.commit().await?; } else { return Err(ApiError::InvalidInput( @@ -244,12 +258,13 @@ pub async fn add_team_member( info: web::Path<(TeamId,)>, pool: web::Data, new_member: web::Json, + redis: web::Data, ) -> Result { let team_id = info.into_inner().0.into(); let mut transaction = pool.begin().await?; - let current_user = get_user_from_headers(req.headers(), &**pool).await?; + let current_user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let member = TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool) .await? .ok_or_else(|| { @@ -281,12 +296,8 @@ pub async fn add_team_member( )); } - let request = crate::database::models::team_item::TeamMember::get_from_user_id_pending( - team_id, - new_member.user_id.into(), - &**pool, - ) - .await?; + let request = + TeamMember::get_from_user_id_pending(team_id, new_member.user_id.into(), &**pool).await?; if let Some(req) = request { if req.accepted { @@ -300,7 +311,7 @@ pub async fn add_team_member( } } - crate::database::models::User::get(member.user_id, &**pool) + crate::database::models::User::get_id(member.user_id, &**pool, &redis) .await? .ok_or_else(|| ApiError::InvalidInput("An invalid User ID specified".to_string()))?; @@ -340,6 +351,8 @@ pub async fn add_team_member( .insert(new_member.user_id.into(), &mut transaction) .await?; + TeamMember::clear_cache(team_id, &redis).await?; + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) @@ -359,12 +372,13 @@ pub async fn edit_team_member( info: web::Path<(TeamId, UserId)>, pool: web::Data, edit_member: web::Json, + redis: web::Data, ) -> Result { let ids = info.into_inner(); let id = ids.0.into(); let user_id = ids.1.into(); - let current_user = get_user_from_headers(req.headers(), &**pool).await?; + let current_user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool) .await? .ok_or_else(|| { @@ -430,6 +444,8 @@ pub async fn edit_team_member( ) .await?; + TeamMember::clear_cache(id, &redis).await?; + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) @@ -446,10 +462,11 @@ pub async fn transfer_ownership( info: web::Path<(TeamId,)>, pool: web::Data, new_owner: web::Json, + redis: web::Data, ) -> Result { let id = info.into_inner().0; - let current_user = get_user_from_headers(req.headers(), &**pool).await?; + let current_user = get_user_from_headers(req.headers(), &**pool, &redis).await?; if !current_user.role.is_admin() { let member = TeamMember::get_from_user_id(id.into(), current_user.id.into(), &**pool) @@ -505,6 +522,8 @@ pub async fn transfer_ownership( ) .await?; + TeamMember::clear_cache(id.into(), &redis).await?; + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) @@ -515,12 +534,13 @@ pub async fn remove_team_member( req: HttpRequest, info: web::Path<(TeamId, UserId)>, pool: web::Data, + redis: web::Data, ) -> Result { let ids = info.into_inner(); let id = ids.0.into(); let user_id = ids.1.into(); - let current_user = get_user_from_headers(req.headers(), &**pool).await?; + let current_user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool) .await? .ok_or_else(|| { @@ -566,6 +586,8 @@ pub async fn remove_team_member( )); } + TeamMember::clear_cache(id, &redis).await?; + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { diff --git a/src/routes/v2/threads.rs b/src/routes/v2/threads.rs index 08bb1296a..d120ba17d 100644 --- a/src/routes/v2/threads.rs +++ b/src/routes/v2/threads.rs @@ -1,13 +1,13 @@ +use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; use crate::database; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::thread_item::ThreadMessageBuilder; use crate::models::ids::ThreadMessageId; use crate::models::notifications::NotificationBody; use crate::models::projects::ProjectStatus; -use crate::models::threads::{MessageBody, Thread, ThreadId, ThreadMessage, ThreadType}; +use crate::models::threads::{MessageBody, Thread, ThreadId, ThreadType}; use crate::models::users::User; use crate::routes::ApiError; -use crate::util::auth::{check_is_moderator_from_headers, get_user_from_headers}; use actix_web::{delete, get, post, web, HttpRequest, HttpResponse}; use futures::TryStreamExt; use serde::Deserialize; @@ -68,6 +68,7 @@ pub async fn filter_authorized_threads( threads: Vec, user: &User, pool: &web::Data, + redis: &deadpool_redis::Pool, ) -> Result, ApiError> { let user_id: database::models::UserId = user.id.into(); @@ -171,7 +172,7 @@ pub async fn filter_authorized_threads( .collect::>(), ); - let users: Vec = database::models::User::get_many(&user_ids, &***pool) + let users: Vec = database::models::User::get_many_ids(&user_ids, &***pool, redis) .await? .into_iter() .map(From::from) @@ -190,7 +191,7 @@ pub async fn filter_authorized_threads( .collect::>(), ); - final_threads.push(convert_thread( + final_threads.push(Thread::from( thread, users .iter() @@ -204,56 +205,18 @@ pub async fn filter_authorized_threads( Ok(final_threads) } -fn convert_thread(data: database::models::Thread, users: Vec, user: &User) -> Thread { - let thread_type = data.type_; - - Thread { - id: data.id.into(), - type_: thread_type, - messages: data - .messages - .into_iter() - .filter(|x| { - if let MessageBody::Text { private, .. } = x.body { - !private || user.role.is_mod() - } else { - true - } - }) - .map(|x| ThreadMessage { - id: x.id.into(), - author_id: if users - .iter() - .find(|y| x.author_id == Some(y.id.into())) - .map(|x| x.role.is_mod() && !user.role.is_mod()) - .unwrap_or(false) - { - None - } else { - x.author_id.map(|x| x.into()) - }, - body: x.body, - created: x.created, - }) - .collect(), - members: users - .into_iter() - .filter(|x| !x.role.is_mod() || user.role.is_mod()) - .collect(), - } -} - #[get("{id}")] pub async fn thread_get( req: HttpRequest, info: web::Path<(ThreadId,)>, pool: web::Data, + redis: web::Data, ) -> Result { let string = info.into_inner().0.into(); let thread_data = database::models::Thread::get(string, &**pool).await?; - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; if let Some(mut data) = thread_data { if is_authorized_thread(&data, &user, &pool).await? { @@ -267,13 +230,13 @@ pub async fn thread_get( .collect::>(), ); - let users: Vec = database::models::User::get_many(authors, &**pool) + let users: Vec = database::models::User::get_many_ids(authors, &**pool, &redis) .await? .into_iter() .map(From::from) .collect(); - return Ok(HttpResponse::Ok().json(convert_thread(data, users, &user))); + return Ok(HttpResponse::Ok().json(Thread::from(data, users, &user))); } } Ok(HttpResponse::NotFound().body("")) @@ -289,8 +252,9 @@ pub async fn threads_get( req: HttpRequest, web::Query(ids): web::Query, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let thread_ids: Vec = serde_json::from_str::>(&ids.ids)? @@ -300,7 +264,7 @@ pub async fn threads_get( let threads_data = database::models::Thread::get_many(&thread_ids, &**pool).await?; - let threads = filter_authorized_threads(threads_data, &user, &pool).await?; + let threads = filter_authorized_threads(threads_data, &user, &pool, &redis).await?; Ok(HttpResponse::Ok().json(threads)) } @@ -316,8 +280,9 @@ pub async fn thread_send_message( info: web::Path<(ThreadId,)>, pool: web::Data, new_message: web::Json, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let string: database::models::ThreadId = info.into_inner().0.into(); @@ -392,6 +357,7 @@ pub async fn thread_send_message( let members = database::models::TeamMember::get_from_team_full( database::models::TeamId(record.team_id), &**pool, + &redis, ) .await?; @@ -475,8 +441,9 @@ pub async fn thread_send_message( pub async fn moderation_inbox( req: HttpRequest, pool: web::Data, + redis: web::Data, ) -> Result { - let user = check_is_moderator_from_headers(req.headers(), &**pool).await?; + let user = check_is_moderator_from_headers(req.headers(), &**pool, &redis).await?; let ids = sqlx::query!( " @@ -491,7 +458,7 @@ pub async fn moderation_inbox( .await?; let threads_data = database::models::Thread::get_many(&ids, &**pool).await?; - let threads = filter_authorized_threads(threads_data, &user, &pool).await?; + let threads = filter_authorized_threads(threads_data, &user, &pool, &redis).await?; Ok(HttpResponse::Ok().json(threads)) } @@ -501,8 +468,9 @@ pub async fn thread_read( req: HttpRequest, info: web::Path<(ThreadId,)>, pool: web::Data, + redis: web::Data, ) -> Result { - check_is_moderator_from_headers(req.headers(), &**pool).await?; + check_is_moderator_from_headers(req.headers(), &**pool, &redis).await?; let id = info.into_inner().0; let mut transaction = pool.begin().await?; @@ -528,8 +496,9 @@ pub async fn message_delete( req: HttpRequest, info: web::Path<(ThreadMessageId,)>, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let result = database::models::ThreadMessage::get(info.into_inner().0.into(), &**pool).await?; diff --git a/src/routes/v2/users.rs b/src/routes/v2/users.rs index 14666e6a1..990318f70 100644 --- a/src/routes/v2/users.rs +++ b/src/routes/v2/users.rs @@ -1,3 +1,4 @@ +use crate::auth::get_user_from_headers; use crate::database::models::User; use crate::file_hosting::FileHost; use crate::models::notifications::Notification; @@ -5,7 +6,6 @@ use crate::models::projects::Project; use crate::models::users::{Badges, RecipientType, RecipientWallet, Role, UserId}; use crate::queue::payouts::{PayoutAmount, PayoutItem, PayoutsQueue}; use crate::routes::ApiError; -use crate::util::auth::get_user_from_headers; use crate::util::routes::read_from_payload; use crate::util::validate::validation_errors_to_string; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; @@ -43,8 +43,9 @@ pub fn config(cfg: &mut web::ServiceConfig) { pub async fn user_auth_get( req: HttpRequest, pool: web::Data, + redis: web::Data, ) -> Result { - Ok(HttpResponse::Ok().json(get_user_from_headers(req.headers(), &**pool).await?)) + Ok(HttpResponse::Ok().json(get_user_from_headers(req.headers(), &**pool, &redis).await?)) } #[derive(Serialize)] @@ -57,8 +58,9 @@ pub struct UserData { pub async fn user_data_get( req: HttpRequest, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let data = sqlx::query!( " @@ -93,13 +95,11 @@ pub struct UserIds { pub async fn users_get( web::Query(ids): web::Query, pool: web::Data, + redis: web::Data, ) -> Result { - let user_ids = serde_json::from_str::>(&ids.ids)? - .into_iter() - .map(|x| x.into()) - .collect::>(); + let user_ids = serde_json::from_str::>(&ids.ids)?; - let users_data = User::get_many(&user_ids, &**pool).await?; + let users_data = User::get_many(&user_ids, &**pool, &redis).await?; let users: Vec = users_data.into_iter().map(From::from).collect(); @@ -110,21 +110,9 @@ pub async fn users_get( pub async fn user_get( info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, ) -> Result { - let string = info.into_inner().0; - let id_option: Option = serde_json::from_str(&format!("\"{string}\"")).ok(); - - let mut user_data; - - if let Some(id) = id_option { - user_data = User::get(id.into(), &**pool).await?; - - if user_data.is_none() { - user_data = User::get_from_username(string, &**pool).await?; - } - } else { - user_data = User::get_from_username(string, &**pool).await?; - } + let user_data = User::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(data) = user_data { let response: crate::models::users::User = data.into(); @@ -139,12 +127,15 @@ pub async fn projects_list( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await.ok(); + let user = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); - let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?; + let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; - if let Some(id) = id_option { + if let Some(id) = id_option.map(|x| x.id) { let user_id: UserId = id.into(); let can_view_private = user @@ -153,12 +144,13 @@ pub async fn projects_list( let project_data = User::get_projects(id, &**pool).await?; - let response: Vec<_> = crate::database::Project::get_many_full(&project_data, &**pool) - .await? - .into_iter() - .filter(|x| can_view_private || x.inner.status.is_searchable()) - .map(Project::from) - .collect(); + let response: Vec<_> = + crate::database::Project::get_many_ids(&project_data, &**pool, &redis) + .await? + .into_iter() + .filter(|x| can_view_private || x.inner.status.is_searchable()) + .map(Project::from) + .collect(); Ok(HttpResponse::Ok().json(response)) } else { @@ -211,29 +203,30 @@ pub struct EditPayoutData { pub async fn user_edit( req: HttpRequest, info: web::Path<(String,)>, - pool: web::Data, new_user: web::Json, + pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; new_user .validate() .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?; - let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?; + let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; - if let Some(id) = id_option { + if let Some(actual_user) = id_option { + let id = actual_user.id; let user_id: UserId = id.into(); if user.id == user_id || user.role.is_mod() { let mut transaction = pool.begin().await?; if let Some(username) = &new_user.username { - let existing_user_id_option = - User::get_id_from_username_or_id(username, &**pool).await?; + let existing_user_id_option = User::get(username, &**pool, &redis).await?; if existing_user_id_option - .map(UserId::from) + .map(|x| UserId::from(x.id)) .map(|id| id == user.id) .unwrap_or(true) { @@ -394,6 +387,7 @@ pub async fn user_edit( } } + User::clear_caches(&[(id, Some(actual_user.username))], &redis).await?; transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { @@ -417,34 +411,24 @@ pub async fn user_icon_edit( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, file_host: web::Data>, mut payload: web::Payload, ) -> Result { if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) { let cdn_url = dotenvy::var("CDN_URL")?; - let user = get_user_from_headers(req.headers(), &**pool).await?; - let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; + let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; - if let Some(id) = id_option { - if user.id != id.into() && !user.role.is_mod() { + 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(), )); } - let mut icon_url = user.avatar_url; - - let user_id: UserId = id.into(); - - if user.id != user_id { - let new_user = User::get(id, &**pool).await?; - - if let Some(new) = new_user { - icon_url = new.avatar_url; - } else { - return Ok(HttpResponse::NotFound().body("")); - } - } + let icon_url = actual_user.avatar_url; + let user_id: UserId = actual_user.id.into(); if let Some(icon) = icon_url { let name = icon.split(&format!("{cdn_url}/")).nth(1); @@ -473,10 +457,12 @@ pub async fn user_icon_edit( WHERE (id = $2) ", format!("{}/{}", cdn_url, upload_data.file_name), - id as crate::database::models::ids::UserId, + actual_user.id as crate::database::models::ids::UserId, ) .execute(&**pool) .await?; + User::clear_caches(&[(actual_user.id, None)], &redis).await?; + Ok(HttpResponse::NoContent().body("")) } else { Ok(HttpResponse::NotFound().body("")) @@ -505,11 +491,12 @@ pub async fn user_delete( info: web::Path<(String,)>, pool: web::Data, removal_type: web::Query, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; - let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; + let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; - if let Some(id) = id_option { + 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(), @@ -518,11 +505,13 @@ pub async fn user_delete( let mut transaction = pool.begin().await?; - let result = if &*removal_type.removal_type == "full" { - User::remove_full(id, &mut transaction).await? - } else { - User::remove(id, &mut transaction).await? - }; + let result = User::remove( + id, + removal_type.removal_type == "full", + &mut transaction, + &redis, + ) + .await?; transaction.commit().await?; @@ -541,11 +530,12 @@ pub async fn user_follows( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; - let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; + let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; - if let Some(id) = id_option { + 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(), @@ -569,11 +559,12 @@ pub async fn user_follows( .try_collect::>() .await?; - let projects: Vec<_> = crate::database::Project::get_many_full(&project_ids, &**pool) - .await? - .into_iter() - .map(Project::from) - .collect(); + let projects: Vec<_> = + crate::database::Project::get_many_ids(&project_ids, &**pool, &redis) + .await? + .into_iter() + .map(Project::from) + .collect(); Ok(HttpResponse::Ok().json(projects)) } else { @@ -586,11 +577,12 @@ pub async fn user_notifications( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; - let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; + let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; - if let Some(id) = id_option { + 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(), @@ -624,11 +616,12 @@ pub async fn user_payouts( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; - let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; + let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; - if let Some(id) = id_option { + 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 payouts of this user!".to_string(), @@ -699,13 +692,14 @@ pub async fn user_payouts_request( pool: web::Data, data: web::Json, payouts_queue: web::Data>>, + redis: web::Data, ) -> Result { let mut payouts_queue = payouts_queue.lock().await; - let user = get_user_from_headers(req.headers(), &**pool).await?; - let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; + let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; - if let Some(id) = id_option { + 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 request payouts of this user!".to_string(), @@ -761,6 +755,7 @@ pub async fn user_payouts_request( ) .execute(&mut *transaction) .await?; + User::clear_caches(&[(id, None)], &redis).await?; transaction.commit().await?; diff --git a/src/routes/v2/version_creation.rs b/src/routes/v2/version_creation.rs index bed9eff50..40977b914 100644 --- a/src/routes/v2/version_creation.rs +++ b/src/routes/v2/version_creation.rs @@ -1,4 +1,5 @@ use super::project_creation::{CreateError, UploadedFile}; +use crate::auth::get_user_from_headers; use crate::database::models; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::version_item::{ @@ -12,7 +13,6 @@ use crate::models::projects::{ VersionId, VersionStatus, VersionType, }; use crate::models::teams::Permissions; -use crate::util::auth::get_user_from_headers_transaction; use crate::util::routes::read_from_field; use crate::util::validate::validation_errors_to_string; use crate::validate::{validate_file, ValidationResult}; @@ -82,6 +82,7 @@ pub async fn version_create( req: HttpRequest, mut payload: Multipart, client: Data, + redis: Data, file_host: Data>, ) -> Result { let mut transaction = client.begin().await?; @@ -91,8 +92,10 @@ pub async fn version_create( req, &mut payload, &mut transaction, + &redis, &***file_host, &mut uploaded_files, + &client, ) .await; @@ -116,8 +119,10 @@ async fn version_create_inner( req: HttpRequest, payload: &mut Multipart, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + redis: &deadpool_redis::Pool, file_host: &dyn FileHost, uploaded_files: &mut Vec, + pool: &PgPool, ) -> Result { let cdn_url = dotenvy::var("CDN_URL")?; @@ -127,7 +132,7 @@ async fn version_create_inner( let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?; let all_loaders = models::categories::Loader::list(&mut *transaction).await?; - let user = get_user_from_headers_transaction(req.headers(), &mut *transaction).await?; + let user = get_user_from_headers(req.headers(), pool, redis).await?; let mut error = None; while let Some(item) = payload.next().await { @@ -417,8 +422,7 @@ async fn version_create_inner( let project_id = builder.project_id; builder.insert(transaction).await?; - models::Project::update_game_versions(project_id, &mut *transaction).await?; - models::Project::update_loaders(project_id, &mut *transaction).await?; + models::Project::clear_cache(project_id, None, Some(true), redis).await?; Ok(HttpResponse::Ok().json(response)) } @@ -430,6 +434,7 @@ pub async fn upload_file_to_version( url_data: web::Path<(VersionId,)>, mut payload: Multipart, client: Data, + redis: web::Data, file_host: Data>, ) -> Result { let mut transaction = client.begin().await?; @@ -442,6 +447,7 @@ pub async fn upload_file_to_version( &mut payload, client, &mut transaction, + redis, &***file_host, &mut uploaded_files, version_id, @@ -470,6 +476,7 @@ async fn upload_file_to_version_inner( payload: &mut Multipart, client: Data, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + redis: Data, file_host: &dyn FileHost, uploaded_files: &mut Vec, version_id: models::VersionId, @@ -479,9 +486,9 @@ async fn upload_file_to_version_inner( let mut initial_file_data: Option = None; let mut file_builders: Vec = Vec::new(); - let user = get_user_from_headers_transaction(req.headers(), &mut *transaction).await?; + let user = get_user_from_headers(req.headers(), &**client, &redis).await?; - let result = models::Version::get_full(version_id, &**client).await?; + let result = models::Version::get(version_id, &**client, &redis).await?; let version = match result { Some(v) => v, @@ -493,8 +500,8 @@ async fn upload_file_to_version_inner( }; if !user.role.is_admin() { - let team_member = models::TeamMember::get_from_user_id_version( - version_id, + let team_member = models::TeamMember::get_from_user_id_project( + version.inner.project_id, user.id.into(), &mut *transaction, ) diff --git a/src/routes/v2/version_file.rs b/src/routes/v2/version_file.rs index 673999a5b..dabcdfaad 100644 --- a/src/routes/v2/version_file.rs +++ b/src/routes/v2/version_file.rs @@ -1,13 +1,13 @@ use super::ApiError; -use crate::database::models::{version_item::QueryVersion, DatabaseError}; +use crate::auth::{ + filter_authorized_projects, filter_authorized_versions, get_user_from_headers, + is_authorized_version, +}; use crate::models::ids::VersionId; -use crate::models::projects::{GameVersion, Loader, Project, Version}; +use crate::models::projects::VersionType; use crate::models::teams::Permissions; -use crate::util::auth::get_user_from_headers; -use crate::util::routes::ok_or_not_found; use crate::{database, models}; use actix_web::{delete, get, post, web, HttpRequest, HttpResponse}; -use futures::TryStreamExt; use itertools::Itertools; use serde::{Deserialize, Serialize}; use sqlx::PgPool; @@ -25,7 +25,6 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("version_files") .service(get_versions_from_hashes) - .service(download_files) .service(update_files), ); } @@ -34,8 +33,6 @@ pub fn config(cfg: &mut web::ServiceConfig) { pub struct HashQuery { #[serde(default = "default_algorithm")] pub algorithm: String, - #[serde(default = "default_multiple")] - pub multiple: bool, pub version_id: Option, } @@ -43,59 +40,40 @@ fn default_algorithm() -> String { "sha1".into() } -fn default_multiple() -> bool { - false -} - // under /api/v1/version_file/{hash} #[get("{version_id}")] pub async fn get_version_from_hash( + req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, hash_query: web::Query, ) -> Result { - let hash = info.into_inner().0.to_lowercase(); + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); - let result = sqlx::query!( - " - SELECT f.version_id version_id - FROM hashes h - INNER JOIN files f ON h.file_id = f.id - INNER JOIN versions v on f.version_id = v.id AND v.status != ALL($1) - INNER JOIN mods m on v.mod_id = m.id - WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ALL($4) - ORDER BY v.date_published ASC - ", - &*crate::models::projects::VersionStatus::iterator() - .filter(|x| x.is_hidden()) - .map(|x| x.to_string()) - .collect::>(), - hash.as_bytes(), - hash_query.algorithm, - &*crate::models::projects::ProjectStatus::iterator() - .filter(|x| x.is_hidden()) - .map(|x| x.to_string()) - .collect::>(), + let hash = info.into_inner().0.to_lowercase(); + let file = database::models::Version::get_file_from_hash( + hash_query.algorithm.clone(), + hash, + hash_query.version_id.map(|x| x.into()), + &**pool, + &redis, ) - .fetch_all(&**pool) .await?; - let version_ids = result - .iter() - .map(|x| database::models::VersionId(x.version_id)) - .collect::>(); - let versions_data = database::models::Version::get_many_full(&version_ids, &**pool).await?; + if let Some(file) = file { + let version = database::models::Version::get(file.version_id, &**pool, &redis).await?; - if let Some(first) = versions_data.first() { - if hash_query.multiple { - Ok(HttpResponse::Ok().json( - versions_data - .into_iter() - .map(models::projects::Version::from) - .collect::>(), - )) + if let Some(version) = version { + if !is_authorized_version(&version.inner, &user_option, &pool).await? { + return Ok(HttpResponse::NotFound().body("")); + } + + Ok(HttpResponse::Ok().json(models::projects::Version::from(version))) } else { - Ok(HttpResponse::Ok().json(models::projects::Version::from(first.clone()))) + Ok(HttpResponse::NotFound().body("")) } } else { Ok(HttpResponse::NotFound().body("")) @@ -110,42 +88,40 @@ pub struct DownloadRedirect { // under /api/v1/version_file/{hash}/download #[get("{version_id}/download")] pub async fn download_version( + req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, hash_query: web::Query, ) -> Result { - let hash = info.into_inner().0.to_lowercase(); - let mut transaction = pool.begin().await?; + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); - let result = sqlx::query!( - " - SELECT f.url url, f.id id, f.version_id version_id, v.mod_id project_id FROM hashes h - INNER JOIN files f ON h.file_id = f.id - INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1) - INNER JOIN mods m on v.mod_id = m.id - WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ALL($4) - ORDER BY v.date_published ASC - ", - &*crate::models::projects::VersionStatus::iterator() - .filter(|x| x.is_hidden()) - .map(|x| x.to_string()) - .collect::>(), - hash.as_bytes(), - hash_query.algorithm, - &*crate::models::projects::ProjectStatus::iterator() - .filter(|x| x.is_hidden()) - .map(|x| x.to_string()) - .collect::>(), + let hash = info.into_inner().0.to_lowercase(); + let file = database::models::Version::get_file_from_hash( + hash_query.algorithm.clone(), + hash, + hash_query.version_id.map(|x| x.into()), + &**pool, + &redis, ) - .fetch_optional(&mut *transaction) .await?; - if let Some(id) = result { - transaction.commit().await?; + if let Some(file) = file { + let version = database::models::Version::get(file.version_id, &**pool, &redis).await?; - Ok(HttpResponse::TemporaryRedirect() - .append_header(("Location", &*id.url)) - .json(DownloadRedirect { url: id.url })) + if let Some(version) = version { + if !is_authorized_version(&version.inner, &user_option, &pool).await? { + return Ok(HttpResponse::NotFound().body("")); + } + + Ok(HttpResponse::TemporaryRedirect() + .append_header(("Location", &*file.url)) + .json(DownloadRedirect { url: file.url })) + } else { + Ok(HttpResponse::NotFound().body("")) + } } else { Ok(HttpResponse::NotFound().body("")) } @@ -157,33 +133,26 @@ pub async fn delete_file( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, hash_query: web::Query, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let hash = info.into_inner().0.to_lowercase(); - let result = sqlx::query!( - " - SELECT f.id id, f.version_id version_id, f.filename filename, v.version_number version_number, v.mod_id project_id FROM hashes h - INNER JOIN files f ON h.file_id = f.id - INNER JOIN versions v ON v.id = f.version_id - WHERE h.algorithm = $2 AND h.hash = $1 - ORDER BY v.date_published ASC - ", - hash.as_bytes(), - hash_query.algorithm + let file = database::models::Version::get_file_from_hash( + hash_query.algorithm.clone(), + hash, + hash_query.version_id.map(|x| x.into()), + &**pool, + &redis, ) - .fetch_all(&**pool) - .await?; + .await?; - if let Some(row) = result.iter().find_or_first(|x| { - hash_query.version_id.is_none() - || Some(x.version_id) == hash_query.version_id.map(|x| x.0 as i64) - }) { + if let Some(row) = file { if !user.role.is_admin() { let team_member = database::models::TeamMember::get_from_user_id_version( - database::models::ids::VersionId(row.version_id), + row.version_id, user.id.into(), &**pool, ) @@ -205,24 +174,15 @@ pub async fn delete_file( } } - use futures::stream::TryStreamExt; + let version = database::models::Version::get(row.version_id, &**pool, &redis).await?; + if let Some(version) = version { + if version.files.len() < 2 { + return Err(ApiError::InvalidInput( + "Versions must have at least one file uploaded to them".to_string(), + )); + } - let files = sqlx::query!( - " - SELECT f.id id FROM files f - WHERE f.version_id = $1 - ", - row.version_id - ) - .fetch_many(&**pool) - .try_filter_map(|e| async { Ok(e.right().map(|_| ())) }) - .try_collect::>() - .await?; - - if files.len() < 2 { - return Err(ApiError::InvalidInput( - "Versions must have at least one file uploaded to them".to_string(), - )); + database::models::Version::clear_cache(&version, &redis).await?; } let mut transaction = pool.begin().await?; @@ -232,7 +192,7 @@ pub async fn delete_file( DELETE FROM hashes WHERE file_id = $1 ", - row.id + row.id.0 ) .execute(&mut *transaction) .await?; @@ -242,7 +202,7 @@ pub async fn delete_file( DELETE FROM files WHERE files.id = $1 ", - row.id, + row.id.0, ) .execute(&mut *transaction) .await?; @@ -257,82 +217,72 @@ pub async fn delete_file( #[derive(Deserialize)] pub struct UpdateData { - pub loaders: Vec, - pub game_versions: Vec, + pub loaders: Option>, + pub game_versions: Option>, + pub version_types: Option>, } #[post("{version_id}/update")] pub async fn get_update_from_hash( + req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, hash_query: web::Query, update_data: web::Json, ) -> Result { + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); let hash = info.into_inner().0.to_lowercase(); - // get version_id from hash - // get mod_id from hash - // get latest version satisfying conditions - if not found - - let result = sqlx::query!( - " - SELECT v.mod_id project_id FROM hashes h - INNER JOIN files f ON h.file_id = f.id - INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1) - INNER JOIN mods m on v.mod_id = m.id - WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ALL($4) - ORDER BY v.date_published ASC - ", - &*crate::models::projects::VersionStatus::iterator() - .filter(|x| x.is_hidden()) - .map(|x| x.to_string()) - .collect::>(), - hash.as_bytes(), - hash_query.algorithm, - &*crate::models::projects::ProjectStatus::iterator() - .filter(|x| x.is_hidden()) - .map(|x| x.to_string()) - .collect::>(), + if let Some(file) = database::models::Version::get_file_from_hash( + hash_query.algorithm.clone(), + hash, + hash_query.version_id.map(|x| x.into()), + &**pool, + &redis, ) - .fetch_optional(&**pool) - .await?; - - if let Some(id) = result { - let version_ids = database::models::Version::get_project_versions( - database::models::ProjectId(id.project_id), - Some( - update_data - .game_versions - .clone() + .await? + { + if let Some(project) = + database::models::Project::get_id(file.project_id, &**pool, &redis).await? + { + let mut versions = + database::models::Version::get_many(&project.versions, &**pool, &redis) + .await? .into_iter() - .map(|x| x.0) - .collect(), - ), - Some( - update_data - .loaders - .clone() - .into_iter() - .map(|x| x.0) - .collect(), - ), - None, - None, - None, - &**pool, - ) - .await?; + .filter(|x| { + let mut bool = true; - if let Some(version_id) = version_ids.first() { - let version_data = database::models::Version::get_full(*version_id, &**pool).await?; + if let Some(version_types) = &update_data.version_types { + bool &= version_types + .iter() + .any(|y| y.as_str() == x.inner.version_type); + } + if let Some(loaders) = &update_data.loaders { + bool &= x.loaders.iter().any(|y| loaders.contains(y)); + } + if let Some(game_versions) = &update_data.game_versions { + bool &= x.game_versions.iter().any(|y| game_versions.contains(y)); + } - ok_or_not_found::(version_data) - } else { - Ok(HttpResponse::NotFound().body("")) + bool + }) + .sorted_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published)) + .collect::>(); + + if let Some(first) = versions.pop() { + if !is_authorized_version(&first.inner, &user_option, &pool).await? { + return Ok(HttpResponse::NotFound().body("")); + } + + return Ok(HttpResponse::Ok().json(models::projects::Version::from(first))); + } } - } else { - Ok(HttpResponse::NotFound().body("")) } + + Ok(HttpResponse::NotFound().body("")) } // Requests above with multiple versions below @@ -345,274 +295,164 @@ pub struct FileHashes { // under /api/v2/version_files #[post("")] pub async fn get_versions_from_hashes( + req: HttpRequest, pool: web::Data, + redis: web::Data, file_data: web::Json, ) -> Result { - let hashes_parsed: Vec> = file_data - .hashes - .iter() - .map(|x| x.to_lowercase().as_bytes().to_vec()) - .collect(); + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); - let result = sqlx::query!( - " - SELECT h.hash hash, h.algorithm algorithm, f.version_id version_id FROM hashes h - INNER JOIN files f ON h.file_id = f.id - INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1) - INNER JOIN mods m on v.mod_id = m.id - WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ALL($4) - ", - &*crate::models::projects::VersionStatus::iterator() - .filter(|x| x.is_hidden()) - .map(|x| x.to_string()) - .collect::>(), - hashes_parsed.as_slice(), - file_data.algorithm, - &*crate::models::projects::ProjectStatus::iterator() - .filter(|x| x.is_hidden()) - .map(|x| x.to_string()) - .collect::>(), - ) - .fetch_all(&**pool) - .await?; - - let version_ids = result - .iter() - .map(|x| database::models::VersionId(x.version_id)) - .collect::>(); - let versions_data = database::models::Version::get_many_full(&version_ids, &**pool).await?; - - let response: Result, ApiError> = result - .into_iter() - .filter_map(|row| { - versions_data - .clone() - .into_iter() - .find(|x| x.inner.id.0 == row.version_id) - .map(|v| { - if let Ok(parsed_hash) = String::from_utf8(row.hash) { - Ok((parsed_hash, crate::models::projects::Version::from(v))) - } else { - Err(ApiError::Database(DatabaseError::Other(format!( - "Could not parse hash for version {}", - row.version_id - )))) - } - }) - }) - .collect(); - Ok(HttpResponse::Ok().json(response?)) -} - -#[post("project")] -pub async fn get_projects_from_hashes( - pool: web::Data, - file_data: web::Json, -) -> Result { - let hashes_parsed: Vec> = file_data - .hashes - .iter() - .map(|x| x.to_lowercase().as_bytes().to_vec()) - .collect(); - - let result = sqlx::query!( - " - SELECT h.hash hash, h.algorithm algorithm, m.id project_id FROM hashes h - INNER JOIN files f ON h.file_id = f.id - INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1) - INNER JOIN mods m on v.mod_id = m.id - WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ALL($4) - ", - &*crate::models::projects::VersionStatus::iterator() - .filter(|x| x.is_hidden()) - .map(|x| x.to_string()) - .collect::>(), - hashes_parsed.as_slice(), - file_data.algorithm, - &*crate::models::projects::ProjectStatus::iterator() - .filter(|x| x.is_hidden()) - .map(|x| x.to_string()) - .collect::>(), - ) - .fetch_all(&**pool) - .await?; - - let project_ids = result - .iter() - .map(|x| database::models::ProjectId(x.project_id)) - .collect::>(); - let versions_data = database::models::Project::get_many_full(&project_ids, &**pool).await?; - - let response: Result, ApiError> = result - .into_iter() - .filter_map(|row| { - versions_data - .clone() - .into_iter() - .find(|x| x.inner.id.0 == row.project_id) - .map(|v| { - if let Ok(parsed_hash) = String::from_utf8(row.hash) { - Ok((parsed_hash, crate::models::projects::Project::from(v))) - } else { - Err(ApiError::Database(DatabaseError::Other(format!( - "Could not parse hash for version {}", - row.project_id - )))) - } - }) - }) - .collect(); - Ok(HttpResponse::Ok().json(response?)) -} - -#[post("download")] -pub async fn download_files( - pool: web::Data, - file_data: web::Json, -) -> Result { - let hashes_parsed: Vec> = file_data - .hashes - .iter() - .map(|x| x.to_lowercase().as_bytes().to_vec()) - .collect(); - - let mut transaction = pool.begin().await?; - - let result = sqlx::query!( - " - SELECT f.url url, h.hash hash, h.algorithm algorithm, f.version_id version_id, v.mod_id project_id FROM hashes h - INNER JOIN files f ON h.file_id = f.id - INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1) - INNER JOIN mods m on v.mod_id = m.id - WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ALL($4) - ", - &*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::>(), - hashes_parsed.as_slice(), - file_data.algorithm, - &*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::>(), - ) - .fetch_all(&mut *transaction) - .await?; - - let response = result - .into_iter() - .map(|row| { - if let Ok(parsed_hash) = String::from_utf8(row.hash) { - Ok((parsed_hash, row.url)) - } else { - Err(ApiError::Database(DatabaseError::Other(format!( - "Could not parse hash for version {}", - row.version_id - )))) - } - }) - .collect::, ApiError>>(); - - Ok(HttpResponse::Ok().json(response?)) -} - -#[derive(Deserialize)] -pub struct ManyUpdateData { - pub algorithm: String, - pub hashes: Vec, - pub loaders: Vec, - pub game_versions: Vec, -} - -#[post("update")] -pub async fn update_files( - pool: web::Data, - update_data: web::Json, -) -> Result { - let hashes_parsed: Vec> = update_data - .hashes - .iter() - .map(|x| x.to_lowercase().as_bytes().to_vec()) - .collect(); - - let mut transaction = pool.begin().await?; - - let result = sqlx::query!( - " - SELECT h.hash, v.mod_id FROM hashes h - INNER JOIN files f ON h.file_id = f.id - INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1) - INNER JOIN mods m on v.mod_id = m.id - WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ALL($4) - ", - &*crate::models::projects::VersionStatus::iterator() - .filter(|x| x.is_hidden()) - .map(|x| x.to_string()) - .collect::>(), - hashes_parsed.as_slice(), - update_data.algorithm, - &*crate::models::projects::ProjectStatus::iterator() - .filter(|x| x.is_hidden()) - .map(|x| x.to_string()) - .collect::>(), - ) - .fetch_many(&mut *transaction) - .try_filter_map(|e| async { - Ok(e.right() - .map(|m| (m.hash, database::models::ids::ProjectId(m.mod_id)))) - }) - .try_collect::>() - .await?; - - let mut version_ids: HashMap> = HashMap::new(); - - let updated_versions = database::models::Version::get_projects_versions( - result - .iter() - .map(|x| x.1) - .collect::>() - .clone(), - Some( - update_data - .game_versions - .clone() - .iter() - .map(|x| x.0.clone()) - .collect(), - ), - Some( - update_data - .loaders - .clone() - .iter() - .map(|x| x.0.clone()) - .collect(), - ), - None, - None, - None, + let files = database::models::Version::get_files_from_hash( + file_data.algorithm.clone(), + &file_data.hashes, &**pool, + &redis, ) .await?; - for (hash, id) in result { - if let Some(latest_version) = updated_versions.get(&id).and_then(|x| x.last()) { - version_ids.insert(*latest_version, hash); - } - } - - let query_version_ids = version_ids.keys().copied().collect::>(); - let versions = database::models::Version::get_many_full(&query_version_ids, &**pool).await?; + let version_ids = files.iter().map(|x| x.version_id).collect::>(); + let versions_data = filter_authorized_versions( + database::models::Version::get_many(&version_ids, &**pool, &redis).await?, + &user_option, + &pool, + ) + .await?; let mut response = HashMap::new(); - for version in versions { - let hash = version_ids.get(&version.inner.id); - - if let Some(hash) = hash { - if let Ok(parsed_hash) = String::from_utf8(hash.clone()) { - response.insert(parsed_hash, models::projects::Version::from(version)); - } else { - let version_id: VersionId = version.inner.id.into(); - - return Err(ApiError::Database(DatabaseError::Other(format!( - "Could not parse hash for version {version_id}" - )))); + for version in versions_data { + for file in files.iter().filter(|x| x.version_id == version.id.into()) { + if let Some(hash) = file.hashes.get(&file_data.algorithm) { + response.insert(hash.clone(), version.clone()); + } + } + } + + Ok(HttpResponse::Ok().json(response)) +} + +#[post("project")] +pub async fn get_projects_from_hashes( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + file_data: web::Json, +) -> Result { + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); + + let files = database::models::Version::get_files_from_hash( + file_data.algorithm.clone(), + &file_data.hashes, + &**pool, + &redis, + ) + .await?; + + let project_ids = files.iter().map(|x| x.project_id).collect::>(); + + let projects_data = filter_authorized_projects( + database::models::Project::get_many_ids(&project_ids, &**pool, &redis).await?, + &user_option, + &pool, + ) + .await?; + + let mut response = HashMap::new(); + + for project in projects_data { + for file in files.iter().filter(|x| x.project_id == project.id.into()) { + if let Some(hash) = file.hashes.get(&file_data.algorithm) { + response.insert(hash.clone(), project.clone()); + } + } + } + + Ok(HttpResponse::Ok().json(response)) +} + +#[derive(Deserialize)] +pub struct ManyUpdateData { + pub algorithm: String, + pub hashes: Vec, + pub loaders: Option>, + pub game_versions: Option>, + pub version_types: Option>, +} + +#[post("update")] +pub async fn update_files( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + update_data: web::Json, +) -> Result { + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); + + let files = database::models::Version::get_files_from_hash( + update_data.algorithm.clone(), + &update_data.hashes, + &**pool, + &redis, + ) + .await?; + + let projects = database::models::Project::get_many_ids( + &files.iter().map(|x| x.project_id).collect::>(), + &**pool, + &redis, + ) + .await?; + let all_versions = database::models::Version::get_many( + &projects + .iter() + .flat_map(|x| x.versions.clone()) + .collect::>(), + &**pool, + &redis, + ) + .await?; + + let mut response = HashMap::new(); + + for project in projects { + for file in files.iter().filter(|x| x.project_id == project.inner.id) { + let version = all_versions + .iter() + .filter(|x| { + let mut bool = true; + + if let Some(version_types) = &update_data.version_types { + bool &= version_types + .iter() + .any(|y| y.as_str() == x.inner.version_type); + } + if let Some(loaders) = &update_data.loaders { + bool &= x.loaders.iter().any(|y| loaders.contains(y)); + } + if let Some(game_versions) = &update_data.game_versions { + bool &= x.game_versions.iter().any(|y| game_versions.contains(y)); + } + + bool + }) + .sorted_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published)) + .next(); + + if let Some(version) = version { + if is_authorized_version(&version.inner, &user_option, &pool).await? { + if let Some(hash) = file.hashes.get(&update_data.algorithm) { + response.insert( + hash.clone(), + models::projects::Version::from(version.clone()), + ); + } + } } } } diff --git a/src/routes/v2/versions.rs b/src/routes/v2/versions.rs index 5492f2eea..7758ec9dd 100644 --- a/src/routes/v2/versions.rs +++ b/src/routes/v2/versions.rs @@ -1,11 +1,11 @@ use super::ApiError; +use crate::auth::{ + filter_authorized_versions, get_user_from_headers, is_authorized, is_authorized_version, +}; use crate::database; use crate::models; use crate::models::projects::{Dependency, FileType, VersionStatus, VersionType}; use crate::models::teams::Permissions; -use crate::util::auth::{ - filter_authorized_versions, get_user_from_headers, is_authorized, is_authorized_version, -}; use crate::util::validate::validation_errors_to_string; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use chrono::{DateTime, Utc}; @@ -33,8 +33,8 @@ pub struct VersionListFilters { pub loaders: Option, pub featured: Option, pub version_type: Option, - pub limit: Option, - pub offset: Option, + pub limit: Option, + pub offset: Option, } #[get("version")] @@ -43,38 +43,50 @@ pub async fn version_list( info: web::Path<(String,)>, web::Query(filters): web::Query, pool: web::Data, + redis: web::Data, ) -> Result { let string = info.into_inner().0; - let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?; + let result = database::models::Project::get(&string, &**pool, &redis).await?; - let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); if let Some(project) = result { - if !is_authorized(&project, &user_option, &pool).await? { + if !is_authorized(&project.inner, &user_option, &pool).await? { return Ok(HttpResponse::NotFound().body("")); } - let id = project.id; + let version_filters = filters + .game_versions + .as_ref() + .map(|x| serde_json::from_str::>(x).unwrap_or_default()); + let loader_filters = filters + .loaders + .as_ref() + .map(|x| serde_json::from_str::>(x).unwrap_or_default()); + let mut versions = database::models::Version::get_many(&project.versions, &**pool, &redis) + .await? + .into_iter() + .skip(filters.offset.unwrap_or(0)) + .take(filters.limit.unwrap_or(usize::MAX)) + .filter(|x| { + let mut bool = true; - let version_ids = database::models::Version::get_project_versions( - id, - filters - .game_versions - .as_ref() - .map(|x| serde_json::from_str(x).unwrap_or_default()), - filters - .loaders - .as_ref() - .map(|x| serde_json::from_str(x).unwrap_or_default()), - filters.version_type, - filters.limit, - filters.offset, - &**pool, - ) - .await?; + if let Some(version_type) = filters.version_type { + bool &= &*x.inner.version_type == version_type.as_str(); + } + if let Some(loaders) = &loader_filters { + bool &= x.loaders.iter().any(|y| loaders.contains(y)); + } + if let Some(game_versions) = &version_filters { + bool &= x.game_versions.iter().any(|y| game_versions.contains(y)); + } - let mut versions = database::models::Version::get_many_full(&version_ids, &**pool).await?; + bool + }) + .collect::>(); let mut response = versions .iter() @@ -139,12 +151,15 @@ pub async fn version_project_get( req: HttpRequest, info: web::Path<(String, String)>, pool: web::Data, + redis: web::Data, ) -> Result { let id = info.into_inner(); let version_data = - database::models::Version::get_full_from_id_slug(&id.0, &id.1, &**pool).await?; + database::models::Version::get_full_from_id_slug(&id.0, &id.1, &**pool, &redis).await?; - let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); if let Some(data) = version_data { if is_authorized_version(&data.inner, &user_option, &pool).await? { @@ -165,14 +180,17 @@ pub async fn versions_get( req: HttpRequest, web::Query(ids): web::Query, pool: web::Data, + redis: web::Data, ) -> Result { let version_ids = serde_json::from_str::>(&ids.ids)? .into_iter() .map(|x| x.into()) .collect::>(); - let versions_data = database::models::Version::get_many_full(&version_ids, &**pool).await?; + let versions_data = database::models::Version::get_many(&version_ids, &**pool, &redis).await?; - let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); let versions = filter_authorized_versions(versions_data, &user_option, &pool).await?; @@ -184,11 +202,14 @@ pub async fn version_get( req: HttpRequest, info: web::Path<(models::ids::VersionId,)>, pool: web::Data, + redis: web::Data, ) -> Result { let id = info.into_inner().0; - let version_data = database::models::Version::get_full(id.into(), &**pool).await?; + let version_data = database::models::Version::get(id.into(), &**pool, &redis).await?; - let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); + let user_option = get_user_from_headers(req.headers(), &**pool, &redis) + .await + .ok(); if let Some(data) = version_data { if is_authorized_version(&data.inner, &user_option, &pool).await? { @@ -240,9 +261,10 @@ pub async fn version_edit( req: HttpRequest, info: web::Path<(models::ids::VersionId,)>, pool: web::Data, + redis: web::Data, new_version: web::Json, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; new_version .validate() @@ -251,14 +273,15 @@ pub async fn version_edit( let version_id = info.into_inner().0; let id = version_id.into(); - let result = database::models::Version::get_full(id, &**pool).await?; + let result = database::models::Version::get(id, &**pool, &redis).await?; if let Some(version_item) = result { let project_item = - database::models::Project::get_full(version_item.inner.project_id, &**pool).await?; + database::models::Project::get_id(version_item.inner.project_id, &**pool, &redis) + .await?; - let team_member = database::models::TeamMember::get_from_user_id_version( - version_item.inner.id, + let team_member = database::models::TeamMember::get_from_user_id_project( + version_item.inner.project_id, user.id.into(), &**pool, ) @@ -390,12 +413,6 @@ pub async fn version_edit( .execute(&mut *transaction) .await?; } - - database::models::Project::update_game_versions( - version_item.inner.project_id, - &mut transaction, - ) - .await?; } if let Some(loaders) = &new_version.loaders { @@ -429,12 +446,6 @@ pub async fn version_edit( .execute(&mut *transaction) .await?; } - - database::models::Project::update_loaders( - version_item.inner.project_id, - &mut transaction, - ) - .await?; } if let Some(featured) = &new_version.featured { @@ -595,6 +606,14 @@ pub async fn version_edit( } } + database::models::Version::clear_cache(&version_item, &redis).await?; + database::models::Project::clear_cache( + version_item.inner.project_id, + None, + Some(true), + &redis, + ) + .await?; transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { @@ -618,9 +637,10 @@ pub async fn version_schedule( req: HttpRequest, info: web::Path<(models::ids::VersionId,)>, pool: web::Data, + redis: web::Data, scheduling_data: web::Json, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; if scheduling_data.time < Utc::now() { return Err(ApiError::InvalidInput( @@ -635,11 +655,11 @@ pub async fn version_schedule( } let string = info.into_inner().0; - let result = database::models::Version::get_full(string.into(), &**pool).await?; + let result = database::models::Version::get(string.into(), &**pool, &redis).await?; if let Some(version_item) = result { - let team_member = database::models::TeamMember::get_from_user_id_version( - version_item.inner.id, + let team_member = database::models::TeamMember::get_from_user_id_project( + version_item.inner.project_id, user.id.into(), &**pool, ) @@ -655,6 +675,7 @@ pub async fn version_schedule( )); } + let mut transaction = pool.begin().await?; sqlx::query!( " UPDATE versions @@ -665,9 +686,12 @@ pub async fn version_schedule( scheduling_data.time, version_item.inner.id as database::models::ids::VersionId, ) - .execute(&**pool) + .execute(&mut *transaction) .await?; + database::models::Version::clear_cache(&version_item, &redis).await?; + transaction.commit().await?; + Ok(HttpResponse::NoContent().body("")) } else { Ok(HttpResponse::NotFound().body("")) @@ -679,13 +703,20 @@ pub async fn version_delete( req: HttpRequest, info: web::Path<(models::ids::VersionId,)>, pool: web::Data, + redis: web::Data, ) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool, &redis).await?; let id = info.into_inner().0; + let version = database::models::Version::get(id.into(), &**pool, &redis) + .await? + .ok_or_else(|| { + ApiError::InvalidInput("The specified version does not exist!".to_string()) + })?; + if !user.role.is_admin() { - let team_member = database::models::TeamMember::get_from_user_id_version( - id.into(), + let team_member = database::models::TeamMember::get_from_user_id_project( + version.inner.project_id, user.id.into(), &**pool, ) @@ -709,7 +740,11 @@ pub async fn version_delete( let mut transaction = pool.begin().await?; - let result = database::models::Version::remove_full(id.into(), &mut transaction).await?; + let result = + database::models::Version::remove_full(version.inner.id, &redis, &mut transaction).await?; + + database::models::Project::clear_cache(version.inner.project_id, None, Some(true), &redis) + .await?; transaction.commit().await?; diff --git a/src/util/auth.rs b/src/util/auth.rs deleted file mode 100644 index cce6a7a02..000000000 --- a/src/util/auth.rs +++ /dev/null @@ -1,588 +0,0 @@ -use crate::database; -use crate::database::models::project_item::QueryProject; -use crate::database::models::user_item; -use crate::database::models::version_item::QueryVersion; -use crate::database::{models, Project, Version}; -use crate::models::users::{Badges, Role, User, UserId, UserPayoutData}; -use crate::routes::ApiError; -use crate::Utc; -use actix_web::http::header::HeaderMap; -use actix_web::http::header::COOKIE; -use actix_web::web; -use reqwest::header::{HeaderValue, AUTHORIZATION}; -use rust_decimal::Decimal; -use serde::{Deserialize, Serialize}; -use serde_with::serde_as; -use serde_with::DisplayFromStr; -use sqlx::PgPool; -use thiserror::Error; - -use super::pat::get_user_from_pat; - -#[derive(Error, Debug)] -pub enum AuthenticationError { - #[error("An unknown database error occurred")] - Sqlx(#[from] sqlx::Error), - #[error("Database Error: {0}")] - Database(#[from] models::DatabaseError), - #[error("Error while parsing JSON: {0}")] - SerDe(#[from] serde_json::Error), - #[error("Error while communicating over the internet: {0}")] - Reqwest(#[from] reqwest::Error), - #[error("Error while decoding PAT: {0}")] - Decoding(#[from] crate::models::ids::DecodingError), - #[error("Invalid Authentication Credentials")] - InvalidCredentials, - #[error("Authentication method was not valid")] - InvalidAuthMethod, - #[error("GitHub Token from incorrect Client ID")] - InvalidClientId, -} - -// A user as stored in the Minos database -#[derive(Serialize, Deserialize, Debug)] -pub struct MinosUser { - pub id: String, // This is the unique generated Ory name - pub username: String, // unique username - pub email: String, - pub name: Option, // real name - pub github_id: Option, - pub discord_id: Option, - pub google_id: Option, - pub gitlab_id: Option, - pub microsoft_id: Option, - pub apple_id: Option, -} - -// A payload marking a new user in Minos, with data to be inserted into Labrinth -#[serde_as] -#[derive(Deserialize, Debug)] -pub struct MinosNewUser { - pub id: String, // This is the unique generated Ory name - pub username: String, // unique username - pub email: String, - - pub name: Option, // real name - pub default_picture: Option, // uri of default avatar - #[serde_as(as = "Option")] - pub github_id: Option, // we allow Github to be submitted to connect to an existing account -} - -// Attempt to append a Minos user to an existing user, if one exists -// (combining the the legacy user with the Minos user) -pub async fn link_or_insert_new_user( - transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - minos_new_user: MinosNewUser, -) -> Result<(), AuthenticationError> { - // If the user with this Github ID already exists, we can just merge the two accounts - if let Some(github_id) = minos_new_user.github_id { - if let Some(existing_user) = - user_item::User::get_from_github_id(github_id as u64, &mut *transaction).await? - { - existing_user - .merge_minos_user(&minos_new_user.id, &mut *transaction) - .await?; - return Ok(()); - } - } - // No user exists, so we need to create a new user - insert_new_user(transaction, minos_new_user).await?; - - Ok(()) -} - -// Insert a new user into the database from a MinosUser -pub async fn insert_new_user( - transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - minos_new_user: MinosNewUser, -) -> Result<(), AuthenticationError> { - let user_id = crate::database::models::generate_user_id(transaction).await?; - - database::models::User { - id: user_id, - kratos_id: Some(minos_new_user.id), - username: minos_new_user.username, - name: minos_new_user.name, - email: Some(minos_new_user.email), - avatar_url: minos_new_user.default_picture, - bio: None, - github_id: minos_new_user.github_id, - created: Utc::now(), - role: Role::Developer.to_string(), - badges: Badges::default(), - balance: Decimal::ZERO, - payout_wallet: None, - payout_wallet_type: None, - payout_address: None, - } - .insert(transaction) - .await?; - - Ok(()) -} - -// Gets MinosUser from Kratos ID -// This uses an administrative bearer token to access the Minos API -// Should NOT be directly accessible to users -pub async fn get_minos_user(kratos_id: &str) -> Result { - let ory_auth_bearer = dotenvy::var("ORY_AUTH_BEARER").unwrap(); - let req = reqwest::Client::new() - .get(format!( - "{}/admin/user/{kratos_id}", - dotenvy::var("MINOS_URL").unwrap() - )) - .header(reqwest::header::USER_AGENT, "Labrinth") - .header( - reqwest::header::AUTHORIZATION, - format!("Bearer {ory_auth_bearer}"), - ); - let res = req.send().await?.error_for_status()?; - let res = res.json().await?; - Ok(res) -} - -// pass the cookies to Minos to get the user. -pub async fn get_minos_user_from_cookies(cookies: &str) -> Result { - let req = reqwest::Client::new() - .get(dotenvy::var("MINOS_URL").unwrap() + "/user") - .header(reqwest::header::USER_AGENT, "Modrinth") - .header(reqwest::header::COOKIE, cookies); - let res = req.send().await?; - - let res = match res.status() { - reqwest::StatusCode::OK => res, - reqwest::StatusCode::UNAUTHORIZED => return Err(AuthenticationError::InvalidCredentials), - _ => res.error_for_status()?, - }; - Ok(res.json().await?) -} - -pub async fn get_user_from_headers<'a, E>( - headers: &HeaderMap, - executor: E, -) -> Result -where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, -{ - let token: Option<&reqwest::header::HeaderValue> = headers.get(AUTHORIZATION); - let cookies_unparsed: Option<&reqwest::header::HeaderValue> = headers.get(COOKIE); - - // Fetch DB user record and minos user from headers - let (db_user, minos_user) = match (token, cookies_unparsed) { - // If both, favour the bearer token first- redirect to cookie on failure - (Some(token), Some(cookies)) => { - match get_db_and_minos_user_from_bearer_token(token, executor).await { - Ok((db, minos)) => (db, minos), - Err(_) => get_db_and_minos_user_from_cookies(cookies, executor).await?, - } - } - (Some(token), _) => get_db_and_minos_user_from_bearer_token(token, executor).await?, - (_, Some(cookies)) => get_db_and_minos_user_from_cookies(cookies, executor).await?, - _ => return Err(AuthenticationError::InvalidAuthMethod), // No credentials passed - }; - - let user = User { - id: UserId::from(db_user.id), - kratos_id: db_user.kratos_id, - github_id: minos_user.github_id, - discord_id: minos_user.discord_id, - google_id: minos_user.google_id, - microsoft_id: minos_user.microsoft_id, - apple_id: minos_user.apple_id, - gitlab_id: minos_user.gitlab_id, - username: db_user.username, - name: db_user.name, - email: db_user.email, - avatar_url: db_user.avatar_url, - bio: db_user.bio, - created: db_user.created, - role: Role::from_string(&db_user.role), - badges: db_user.badges, - payout_data: Some(UserPayoutData { - balance: db_user.balance, - payout_wallet: db_user.payout_wallet, - payout_wallet_type: db_user.payout_wallet_type, - payout_address: db_user.payout_address, - }), - }; - Ok(user) -} - -pub async fn get_user_from_headers_transaction( - headers: &HeaderMap, - transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, -) -> Result { - let token: Option<&reqwest::header::HeaderValue> = headers.get(AUTHORIZATION); - let cookies_unparsed: Option<&reqwest::header::HeaderValue> = headers.get(COOKIE); - - // Fetch DB user record and minos user from headers - let (db_user, minos_user) = match (token, cookies_unparsed) { - // If both, favour the bearer token first- redirect to cookie on failure - (Some(token), Some(cookies)) => { - match get_db_and_minos_user_from_bearer_token(token, &mut *transaction).await { - Ok((db, minos)) => (db, minos), - Err(_) => get_db_and_minos_user_from_cookies(cookies, &mut *transaction).await?, - } - } - (Some(token), _) => { - get_db_and_minos_user_from_bearer_token(token, &mut *transaction).await? - } - (_, Some(cookies)) => { - get_db_and_minos_user_from_cookies(cookies, &mut *transaction).await? - } - _ => return Err(AuthenticationError::InvalidAuthMethod), // No credentials passed - }; - - let user = User { - id: UserId::from(db_user.id), - kratos_id: db_user.kratos_id, - github_id: minos_user.github_id, - discord_id: minos_user.discord_id, - google_id: minos_user.google_id, - microsoft_id: minos_user.microsoft_id, - apple_id: minos_user.apple_id, - gitlab_id: minos_user.gitlab_id, - username: db_user.username, - name: db_user.name, - email: db_user.email, - avatar_url: db_user.avatar_url, - bio: db_user.bio, - created: db_user.created, - role: Role::from_string(&db_user.role), - badges: db_user.badges, - payout_data: Some(UserPayoutData { - balance: db_user.balance, - payout_wallet: db_user.payout_wallet, - payout_wallet_type: db_user.payout_wallet_type, - payout_address: db_user.payout_address, - }), - }; - Ok(user) -} - -pub async fn get_db_and_minos_user_from_bearer_token<'a, E>( - token: &HeaderValue, - executor: E, -) -> Result<(user_item::User, MinosUser), AuthenticationError> -where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, -{ - let db_user = get_user_record_from_bearer_token( - token - .to_str() - .map_err(|_| AuthenticationError::InvalidCredentials)?, - executor, - ) - .await? - .ok_or_else(|| AuthenticationError::InvalidCredentials)?; - let minos_user = get_minos_user( - &db_user - .kratos_id - .clone() - .ok_or_else(|| AuthenticationError::InvalidCredentials)?, - ) - .await?; - Ok((db_user, minos_user)) -} - -pub async fn get_db_and_minos_user_from_cookies<'a, E>( - cookies: &HeaderValue, - executor: E, -) -> Result<(user_item::User, MinosUser), AuthenticationError> -where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, -{ - let minos_user = get_minos_user_from_cookies( - cookies - .to_str() - .map_err(|_| AuthenticationError::InvalidCredentials)?, - ) - .await?; - let db_user = models::User::get_from_minos_kratos_id(minos_user.id.clone(), executor) - .await? - .ok_or_else(|| AuthenticationError::InvalidCredentials)?; - Ok((db_user, minos_user)) -} - -pub async fn get_user_record_from_bearer_token<'a, 'b, E>( - token: &str, - executor: E, -) -> Result, AuthenticationError> -where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, -{ - if token.starts_with("Bearer ") { - let token: &str = token.trim_start_matches("Bearer "); - - // Tokens beginning with Ory are considered to be Kratos tokens (in reality, extracted cookies) and can be forwarded to Minos - let possible_user = match token.split_once('_') { - Some(("modrinth", _)) => get_user_from_pat(token, executor).await?, - Some(("ory", _)) => get_user_from_minos_session_token(token, executor).await?, - Some(("github", _)) | Some(("gho", _)) | Some(("ghp", _)) => { - get_user_from_github_token(token, executor).await? - } - _ => return Err(AuthenticationError::InvalidAuthMethod), - }; - Ok(possible_user) - } else { - Err(AuthenticationError::InvalidAuthMethod) - } -} - -pub async fn get_user_from_minos_session_token<'a, 'b, E>( - token: &str, - executor: E, -) -> Result, AuthenticationError> -where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, -{ - let ory_auth_bearer = dotenvy::var("ORY_AUTH_BEARER").unwrap(); - let req = reqwest::Client::new() - .get(dotenvy::var("MINOS_URL").unwrap() + "/admin/user/token?token=" + token) - .header(reqwest::header::USER_AGENT, "Labrinth") - .header( - reqwest::header::AUTHORIZATION, - format!("Bearer {ory_auth_bearer}"), - ); - let res = req.send().await?.error_for_status()?; - let minos_user: MinosUser = res.json().await?; - let db_user = models::User::get_from_minos_kratos_id(minos_user.id.clone(), executor).await?; - Ok(db_user) -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct GitHubUser { - pub id: u64, -} -// Get a database user from a GitHub PAT -pub async fn get_user_from_github_token<'a, E>( - access_token: &str, - executor: E, -) -> Result, AuthenticationError> -where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, -{ - let response = reqwest::Client::new() - .get("https://api.github.com/user") - .header(reqwest::header::USER_AGENT, "Modrinth") - .header(AUTHORIZATION, format!("token {access_token}")) - .send() - .await?; - - if access_token.starts_with("gho_") { - let client_id = response - .headers() - .get("x-oauth-client-id") - .and_then(|x| x.to_str().ok()); - - if client_id != Some(&*dotenvy::var("GITHUB_CLIENT_ID").unwrap()) { - return Err(AuthenticationError::InvalidClientId); - } - } - - let github_user: GitHubUser = response.json().await?; - - Ok(user_item::User::get_from_github_id(github_user.id, executor).await?) -} - -pub async fn check_is_moderator_from_headers<'a, 'b, E>( - headers: &HeaderMap, - executor: E, -) -> Result -where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, -{ - let user = get_user_from_headers(headers, executor).await?; - - if user.role.is_mod() { - Ok(user) - } else { - Err(AuthenticationError::InvalidCredentials) - } -} - -pub async fn is_authorized( - project_data: &Project, - user_option: &Option, - pool: &web::Data, -) -> Result { - let mut authorized = !project_data.status.is_hidden(); - - if let Some(user) = &user_option { - if !authorized { - if user.role.is_mod() { - authorized = true; - } else { - let user_id: models::ids::UserId = user.id.into(); - - let project_exists = sqlx::query!( - "SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)", - project_data.team_id as database::models::ids::TeamId, - user_id as database::models::ids::UserId, - ) - .fetch_one(&***pool) - .await? - .exists; - - authorized = project_exists.unwrap_or(false); - } - } - } - - Ok(authorized) -} - -pub async fn filter_authorized_projects( - projects: Vec, - user_option: &Option, - pool: &web::Data, -) -> Result, ApiError> { - let mut return_projects = Vec::new(); - let mut check_projects = Vec::new(); - - for project in projects { - if !project.inner.status.is_hidden() - || user_option - .as_ref() - .map(|x| x.role.is_mod()) - .unwrap_or(false) - { - return_projects.push(project.into()); - } else if user_option.is_some() { - check_projects.push(project); - } - } - - if !check_projects.is_empty() { - if let Some(user) = user_option { - let user_id: models::ids::UserId = user.id.into(); - - use futures::TryStreamExt; - - sqlx::query!( - " - SELECT m.id id, m.team_id team_id FROM team_members tm - INNER JOIN mods m ON m.team_id = tm.team_id - WHERE tm.team_id = ANY($1) AND tm.user_id = $2 - ", - &check_projects - .iter() - .map(|x| x.inner.team_id.0) - .collect::>(), - user_id as database::models::ids::UserId, - ) - .fetch_many(&***pool) - .try_for_each(|e| { - if let Some(row) = e.right() { - check_projects.retain(|x| { - let bool = x.inner.id.0 == row.id && x.inner.team_id.0 == row.team_id; - - if bool { - return_projects.push(x.clone().into()); - } - - !bool - }); - } - - futures::future::ready(Ok(())) - }) - .await?; - } - } - - Ok(return_projects) -} - -pub async fn is_authorized_version( - version_data: &Version, - user_option: &Option, - pool: &web::Data, -) -> Result { - let mut authorized = !version_data.status.is_hidden(); - - if let Some(user) = &user_option { - if !authorized { - if user.role.is_mod() { - authorized = true; - } else { - let user_id: models::ids::UserId = user.id.into(); - - let version_exists = sqlx::query!( - "SELECT EXISTS(SELECT 1 FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 WHERE m.id = $1)", - version_data.project_id as database::models::ids::ProjectId, - user_id as database::models::ids::UserId, - ) - .fetch_one(&***pool) - .await? - .exists; - - authorized = version_exists.unwrap_or(false); - } - } - } - - Ok(authorized) -} - -pub async fn filter_authorized_versions( - versions: Vec, - user_option: &Option, - pool: &web::Data, -) -> Result, ApiError> { - let mut return_versions = Vec::new(); - let mut check_versions = Vec::new(); - - for version in versions { - if !version.inner.status.is_hidden() - || user_option - .as_ref() - .map(|x| x.role.is_mod()) - .unwrap_or(false) - { - return_versions.push(version.into()); - } else if user_option.is_some() { - check_versions.push(version); - } - } - - if !check_versions.is_empty() { - if let Some(user) = user_option { - let user_id: models::ids::UserId = user.id.into(); - - use futures::TryStreamExt; - - sqlx::query!( - " - SELECT m.id FROM mods m - INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 - WHERE m.id = ANY($1) - ", - &check_versions - .iter() - .map(|x| x.inner.project_id.0) - .collect::>(), - user_id as database::models::ids::UserId, - ) - .fetch_many(&***pool) - .try_for_each(|e| { - if let Some(row) = e.right() { - check_versions.retain(|x| { - let bool = x.inner.project_id.0 == row.id; - - if bool { - return_versions.push(x.clone().into()); - } - - !bool - }); - } - - futures::future::ready(Ok(())) - }) - .await?; - } - } - - Ok(return_versions) -} diff --git a/src/util/ext.rs b/src/util/ext.rs index fec68eef8..1f2e9fd38 100644 --- a/src/util/ext.rs +++ b/src/util/ext.rs @@ -9,6 +9,17 @@ pub fn get_image_content_type(extension: &str) -> Option<&'static str> { } } +pub fn get_image_ext(content_type: &str) -> Option<&'static str> { + match content_type { + "image/bmp" => Some("bmp"), + "image/gif" => Some("gif"), + "image/jpeg" => Some("jpg"), + "image/png" => Some("png"), + "image/webp" => Some("webp"), + _ => None, + } +} + pub fn project_file_type(ext: &str) -> Option<&str> { match ext { "jar" => Some("application/java-archive"), diff --git a/src/util/mod.rs b/src/util/mod.rs index 15ad2479d..e0151ed11 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,9 +1,7 @@ -pub mod auth; pub mod env; pub mod ext; pub mod guards; pub mod img; -pub mod pat; pub mod routes; pub mod validate; pub mod webhook; diff --git a/src/util/routes.rs b/src/util/routes.rs index 1cda10e04..79512f720 100644 --- a/src/util/routes.rs +++ b/src/util/routes.rs @@ -2,10 +2,8 @@ use crate::routes::v2::project_creation::CreateError; use crate::routes::ApiError; use actix_multipart::Field; use actix_web::web::Payload; -use actix_web::HttpResponse; use bytes::BytesMut; use futures::StreamExt; -use serde::Serialize; pub async fn read_from_payload( payload: &mut Payload, @@ -40,14 +38,3 @@ pub async fn read_from_field( } Ok(bytes) } - -pub(crate) fn ok_or_not_found(version_data: Option) -> Result -where - U: From + Serialize, -{ - if let Some(data) = version_data { - Ok(HttpResponse::Ok().json(U::from(data))) - } else { - Ok(HttpResponse::NotFound().body("")) - } -}