diff --git a/.env b/.env index 8d033da9..dcf0b518 100644 --- a/.env +++ b/.env @@ -15,6 +15,9 @@ 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 @@ -50,9 +53,6 @@ ALLOWED_CALLBACK_URLS='["localhost", ".modrinth.com"]' 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 diff --git a/Cargo.lock b/Cargo.lock index fb708f22..6a50ebd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand", + "rand 0.8.5", "sha1 0.10.5", "smallvec", "tokio", @@ -104,7 +104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" dependencies = [ "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -142,7 +142,7 @@ dependencies = [ "parse-size", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -258,7 +258,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -269,7 +269,7 @@ checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -294,18 +294,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" dependencies = [ "cfg-if 1.0.0", - "cipher", + "cipher 0.3.0", "cpufeatures", "opaque-debug", ] +[[package]] +name = "aes" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +dependencies = [ + "cfg-if 1.0.0", + "cipher 0.4.4", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom", + "getrandom 0.2.8", "once_cell", "version_check", ] @@ -317,7 +328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if 1.0.0", - "getrandom", + "getrandom 0.2.8", "once_cell", "version_check", ] @@ -398,7 +409,7 @@ checksum = "b84f9ebcc6c1f5b8cb160f6990096a5c127f423fcb6e1ccc46c370cbdfb75dfc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -472,6 +483,18 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" @@ -505,6 +528,12 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" +[[package]] +name = "bitfield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" + [[package]] name = "bitflags" version = "1.3.2" @@ -529,6 +558,25 @@ dependencies = [ "generic-array 0.14.6", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array 0.14.6", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher 0.4.4", +] + [[package]] name = "borsh" version = "0.10.2" @@ -549,7 +597,7 @@ dependencies = [ "borsh-schema-derive-internal", "proc-macro-crate", "proc-macro2", - "syn", + "syn 1.0.109", ] [[package]] @@ -560,7 +608,7 @@ checksum = "186b734fa1c9f6743e90c95d7233c9faab6360d1a96d4ffa19d9cfd1e9350f8a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -571,7 +619,7 @@ checksum = "99b7ff1008316626f485991b960ade129253d4034014616b94f309a15366cc49" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -595,6 +643,26 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "buffer-redux" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2886ea01509598caac116942abd33ab5a88fa32acdf7e4abfa0fc489ca520c9" +dependencies = [ + "memchr", + "safemem", +] + [[package]] name = "build_id" version = "0.2.1" @@ -633,7 +701,7 @@ checksum = "e31225543cb46f81a7e224762764f4a6a0f097b1db0b175f69e8065efaa42de5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -684,6 +752,25 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "camellia" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3264e2574e9ef2b53ce6f536dea83a69ac0bc600b762d1523ff83fe07230ce30" +dependencies = [ + "byteorder", + "cipher 0.4.4", +] + +[[package]] +name = "cast5" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b07d673db1ccf000e90f54b819db9e75a8348d6eb056e9b8ab53231b7a9911" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "castaway" version = "0.1.2" @@ -708,6 +795,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "cfb-mode" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "738b8d467867f80a71351933f70461f5b56f24d5c93e0cf216e59229c968d330" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -745,6 +841,16 @@ dependencies = [ "generic-array 0.14.6", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -770,6 +876,20 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "concurrent-queue" version = "2.1.0" @@ -779,6 +899,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -851,6 +977,12 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" +[[package]] +name = "crc24" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0" + [[package]] name = "crc32fast" version = "1.3.2" @@ -919,6 +1051,30 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array 0.14.6", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" +dependencies = [ + "generic-array 0.14.6", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -970,6 +1126,34 @@ dependencies = [ "winapi", ] +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "4.0.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d928d978dbec61a1167414f5ec534f24bea0d7a0d24dd9b6233d3d8223e585" +dependencies = [ + "cfg-if 1.0.0", + "digest 0.10.6", + "fiat-crypto", + "packed_simd_2", + "platforms", + "subtle", + "zeroize", +] + [[package]] name = "cxx" version = "1.0.92" @@ -994,7 +1178,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn", + "syn 1.0.109", ] [[package]] @@ -1011,7 +1195,7 @@ checksum = "0b75aed41bb2e6367cae39e6326ef817a851db13c13e4f3263714ca3cfb8de56" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1035,7 +1219,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 1.0.109", ] [[package]] @@ -1046,7 +1230,7 @@ checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ "darling_core", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1062,6 +1246,38 @@ dependencies = [ "parking_lot_core 0.9.7", ] +[[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]] name = "debugid" version = "0.8.0" @@ -1072,6 +1288,59 @@ dependencies = [ "uuid 1.3.0", ] +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "pem-rfc7468 0.6.0", + "zeroize", +] + +[[package]] +name = "der" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56acb310e15652100da43d130af8d97b509e95af61aab1c5a7939ef24337ee17" +dependencies = [ + "const-oid", + "pem-rfc7468 0.7.0", + "zeroize", +] + +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -1082,7 +1351,16 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.0", - "syn", + "syn 1.0.109", +] + +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher 0.4.4", ] [[package]] @@ -1101,6 +1379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common", "subtle", ] @@ -1137,6 +1416,78 @@ version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + +[[package]] +name = "ecdsa" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0997c976637b606099b9985693efa3581e84e41f5c11ba5255f88711058ad428" +dependencies = [ + "der 0.7.6", + "digest 0.10.6", + "elliptic-curve 0.13.5", + "rfc6979 0.4.0", + "signature 2.1.0", + "spki 0.7.2", +] + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature 1.6.4", +] + +[[package]] +name = "ed25519" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fb04eee5d9d907f29e80ee6b0e78f7e2c82342c63e3580d8c4f69d9d5aad963" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.1.0", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek 3.2.0", + "ed25519 1.5.3", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "ed25519-dalek" +version = "2.0.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798f704d128510932661a3489b08e3f4c934a01d61c5def59ae7b8e48f19665a" +dependencies = [ + "curve25519-dalek 4.0.0-rc.2", + "ed25519 2.2.1", + "serde", + "sha2 0.10.6", + "zeroize", +] + [[package]] name = "either" version = "1.8.1" @@ -1146,6 +1497,46 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", + "digest 0.10.6", + "ff 0.12.1", + "generic-array 0.14.6", + "group 0.12.1", + "rand_core 0.6.4", + "sec1 0.3.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.2", + "digest 0.10.6", + "ff 0.13.0", + "generic-array 0.14.6", + "group 0.13.0", + "hkdf", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.32" @@ -1220,6 +1611,32 @@ dependencies = [ "instant", ] +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" + [[package]] name = "findshlibs" version = "0.10.2" @@ -1367,7 +1784,7 @@ checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1432,6 +1849,18 @@ checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] @@ -1463,6 +1892,28 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.0", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.3.16" @@ -1709,6 +2160,15 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "idea" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075557004419d7f2031b8bb7f44bb43e55a83ca7b63076a8fb8fe75753836477" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1772,6 +2232,15 @@ dependencies = [ "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.6", +] + [[package]] name = "instant" version = "0.1.12" @@ -1908,6 +2377,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "keccak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +dependencies = [ + "cpufeatures", +] + [[package]] name = "labrinth" version = "2.7.0" @@ -1925,6 +2403,7 @@ dependencies = [ "chrono", "color-thief", "dashmap", + "deadpool-redis", "dotenvy", "env_logger", "futures", @@ -1936,7 +2415,9 @@ dependencies = [ "lazy_static", "log", "meilisearch-sdk", - "rand", + "pgp", + "rand 0.8.5", + "redis", "regex", "reqwest", "rust-s3", @@ -1950,6 +2431,7 @@ dependencies = [ "sha2 0.9.9", "spdx", "sqlx", + "ssh-key", "thiserror", "tokio", "tokio-stream", @@ -1973,6 +2455,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "lebe" @@ -1999,6 +2484,18 @@ version = "0.2.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +[[package]] +name = "libm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" + +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + [[package]] name = "libnghttp2-sys" version = "0.1.7+1.45.0" @@ -2117,7 +2614,7 @@ checksum = "0f1b8c13cb1f814b634a96b2c725449fe7ed464a7b8781de8688be5ffbd3f305" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -2144,7 +2641,7 @@ dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -2255,7 +2752,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "getrandom", + "getrandom 0.2.8", ] [[package]] @@ -2339,6 +2836,35 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" +dependencies = [ + "byteorder", + "lazy_static", + "libm 0.2.7", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -2349,6 +2875,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -2367,6 +2904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", + "libm 0.2.7", ] [[package]] @@ -2429,7 +2967,7 @@ checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -2472,6 +3010,62 @@ dependencies = [ "winapi", ] +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2 0.10.6", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa 0.16.7", + "elliptic-curve 0.13.5", + "primeorder", + "sha2 0.10.6", +] + +[[package]] +name = "p384" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" +dependencies = [ + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2 0.10.6", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa 0.16.7", + "elliptic-curve 0.13.5", + "primeorder", + "sha2 0.10.6", +] + +[[package]] +name = "packed_simd_2" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" +dependencies = [ + "cfg-if 1.0.0", + "libm 0.1.4", +] + [[package]] name = "palaver" version = "0.2.8" @@ -2556,7 +3150,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -2578,12 +3172,81 @@ dependencies = [ "sha2 0.10.6", ] -[[package]] -name = "percent-encoding" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" - +[[package]] +name = "pem-rfc7468" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pgp" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a79d6411154d1a9908e7a2c4bac60a5742f6125823c2c30780c7039aef02f0" +dependencies = [ + "aes 0.8.2", + "base64 0.21.0", + "bitfield", + "block-padding", + "blowfish", + "bstr", + "buffer-redux", + "byteorder", + "camellia", + "cast5", + "cfb-mode", + "chrono", + "cipher 0.4.4", + "crc24", + "derive_builder", + "des", + "digest 0.10.6", + "ed25519-dalek 2.0.0-rc.2", + "elliptic-curve 0.13.5", + "flate2", + "generic-array 0.14.6", + "hex", + "idea", + "log", + "md-5", + "nom 7.1.3", + "num-bigint-dig", + "num-derive", + "num-traits", + "p256 0.13.2", + "p384 0.13.0", + "rand 0.8.5", + "ripemd", + "rsa 0.9.0-pre.2", + "sha1 0.10.5", + "sha2 0.10.6", + "sha3", + "signature 2.1.0", + "smallvec", + "thiserror", + "twofish", + "x25519-dalek", + "zeroize", +] + [[package]] name = "phonenumber" version = "0.3.1+8.12.9" @@ -2621,7 +3284,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -2636,12 +3299,61 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +dependencies = [ + "der 0.6.1", + "pkcs8 0.9.0", + "spki 0.6.0", + "zeroize", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.6", + "pkcs8 0.10.2", + "spki 0.7.2", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.6", + "spki 0.7.2", +] + [[package]] name = "pkg-config" version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +[[package]] +name = "platforms" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" + [[package]] name = "png" version = "0.17.7" @@ -2696,6 +3408,15 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "primeorder" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c2fcef82c0ec6eefcc179b978446c399b3cdf73c392c35604e399eee6df1ee3" +dependencies = [ + "elliptic-curve 0.13.5", +] + [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -2714,7 +3435,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "version_check", ] @@ -2767,7 +3488,7 @@ checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -2781,13 +3502,37 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.24" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50686e0021c4136d1d453b2dfe059902278681512a34d4248435dc34b6b5c8ec" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" 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 = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -2795,8 +3540,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -2806,7 +3561,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -2815,7 +3579,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.8", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -2840,6 +3613,28 @@ 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" @@ -2855,7 +3650,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom", + "getrandom 0.2.8", "redox_syscall", "thiserror", ] @@ -2938,6 +3733,33 @@ dependencies = [ "winreg", ] +[[package]] +name = "retain_mut" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" + +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac 0.12.1", + "zeroize", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "rgb" version = "0.8.36" @@ -2962,6 +3784,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.6", +] + [[package]] name = "rkyv" version = "0.7.40" @@ -2984,7 +3815,49 @@ checksum = "ff26ed6c7c4dfc2aa9480b86a60e3c7233543a270a680e10758a507c5a4ce476" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", +] + +[[package]] +name = "rsa" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "094052d5470cbcef561cb848a7209968c9f12dfa6d668f4bca048ac5de51099c" +dependencies = [ + "byteorder", + "digest 0.10.6", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1 0.4.1", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "signature 1.6.4", + "smallvec", + "subtle", + "zeroize", +] + +[[package]] +name = "rsa" +version = "0.9.0-pre.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65db0998ad35adcaca498b7358992e088ee16cc783fe6fb899da203e113a63e5" +dependencies = [ + "byteorder", + "const-oid", + "digest 0.10.6", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1 0.7.5", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "signature 2.1.0", + "subtle", + "zeroize", ] [[package]] @@ -3040,7 +3913,7 @@ dependencies = [ "byteorder", "bytes", "num-traits", - "rand", + "rand 0.8.5", "rkyv", "serde", "serde_json", @@ -3140,6 +4013,12 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "schannel" version = "0.1.21" @@ -3149,6 +4028,15 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot 0.12.1", +] + [[package]] name = "scoped_threadpool" version = "0.1.9" @@ -3183,6 +4071,34 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct 0.1.1", + "der 0.6.1", + "generic-array 0.14.6", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48518a2b5775ba8ca5b46596aae011caa431e6ce7e4a67ead66d92f08884220e" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.6", + "generic-array 0.14.6", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.8.2" @@ -3294,7 +4210,7 @@ dependencies = [ "libc", "once_cell", "pprof", - "rand", + "rand 0.8.5", "rustc_version_runtime", "sentry-types", "serde", @@ -3331,7 +4247,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "360ee3270f7a4a1eee6c667f7d38360b995431598a73b740dfe420da548d9cc9" dependencies = [ "debugid", - "getrandom", + "getrandom 0.2.8", "hex", "serde", "serde_json", @@ -3370,7 +4286,7 @@ checksum = "d071a94a3fac4aff69d023a7f411e33f40f3483f8c5190b1953822b6b76d7630" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -3430,7 +4346,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -3483,6 +4399,16 @@ dependencies = [ "digest 0.10.6", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.6", + "keccak", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -3492,6 +4418,26 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest 0.10.6", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.6", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.4" @@ -3573,6 +4519,26 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der 0.7.6", +] + [[package]] name = "sqlformat" version = "0.2.1" @@ -3631,7 +4597,7 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rand", + "rand 0.8.5", "rust_decimal", "rustls", "rustls-pemfile", @@ -3668,7 +4634,7 @@ dependencies = [ "sha2 0.10.6", "sqlx-core", "sqlx-rt", - "syn", + "syn 1.0.109", "url", ] @@ -3683,6 +4649,35 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "ssh-encoding" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19cfdc32e0199062113edf41f344fbf784b8205a94600233c84eb838f45191e1" +dependencies = [ + "base64ct", + "pem-rfc7468 0.6.0", + "sha2 0.10.6", +] + +[[package]] +name = "ssh-key" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288d8f5562af5a3be4bda308dd374b2c807b940ac370b5efa1c99311da91d9a1" +dependencies = [ + "ed25519-dalek 1.0.1", + "p256 0.11.1", + "p384 0.11.2", + "rand_core 0.6.4", + "rsa 0.7.2", + "sec1 0.3.0", + "sha2 0.10.6", + "signature 1.6.4", + "ssh-encoding", + "zeroize", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3750,6 +4745,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sys-info" version = "0.9.1" @@ -3799,7 +4805,7 @@ checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -3967,7 +4973,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -3995,6 +5001,15 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "twofish" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78e83a30223c757c3947cd144a31014ff04298d8719ae10d03c31c0448c8013" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "twox-hash" version = "1.6.3" @@ -4117,7 +5132,7 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" dependencies = [ - "getrandom", + "getrandom 0.2.8", "serde", ] @@ -4150,7 +5165,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn", + "syn 1.0.109", "validator_types", ] @@ -4161,7 +5176,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" dependencies = [ "proc-macro2", - "syn", + "syn 1.0.109", ] [[package]] @@ -4198,6 +5213,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -4231,7 +5252,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wasm-bindgen-shared", ] @@ -4265,7 +5286,7 @@ checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4461,6 +5482,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "x25519-dalek" +version = "2.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" +dependencies = [ + "curve25519-dalek 3.2.0", + "rand_core 0.6.4", + "zeroize", +] + [[package]] name = "xml-rs" version = "0.8.4" @@ -4487,7 +5519,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "xml-rs", ] @@ -4501,13 +5533,33 @@ dependencies = [ "url", ] +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.12", +] + [[package]] name = "zip" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0445d0fbc924bb93539b4316c11afb121ea39296f99a3c4c9edad09e3658cdef" dependencies = [ - "aes", + "aes 0.7.5", "byteorder", "bzip2", "constant_time_eq", diff --git a/Cargo.toml b/Cargo.toml index cc801657..a9e6481e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,9 +66,13 @@ thiserror = "1.0.38" 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"] } +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" image = "0.24.5" color-thief = "0.2.2" +pgp = "0.10.1" +ssh-key = "0.5.1" diff --git a/docker-compose.yml b/docker-compose.yml index f6543609..c88a72bc 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/20230616200130_code-signing.sql b/migrations/20230616200130_code-signing.sql new file mode 100644 index 00000000..4257ede8 --- /dev/null +++ b/migrations/20230616200130_code-signing.sql @@ -0,0 +1,7 @@ +CREATE TABLE signing_keys ( + id bigint PRIMARY KEY, + owner_id bigint REFERENCES users ON UPDATE CASCADE NOT NULL, + body_type varchar(16) NOT NULL, + body text NOT NULL UNIQUE, + created timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL +) diff --git a/sqlx-data.json b/sqlx-data.json index adb3f8a0..48b05618 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -249,92 +249,6 @@ }, "query": "\n UPDATE versions\n SET name = $1\n WHERE (id = $2)\n " }, - "0b77fb8853f15ba9814c80feab68515aa81b82da1414210635239ae6adcd0dd1": { - "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": "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" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Int8Array" - ] - } - }, - "query": "\n SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog, v.date_published, v.downloads,\n v.version_type, v.featured, v.status, v.requested_status\n FROM versions v\n WHERE v.id = ANY($1)\n ORDER BY v.date_published ASC\n " - }, "0ba5a9f4d1381ed37a67b7dc90edf7e3ec86cae6c2860e5db1e53144d4654e58": { "describe": { "columns": [ @@ -528,7 +442,65 @@ }, "query": "\n UPDATE mods\n SET webhook_sent = TRUE\n WHERE id = $1\n " }, - "154f8fdf00fc9905d8e2cd339687c63a523de7e19f6072457cfe723600fb1690": { + "15b8ea323c2f6d03c2e385d9c46d7f13460764f2f106fd638226c42ae0217f75": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM notifications\n WHERE user_id = $1\n " + }, + "16049957962ded08751d5a4ddce2ffac17ecd486f61210c51a952508425d83e6": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + } + }, + "query": "\n UPDATE versions\n SET changelog = $1\n WHERE (id = $2)\n " + }, + "164e5168aabe47d64f99ea851392c9d8479022cff360a610f185c342a24e88d8": { + "describe": { + "columns": [ + { + "name": "mod_id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT mod_id FROM versions WHERE id = $1\n " + }, + "16b3ac53ef5e94f51ab39484add21e2f76d49015917dc877560607a31f5537e9": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + } + }, + "query": "\n UPDATE users\n SET email = $1\n WHERE (id = $2)\n " + }, + "1762798f2b3221292a0152723beda6b4888cf2d15793cca13c3bebc320b2f23e": { "describe": { "columns": [ { @@ -537,9 +509,9 @@ "type_info": "Int8" }, { - "name": "project_type", + "name": "user_id", "ordinal": 1, - "type_info": "Int4" + "type_info": "Int8" }, { "name": "title", @@ -547,239 +519,122 @@ "type_info": "Varchar" }, { - "name": "description", + "name": "text", "ordinal": 3, "type_info": "Varchar" }, { - "name": "downloads", + "name": "link", "ordinal": 4, - "type_info": "Int4" + "type_info": "Varchar" }, { - "name": "follows", + "name": "created", "ordinal": 5, - "type_info": "Int4" + "type_info": "Timestamptz" }, { - "name": "icon_url", + "name": "read", "ordinal": 6, - "type_info": "Varchar" + "type_info": "Bool" }, { - "name": "body", + "name": "notification_type", "ordinal": 7, "type_info": "Varchar" }, { - "name": "published", + "name": "actions", "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" - }, + "type_info": "Jsonb" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + null + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type,\n JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'title', na.title, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.user_id = $1\n GROUP BY n.id, n.user_id;\n " + }, + "177716d2b04fd2a2b63b2e14c8ffdfa554d84254b14053496c118dec24bf5049": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "TextArray" + ] + } + }, + "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 " + }, + "19422e88b1b13318d75e8eb2ba142a562c550358b3136eef9ef73b5a216bbcdb": { + "describe": { + "columns": [ { - "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": "flame_anvil_project", - "ordinal": 26, - "type_info": "Int4" - }, - { - "name": "flame_anvil_user", - "ordinal": 27, - "type_info": "Int8" - }, - { - "name": "webhook_sent", - "ordinal": 28, - "type_info": "Bool" - }, - { - "name": "color", - "ordinal": 29, + "name": "id", + "ordinal": 0, "type_info": "Int4" - }, - { - "name": "loaders", - "ordinal": 30, - "type_info": "VarcharArray" - }, - { - "name": "game_versions", - "ordinal": 31, - "type_info": "VarcharArray" } ], "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, - true, - true, - false, - true, - false, false ], "parameters": { "Left": [ - "Int8Array" + "Varchar", + "Int4", + "Varchar", + "Varchar" ] } }, - "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, flame_anvil_project,\n flame_anvil_user, webhook_sent, color, loaders, game_versions\n FROM mods\n WHERE id = ANY($1)\n " + "query": "\n INSERT INTO categories (category, project_type, icon, header)\n VALUES ($1, $2, $3, $4)\n RETURNING id\n " }, - "15b8ea323c2f6d03c2e385d9c46d7f13460764f2f106fd638226c42ae0217f75": { + "194bf81152eaf4af65d3ec8f735f0472cf19be9234f9da2ad82bad0ea434e8d4": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Int8" + "Int8", + "Int8", + "Varchar", + "Text" ] } }, - "query": "\n DELETE FROM notifications\n WHERE user_id = $1\n " + "query": "\n INSERT INTO signing_keys (\n id, owner_id, body_type, body\n )\n VALUES (\n $1, $2, $3, $4\n )\n " }, - "16049957962ded08751d5a4ddce2ffac17ecd486f61210c51a952508425d83e6": { + "196c8ac2228e199f23eaf980f7ea15b37f76e66bb81da1115a754aad0be756e4": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Varchar", - "Int8" - ] - } - }, - "query": "\n UPDATE versions\n SET changelog = $1\n WHERE (id = $2)\n " - }, - "164e5168aabe47d64f99ea851392c9d8479022cff360a610f185c342a24e88d8": { - "describe": { - "columns": [ - { - "name": "mod_id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Int8" + "Int8", + "Int8", + "Numeric", + "Timestamptz" ] } }, - "query": "\n SELECT mod_id FROM versions WHERE id = $1\n " + "query": "\n INSERT INTO payouts_values (user_id, mod_id, amount, created)\n VALUES ($1, $2, $3, $4)\n " }, - "167ff5286b21a5b93cdc240fcbe66298dcee51de6be272952531b9bf41c5b201": { + "19a10db455e4ce793ded0d93ef7b6b69980bf7ee79a1fb2afa174a9031500da1": { "describe": { "columns": [ { @@ -788,144 +643,110 @@ "type_info": "Int8" }, { - "name": "team_id", + "name": "title", "ordinal": 1, - "type_info": "Int8" + "type_info": "Varchar" }, { - "name": "member_role", + "name": "description", "ordinal": 2, "type_info": "Varchar" }, { - "name": "permissions", + "name": "color", "ordinal": 3, - "type_info": "Int8" + "type_info": "Int4" }, { - "name": "accepted", + "name": "icon_url", "ordinal": 4, - "type_info": "Bool" + "type_info": "Varchar" }, { - "name": "payouts_split", + "name": "slug", "ordinal": 5, - "type_info": "Numeric" + "type_info": "Varchar" }, { - "name": "ordering", + "name": "client_side_type", "ordinal": 6, - "type_info": "Int8" + "type_info": "Varchar" }, { - "name": "user_id", + "name": "server_side_type", "ordinal": 7, - "type_info": "Int8" + "type_info": "Varchar" }, { - "name": "github_id", + "name": "project_type", "ordinal": 8, - "type_info": "Int8" + "type_info": "Varchar" }, { - "name": "user_name", + "name": "username", "ordinal": 9, "type_info": "Varchar" }, { - "name": "email", + "name": "avatar_url", "ordinal": 10, "type_info": "Varchar" }, { - "name": "avatar_url", + "name": "categories", "ordinal": 11, - "type_info": "Varchar" + "type_info": "VarcharArray" }, { - "name": "username", + "name": "loaders", "ordinal": 12, - "type_info": "Varchar" + "type_info": "VarcharArray" }, { - "name": "bio", + "name": "versions", "ordinal": 13, - "type_info": "Varchar" + "type_info": "Jsonb" }, { - "name": "created", + "name": "gallery", "ordinal": 14, - "type_info": "Timestamptz" + "type_info": "VarcharArray" }, { - "name": "user_role", + "name": "featured_gallery", "ordinal": 15, - "type_info": "Varchar" - }, - { - "name": "badges", - "ordinal": 16, - "type_info": "Int8" - }, - { - "name": "balance", - "ordinal": 17, - "type_info": "Numeric" - }, - { - "name": "payout_wallet", - "ordinal": 18, - "type_info": "Varchar" - }, - { - "name": "payout_wallet_type", - "ordinal": 19, - "type_info": "Varchar" - }, - { - "name": "payout_address", - "ordinal": 20, - "type_info": "Varchar" - }, - { - "name": "flame_anvil_key", - "ordinal": 21, - "type_info": "Varchar" + "type_info": "VarcharArray" } ], "nullable": [ false, false, false, - false, - false, - false, - false, - false, - true, true, true, true, false, - true, - false, false, false, false, true, - true, - true, - true + null, + null, + null, + null, + null ], "parameters": { "Left": [ - "Int8Array" + "Int8", + "TextArray", + "Text" ] } }, - "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.github_id github_id, u.name user_name, u.email email,\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, u.flame_anvil_key flame_anvil_key\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 m.id id, m.title title, m.description description, m.color color,\n m.icon_url icon_url, m.slug slug, cs.name client_side_type, ss.name server_side_type,\n pt.name project_type, u.username username, u.avatar_url avatar_url,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n JSONB_AGG(DISTINCT jsonb_build_object('id', gv.id, 'version', gv.version, 'type', gv.type, 'created', gv.created, 'major', gv.major)) 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 FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id AND mc.is_additional = FALSE\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 != ANY($2)\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 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.id = $1\n GROUP BY m.id, cs.id, ss.id, pt.id, u.id;\n " }, - "16b3ac53ef5e94f51ab39484add21e2f76d49015917dc877560607a31f5537e9": { + "19dc22c4d6d14222f8e8bace74c2961761c53b7375460ade15af921754d5d7da": { "describe": { "columns": [], "nullable": [], @@ -936,66 +757,31 @@ ] } }, - "query": "\n UPDATE users\n SET email = $1\n WHERE (id = $2)\n " + "query": "\n UPDATE mods\n SET license = $1\n WHERE (id = $2)\n " }, - "1762798f2b3221292a0152723beda6b4888cf2d15793cca13c3bebc320b2f23e": { + "1ab781d26c93aa74bf90b78b74b99e50004d25d42d56b734e5e83f2333d0c0d2": { "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "user_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "text", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "link", - "ordinal": 4, - "type_info": "Varchar" - }, - { - "name": "created", - "ordinal": 5, - "type_info": "Timestamptz" - }, + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + } + }, + "query": "\n UPDATE users\n SET avatar_url = $1\n WHERE (id = $2)\n " + }, + "1c7b0eb4341af5a7942e52f632cf582561f10b4b6a41a082fb8a60f04ac17c6e": { + "describe": { + "columns": [ { - "name": "read", - "ordinal": 6, + "name": "exists", + "ordinal": 0, "type_info": "Bool" - }, - { - "name": "notification_type", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "actions", - "ordinal": 8, - "type_info": "Jsonb" } ], "nullable": [ - false, - false, - false, - false, - false, - false, - false, - true, null ], "parameters": { @@ -1004,370 +790,218 @@ ] } }, - "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type,\n JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'title', na.title, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.user_id = $1\n GROUP BY n.id, n.user_id;\n " + "query": "SELECT EXISTS(SELECT 1 FROM states WHERE id=$1)" }, - "19422e88b1b13318d75e8eb2ba142a562c550358b3136eef9ef73b5a216bbcdb": { + "1cb2e27dc45e65fd6f2f5118cc3547860762ceef37a75c352ec0ac0ea4214c32": { "describe": { "columns": [ { - "name": "id", + "name": "version_id", "ordinal": 0, - "type_info": "Int4" + "type_info": "Int8" + }, + { + "name": "date_published", + "ordinal": 1, + "type_info": "Timestamptz" } ], "nullable": [ + false, false ], "parameters": { "Left": [ + "Int8", + "VarcharArray", + "VarcharArray", "Varchar", - "Int4", - "Varchar", - "Varchar" + "Int8", + "Int8" ] } }, - "query": "\n INSERT INTO categories (category, project_type, icon, header)\n VALUES ($1, $2, $3, $4)\n RETURNING id\n " + "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 " }, - "196c8ac2228e199f23eaf980f7ea15b37f76e66bb81da1115a754aad0be756e4": { + "1ce90594000fa30876bf277d9ebe2901acf9afaf256dd4488166d55fdd950347": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Int8", - "Int8", - "Numeric", - "Timestamptz" + "Text" ] } }, - "query": "\n INSERT INTO payouts_values (user_id, mod_id, amount, created)\n VALUES ($1, $2, $3, $4)\n " + "query": "\n DELETE FROM donation_platforms\n WHERE short = $1\n " }, - "19a10db455e4ce793ded0d93ef7b6b69980bf7ee79a1fb2afa174a9031500da1": { + "1cefe4924d3c1f491739858ce844a22903d2dbe26f255219299f1833a10ce3d7": { "describe": { "columns": [ { "name": "id", "ordinal": 0, "type_info": "Int8" - }, + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Int8", + "TextArray" + ] + } + }, + "query": "\n SELECT id FROM mods TABLESAMPLE SYSTEM_ROWS($1) WHERE status = ANY($2)\n " + }, + "1d1fe6f0c03a63b1c6bd5ffbddfd82aa7d24e1db3f3137ed046724cb78929f88": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + } + }, + "query": "\n UPDATE users\n SET flame_anvil_key = $1\n WHERE (id = $2)\n " + }, + "1d3b582e6765e1ae578039e44b5dc9be6f3f845c96ffd43b7ba83f9eab816f93": { + "describe": { + "columns": [ { - "name": "title", - "ordinal": 1, + "name": "name", + "ordinal": 0, "type_info": "Varchar" - }, + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Int4" + ] + } + }, + "query": "\n SELECT name FROM report_types\n WHERE id = $1\n " + }, + "1d6f3e926fc4a27c5af172f672b7f825f9f5fe2d538b06337ef182ab1a553398": { + "describe": { + "columns": [ { - "name": "description", - "ordinal": 2, + "name": "name", + "ordinal": 0, "type_info": "Varchar" - }, + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT name FROM project_types pt\n INNER JOIN mods ON mods.project_type = pt.id\n WHERE mods.id = $1\n " + }, + "1db6be78a74ff04c52ee105e0df30acf5bbf18f1de328980bb7f3da7f5f6569e": { + "describe": { + "columns": [ { - "name": "color", - "ordinal": 3, + "name": "id", + "ordinal": 0, "type_info": "Int4" - }, - { - "name": "icon_url", - "ordinal": 4, - "type_info": "Varchar" - }, - { - "name": "slug", - "ordinal": 5, - "type_info": "Varchar" - }, - { - "name": "client_side_type", - "ordinal": 6, - "type_info": "Varchar" - }, - { - "name": "server_side_type", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "project_type", - "ordinal": 8, - "type_info": "Varchar" - }, - { - "name": "username", - "ordinal": 9, - "type_info": "Varchar" - }, - { - "name": "avatar_url", - "ordinal": 10, - "type_info": "Varchar" - }, - { - "name": "categories", - "ordinal": 11, - "type_info": "VarcharArray" - }, - { - "name": "loaders", - "ordinal": 12, - "type_info": "VarcharArray" - }, - { - "name": "versions", - "ordinal": 13, - "type_info": "Jsonb" - }, - { - "name": "gallery", - "ordinal": 14, - "type_info": "VarcharArray" - }, - { - "name": "featured_gallery", - "ordinal": 15, - "type_info": "VarcharArray" } ], "nullable": [ - false, - false, - false, - true, - true, - true, - false, - false, - false, - false, - true, - null, - null, - null, - null, - null + false ], "parameters": { "Left": [ - "Int8", - "TextArray", "Text" ] } }, - "query": "\n SELECT m.id id, m.title title, m.description description, m.color color,\n m.icon_url icon_url, m.slug slug, cs.name client_side_type, ss.name server_side_type,\n pt.name project_type, u.username username, u.avatar_url avatar_url,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n JSONB_AGG(DISTINCT jsonb_build_object('id', gv.id, 'version', gv.version, 'type', gv.type, 'created', gv.created, 'major', gv.major)) 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 FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id AND mc.is_additional = FALSE\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 != ANY($2)\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 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.id = $1\n GROUP BY m.id, cs.id, ss.id, pt.id, u.id;\n " + "query": "\n SELECT id FROM side_types\n WHERE name = $1\n " }, - "19bcfcd376172d2b293e86e9dd69ee778f7447ae708fd0c3c70239d2c8b6a419": { + "1ffce9b2d5c9fa6c8b9abce4bad9f9419c44ad6367b7463b979c91b9b5b4fea1": { "describe": { "columns": [ { - "name": "id", + "name": "exists", "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": [], - "nullable": [], - "parameters": { - "Left": [ - "Varchar", "Int8" ] } }, - "query": "\n UPDATE mods\n SET license = $1\n WHERE (id = $2)\n " + "query": "SELECT EXISTS(SELECT 1 FROM versions WHERE id=$1)" }, - "1ab781d26c93aa74bf90b78b74b99e50004d25d42d56b734e5e83f2333d0c0d2": { + "20413fce27fe9c1dec71900f9563e787acc11e7789b5294786e0ea6f20d7d958": { "describe": { "columns": [], "nullable": [], - "parameters": { - "Left": [ - "Varchar", - "Int8" - ] - } - }, - "query": "\n UPDATE users\n SET avatar_url = $1\n WHERE (id = $2)\n " - }, - "1c7b0eb4341af5a7942e52f632cf582561f10b4b6a41a082fb8a60f04ac17c6e": { - "describe": { - "columns": [ - { - "name": "exists", - "ordinal": 0, - "type_info": "Bool" - } - ], - "nullable": [ - null - ], "parameters": { "Left": [ "Int8" ] } }, - "query": "SELECT EXISTS(SELECT 1 FROM states WHERE id=$1)" + "query": "\n UPDATE mods\n SET flame_anvil_user = NULL\n WHERE (flame_anvil_user = $1)\n " }, - "1cb2e27dc45e65fd6f2f5118cc3547860762ceef37a75c352ec0ac0ea4214c32": { + "20c6f94eae9260fc3f91de3e4a42c544e0b5c01227854956d04db7641c03c1b8": { "describe": { "columns": [ { - "name": "version_id", + "name": "id", "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "date_published", - "ordinal": 1, - "type_info": "Timestamptz" + "type_info": "Int4" } ], "nullable": [ - false, false ], "parameters": { "Left": [ "Int8", - "VarcharArray", - "VarcharArray", - "Varchar", - "Int8", - "Int8" + "Int4Array", + "Int4Array" ] } }, - "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 " + "query": "\n SELECT d.id id\n FROM dependencies d\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = d.dependent_id AND gvv.game_version_id = ANY($2)\n INNER JOIN loaders_versions lv ON lv.version_id = d.dependent_id AND lv.loader_id = ANY($3)\n WHERE d.mod_dependency_id = $1\n " }, - "1ce90594000fa30876bf277d9ebe2901acf9afaf256dd4488166d55fdd950347": { + "2162043897db26d0b55a0652c1a6db66c555f1d148ce69bd0bd0d2122de1bd6a": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Text" + "Int8" ] } }, - "query": "\n DELETE FROM donation_platforms\n WHERE short = $1\n " + "query": "\n DELETE FROM mods_gallery\n WHERE mod_id = $1\n " }, - "1cefe4924d3c1f491739858ce844a22903d2dbe26f255219299f1833a10ce3d7": { + "21ef50f46b7b3e62b91e7d067c1cb33806e14c33bb76d63c2711f822c44261f6": { "describe": { "columns": [ { - "name": "id", + "name": "name", "ordinal": 0, - "type_info": "Int8" + "type_info": "Varchar" } ], "nullable": [ @@ -1375,33 +1009,19 @@ ], "parameters": { "Left": [ - "Int8", - "TextArray" - ] - } - }, - "query": "\n SELECT id FROM mods TABLESAMPLE SYSTEM_ROWS($1) WHERE status = ANY($2)\n " - }, - "1d1fe6f0c03a63b1c6bd5ffbddfd82aa7d24e1db3f3137ed046724cb78929f88": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Varchar", "Int8" ] } }, - "query": "\n UPDATE users\n SET flame_anvil_key = $1\n WHERE (id = $2)\n " + "query": "\n SELECT name FROM project_types pt\n INNER JOIN mods ON mods.project_type = pt.id\n WHERE mods.id = $1\n " }, - "1d3b582e6765e1ae578039e44b5dc9be6f3f845c96ffd43b7ba83f9eab816f93": { + "220e59ae72edef546e3c7682ae91336bfba3e4230add1543910d80e846e0ad95": { "describe": { "columns": [ { - "name": "name", + "name": "id", "ordinal": 0, - "type_info": "Varchar" + "type_info": "Int8" } ], "nullable": [ @@ -1409,22 +1029,28 @@ ], "parameters": { "Left": [ - "Int4" + "Int8" ] } }, - "query": "\n SELECT name FROM report_types\n WHERE id = $1\n " + "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 " }, - "1d6f3e926fc4a27c5af172f672b7f825f9f5fe2d538b06337ef182ab1a553398": { + "2278a7db5eb0474576fa9c86ba97bd6bf13864b3f9ce55ed2ab0cb94edbadaf5": { "describe": { "columns": [ { - "name": "name", + "name": "url", "ordinal": 0, "type_info": "Varchar" + }, + { + "name": "expires", + "ordinal": 1, + "type_info": "Timestamptz" } ], "nullable": [ + false, false ], "parameters": { @@ -1433,9 +1059,23 @@ ] } }, - "query": "\n SELECT name FROM project_types pt\n INNER JOIN mods ON mods.project_type = pt.id\n WHERE mods.id = $1\n " + "query": "\n SELECT url, expires FROM states\n WHERE id = $1\n " }, - "1db6be78a74ff04c52ee105e0df30acf5bbf18f1de328980bb7f3da7f5f6569e": { + "232d7d0319c20dd5fff29331b067d6c6373bcff761a77958a2bb5f59068a83a5": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int8" + ] + } + }, + "query": "\n UPDATE team_members\n SET permissions = $1\n WHERE (team_id = $2 AND user_id = $3)\n " + }, + "25131559cb73a088000ab6379a769233440ade6c7511542da410065190d203fc": { "describe": { "columns": [ { @@ -1453,63 +1093,79 @@ ] } }, - "query": "\n SELECT id FROM side_types\n WHERE name = $1\n " + "query": "\n SELECT id FROM loaders\n WHERE loader = $1\n " }, - "1ffce9b2d5c9fa6c8b9abce4bad9f9419c44ad6367b7463b979c91b9b5b4fea1": { + "2534464b06d567078bcfaa94e0c5e37729db111f5b46c4035cabe72634104b2e": { "describe": { "columns": [ { - "name": "exists", + "name": "id", "ordinal": 0, - "type_info": "Bool" + "type_info": "Int8" + }, + { + "name": "user_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "payouts_split", + "ordinal": 2, + "type_info": "Numeric" } ], "nullable": [ - null + false, + false, + false ], "parameters": { "Left": [ - "Int8" + "Int8Array" ] } }, - "query": "SELECT EXISTS(SELECT 1 FROM versions WHERE id=$1)" + "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 " }, - "20413fce27fe9c1dec71900f9563e787acc11e7789b5294786e0ea6f20d7d958": { + "27a35fca63dfc3801f95958604f0ac27afd81800e2dc981382d6f923c4415d32": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Int8" + "Int8Array" ] } }, - "query": "\n UPDATE mods\n SET flame_anvil_user = NULL\n WHERE (flame_anvil_user = $1)\n " + "query": "\n DELETE FROM notifications_actions\n WHERE notification_id = ANY($1)\n " }, - "20c6f94eae9260fc3f91de3e4a42c544e0b5c01227854956d04db7641c03c1b8": { + "27f223d15ee8fb46c98fd61cbc8f12ec5adf034532afb5ac548c78f39154c64a": { "describe": { "columns": [ { - "name": "id", + "name": "team_id", "ordinal": 0, - "type_info": "Int4" + "type_info": "Int8" + }, + { + "name": "slug", + "ordinal": 1, + "type_info": "Varchar" } ], "nullable": [ - false + false, + true ], "parameters": { "Left": [ - "Int8", - "Int4Array", - "Int4Array" + "Int8" ] } }, - "query": "\n SELECT d.id id\n FROM dependencies d\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = d.dependent_id AND gvv.game_version_id = ANY($2)\n INNER JOIN loaders_versions lv ON lv.version_id = d.dependent_id AND lv.loader_id = ANY($3)\n WHERE d.mod_dependency_id = $1\n " + "query": "\n SELECT team_id, slug FROM mods WHERE id = $1\n " }, - "2162043897db26d0b55a0652c1a6db66c555f1d148ce69bd0bd0d2122de1bd6a": { + "28d5825964b0fddc43bd7d6851daf91845b79c9e88c82d5c7d97ae02502d0b4f": { "describe": { "columns": [], "nullable": [], @@ -1519,75 +1175,290 @@ ] } }, - "query": "\n DELETE FROM mods_gallery\n WHERE mod_id = $1\n " + "query": "INSERT INTO banned_users (github_id) VALUES ($1);" }, - "21ef50f46b7b3e62b91e7d067c1cb33806e14c33bb76d63c2711f822c44261f6": { + "299b8ea6e7a0048fa389cc4432715dc2a09e227d2f08e91167a43372a7ac6e35": { "describe": { - "columns": [ - { - "name": "name", - "ordinal": 0, - "type_info": "Varchar" - } - ], - "nullable": [ - false - ], + "columns": [], + "nullable": [], "parameters": { "Left": [ "Int8" ] } }, - "query": "\n SELECT name FROM project_types pt\n INNER JOIN mods ON mods.project_type = pt.id\n WHERE mods.id = $1\n " + "query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1 AND is_additional = FALSE\n " }, - "220e59ae72edef546e3c7682ae91336bfba3e4230add1543910d80e846e0ad95": { + "29e657d26f0fb24a766f5b5eb6a94d01d1616884d8ca10e91536e974d5b585a6": { "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], + "columns": [], + "nullable": [], "parameters": { "Left": [ + "Int4", "Int8" ] } }, - "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 " + "query": "\n INSERT INTO loaders_versions (loader_id, version_id)\n VALUES ($1, $2)\n " }, - "2278a7db5eb0474576fa9c86ba97bd6bf13864b3f9ce55ed2ab0cb94edbadaf5": { + "29ed222444c301e9cc9e95b43948c08d3f78ad64ca09cde085775b8dd13f39ea": { "describe": { "columns": [ { - "name": "url", + "name": "id", "ordinal": 0, - "type_info": "Varchar" + "type_info": "Int8" }, { - "name": "expires", + "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": "flame_anvil_project", + "ordinal": 29, + "type_info": "Int4" + }, + { + "name": "flame_anvil_user", + "ordinal": 30, + "type_info": "Int8" + }, + { + "name": "webhook_sent", + "ordinal": 31, + "type_info": "Bool" + }, + { + "name": "color", + "ordinal": 32, + "type_info": "Int4" + }, + { + "name": "loaders", + "ordinal": 33, + "type_info": "VarcharArray" + }, + { + "name": "game_versions", + "ordinal": 34, + "type_info": "VarcharArray" + }, + { + "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, + false, + true, + false, + false, + false, + true, + true, + false, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true, + false, + false, + false, + true, + true, + false, + true, + false, + false, + null, + null, + null, + null, + null ], "parameters": { "Left": [ - "Int8" + "Int8Array", + "TextArray", + "TextArray" ] } }, - "query": "\n SELECT url, expires FROM states\n WHERE id = $1\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.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user, m.webhook_sent, m.color,\n m.loaders loaders, m.game_versions 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_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 mods_gallery mg ON mg.mod_id = m.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY pt.id, cs.id, ss.id, m.id;\n " }, - "232d7d0319c20dd5fff29331b067d6c6373bcff761a77958a2bb5f59068a83a5": { + "29fcff0f1d36bd1a9e0c8c4005209308f0c5f383e4e52ed8c6b989994ead32df": { "describe": { "columns": [], "nullable": [], @@ -1599,122 +1470,54 @@ ] } }, - "query": "\n UPDATE team_members\n SET permissions = $1\n WHERE (team_id = $2 AND user_id = $3)\n " - }, - "25131559cb73a088000ab6379a769233440ade6c7511542da410065190d203fc": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int4" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT id FROM loaders\n WHERE loader = $1\n " + "query": "\n UPDATE team_members\n SET ordering = $1\n WHERE (team_id = $2 AND user_id = $3)\n " }, - "2534464b06d567078bcfaa94e0c5e37729db111f5b46c4035cabe72634104b2e": { + "2a4d12bd340ed79773d36d68b0bdaf86cd5df5f1d83389d9f4a1b530b53e7bc6": { "describe": { "columns": [ { - "name": "id", + "name": "url", "ordinal": 0, - "type_info": "Int8" + "type_info": "Varchar" }, { - "name": "user_id", + "name": "hash", "ordinal": 1, - "type_info": "Int8" + "type_info": "Bytea" }, { - "name": "payouts_split", + "name": "algorithm", "ordinal": 2, - "type_info": "Numeric" + "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": [ - "Int8Array" - ] - } - }, - "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 " - }, - "28d5825964b0fddc43bd7d6851daf91845b79c9e88c82d5c7d97ae02502d0b4f": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "INSERT INTO banned_users (github_id) VALUES ($1);" - }, - "299b8ea6e7a0048fa389cc4432715dc2a09e227d2f08e91167a43372a7ac6e35": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1 AND is_additional = FALSE\n " - }, - "29e657d26f0fb24a766f5b5eb6a94d01d1616884d8ca10e91536e974d5b585a6": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int4", - "Int8" - ] - } - }, - "query": "\n INSERT INTO loaders_versions (loader_id, version_id)\n VALUES ($1, $2)\n " - }, - "29fcff0f1d36bd1a9e0c8c4005209308f0c5f383e4e52ed8c6b989994ead32df": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Int8" + "TextArray", + "ByteaArray", + "Text", + "TextArray" ] } }, - "query": "\n UPDATE team_members\n SET ordering = $1\n WHERE (team_id = $2 AND user_id = $3)\n " + "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": { @@ -1891,26 +1694,6 @@ }, "query": "\n DELETE FROM loaders_versions\n WHERE loaders_versions.version_id = $1\n " }, - "34c0c25212dd8bc133f1e79b968d18d2b66eb537aeaba752e7ab2847a2214db4": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT u.id\n FROM users u\n WHERE u.stripe_customer_id = $1\n " - }, "352185977065c9903c2504081ef7c400075807785d4b62fdb48d0a45ca560f51": { "describe": { "columns": [ @@ -2205,45 +1988,191 @@ }, "query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1 AND is_additional = TRUE\n " }, - "414951c52e3342b4009cd1d0169bc34b164ab00db0af8c2d446a178a52e5fd6c": { + "447350097928db863d47d756354cd52668f52f7156dd7f3673a826f7b9aca2fd": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int4" + }, + { + "name": "version_", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "type_", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "created", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "major", + "ordinal": 4, + "type_info": "Bool" + } + ], + "nullable": [ + false, + false, + false, + false, + false + ], + "parameters": { + "Left": [ + "Bool", + "Text" + ] + } + }, + "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 " + }, + "44bb1034872a80bbea122e04399470fd5f029b819c70cb6e0cb2db6d3193b97e": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Text" + "Int4", + "Int4" ] } }, - "query": "\n UPDATE users\n SET stripe_customer_id = NULL, midas_expires = NULL, is_overdue = NULL\n WHERE (stripe_customer_id = $1)\n " + "query": "\n INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id)\n VALUES ($1, $2)\n " }, - "447350097928db863d47d756354cd52668f52f7156dd7f3673a826f7b9aca2fd": { + "4514723bdc1eb8a781215075bec51af1cc6fabe88a469338d5a59533eabf80c5": { "describe": { "columns": [ { - "name": "id", - "ordinal": 0, + "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": "published", + "ordinal": 7, + "type_info": "Timestamptz" + }, + { + "name": "approved", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "updated", + "ordinal": 9, + "type_info": "Timestamptz" + }, + { + "name": "team_id", + "ordinal": 10, + "type_info": "Int8" + }, + { + "name": "license", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "slug", + "ordinal": 12, + "type_info": "Varchar" + }, + { + "name": "status_name", + "ordinal": 13, + "type_info": "Varchar" + }, + { + "name": "color", + "ordinal": 14, "type_info": "Int4" }, { - "name": "version_", - "ordinal": 1, + "name": "client_side_type", + "ordinal": 15, "type_info": "Varchar" }, { - "name": "type_", - "ordinal": 2, + "name": "server_side_type", + "ordinal": 16, "type_info": "Varchar" }, { - "name": "created", - "ordinal": 3, - "type_info": "Timestamptz" + "name": "project_type_name", + "ordinal": 17, + "type_info": "Varchar" }, { - "name": "major", - "ordinal": 4, - "type_info": "Bool" + "name": "username", + "ordinal": 18, + "type_info": "Varchar" + }, + { + "name": "categories", + "ordinal": 19, + "type_info": "VarcharArray" + }, + { + "name": "additional_categories", + "ordinal": 20, + "type_info": "VarcharArray" + }, + { + "name": "loaders", + "ordinal": 21, + "type_info": "VarcharArray" + }, + { + "name": "versions", + "ordinal": 22, + "type_info": "VarcharArray" + }, + { + "name": "gallery", + "ordinal": 23, + "type_info": "VarcharArray" + }, + { + "name": "featured_gallery", + "ordinal": 24, + "type_info": "VarcharArray" } ], "nullable": [ @@ -2251,29 +2180,37 @@ false, false, false, - false + false, + false, + true, + false, + true, + false, + false, + false, + true, + false, + true, + false, + false, + false, + false, + null, + null, + null, + null, + null, + null ], "parameters": { "Left": [ - "Bool", + "TextArray", + "TextArray", "Text" ] } }, - "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 " - }, - "44bb1034872a80bbea122e04399470fd5f029b819c70cb6e0cb2db6d3193b97e": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int4", - "Int4" - ] - } - }, - "query": "\n INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id)\n VALUES ($1, $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.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 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 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 " }, "4567790f0dc98ff20b596a33161d1f6ac8af73da67fe8c54192724626c6bf670": { "describe": { @@ -2456,26 +2393,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": [], @@ -2551,41 +2468,6 @@ }, "query": "\n UPDATE mods\n SET slug = LOWER($1)\n WHERE (id = $2)\n " }, - "501c4aec0d0b2b17b86b1923b949b27e8091ff8f9a75fa4a2ce7ecf294241f46": { - "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 != ANY($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 != ANY($4)\n " - }, "507314fdcacaa3c7751738c9d0baee2b90aec719b6b203f922824eced5ea8369": { "describe": { "columns": [], @@ -2634,19 +2516,6 @@ }, "query": "\n SELECT l.id id, l.loader loader, l.icon icon,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types\n FROM loaders l\n LEFT OUTER JOIN loaders_project_types lpt ON joining_loader_id = l.id\n LEFT OUTER JOIN project_types pt ON lpt.joining_project_type_id = pt.id\n GROUP BY l.id;\n " }, - "529b02dc17d406ef66a4e2720cf9e50dff40c5a59a3521f348565cfb1ca6f5c9": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "TextArray" - ] - } - }, - "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 != ANY($2)\n )\n WHERE id = $1\n " - }, "53a8966ac345cc334ad65ea907be81af74e90b1217696c7eedcf8a8e3fca736e": { "describe": { "columns": [], @@ -2812,47 +2681,6 @@ }, "query": "\n DELETE FROM mod_follows\n WHERE mod_id = $1\n " }, - "5e0bac32b936202670596d24c0a2a6bbe30f4f4fdc28ad5a3846ac4c51c77b7d": { - "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 != ANY($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ANY($4)\n ORDER BY v.date_published ASC\n " - }, "5eb2795d25d6d03e22564048c198d821cd5ff22eb4e39b9dd7f198c9113d4f87": { "describe": { "columns": [], @@ -2960,25 +2788,69 @@ }, "query": "\n UPDATE mods\n SET requested_status = $1\n WHERE (id = $2)\n " }, - "67d021f0776276081d3c50ca97afa6b78b98860bf929009e845e9c00a192e3b5": { + "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 " + }, + "68ca661100d6110141bd4c1b98e18831f26ae5b899786f42b71e03396bac5ab3": { "describe": { "columns": [ { "name": "id", "ordinal": 0, - "type_info": "Int4" + "type_info": "Int8" + }, + { + "name": "owner_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "body_type", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "body", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "created", + "ordinal": 4, + "type_info": "Timestamptz" } ], "nullable": [ + false, + false, + false, + false, false ], "parameters": { "Left": [ - "Text" + "Int8Array" ] } }, - "query": "\n SELECT id FROM report_types\n WHERE name = $1\n " + "query": "\n SELECT k.id, k.owner_id, k.body_type, k.body, k.created\n FROM signing_keys k\n WHERE k.id = ANY($1)\n ORDER BY k.created DESC\n " }, "69bb839ea7fd5687538656e1907599d75e2c4948a54d58446bec8a90170ee618": { "describe": { @@ -3033,6 +2905,19 @@ }, "query": "\n DELETE FROM mods\n WHERE id = $1\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 " + }, "6c7aeb0db4a4fb3387c37b8d7aca6fdafaa637fd883a44416b56270aeebb7a01": { "describe": { "columns": [], @@ -3444,29 +3329,6 @@ }, "query": "\n DELETE FROM loaders_versions WHERE version_id = $1\n " }, - "7a27b31e1e671c33898b88a5ecba8a95b6920c1bb1527a44d5c54432a14a5d18": { - "describe": { - "columns": [ - { - "name": "project_id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "TextArray", - "Bytea", - "Text", - "TextArray" - ] - } - }, - "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 != ANY($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ANY($4)\n ORDER BY v.date_published ASC\n " - }, "7ab21e7613dd88e97cf602e76bff62170c13ceef8104a4ce4cb2d101f8ce4f48": { "describe": { "columns": [], @@ -3654,19 +3516,6 @@ }, "query": "\n UPDATE users\n SET username = $1\n WHERE (id = $2)\n " }, - "8795ba421d96b38384e38c8c880c66078b1fcd3c72b76a5bbc24253ebbad63fe": { - "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 != ANY($2)\n )\n WHERE id = $1\n " - }, "87fd169e19ba231c6cf131ad2841d5c3b95adde53e5ed4000f8e7d54c0e87320": { "describe": { "columns": [], @@ -3745,35 +3594,6 @@ }, "query": "\n DELETE FROM loaders\n WHERE loader = $1\n " }, - "9284d7f22617e0a7daf91540ff31791d0921ec5d4eb4809846dc67567bec1a81": { - "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 != ANY($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 != ANY($4)\n " - }, "9348309884811e8b22f33786ae7c0f259f37f3c90e545f00761a641570107160": { "describe": { "columns": [ @@ -3806,26 +3626,6 @@ }, "query": "\n SELECT m.title title, m.id id, pt.name project_type\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n WHERE m.team_id = $1\n " }, - "9381c483b29d364f14c46d5e73bc14b1ec5d0525e27b9e9b099cb0786934fe78": { - "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 " - }, "96b2f4e0e619e7ed312d191dc90d64113235d72254fbda8f528ce866d1795cb5": { "describe": { "columns": [ @@ -3982,26 +3782,6 @@ }, "query": "\n INSERT INTO team_members (id, team_id, user_id, role, permissions, accepted, payouts_split, ordering)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\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": [], @@ -4047,18 +3827,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": [ @@ -4178,7 +3946,123 @@ ] } }, - "query": "\n UPDATE mods\n SET status = $1, approved = $2\n WHERE (id = $3)\n " + "query": "\n UPDATE mods\n SET status = $1, approved = $2\n WHERE (id = $3)\n " + }, + "a62767e812783e8836a11b22878a4248123f3fe212a876e192f549acd6edcb39": { + "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 " }, "a647c282a276b63f36d2d8a253c32d0f627cea9cab8eb1b32b39875536bdfcbb": { "describe": { @@ -4315,261 +4199,6 @@ }, "query": "\n SELECT loader_id id FROM loaders_versions\n WHERE version_id = $1\n " }, - "ad9c63f994f1e075fea75cff02b14b5a17f3a09ed0132ed8a38d1d22ff7ae5ac": { - "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": "flame_anvil_project", - "ordinal": 29, - "type_info": "Int4" - }, - { - "name": "flame_anvil_user", - "ordinal": 30, - "type_info": "Int8" - }, - { - "name": "webhook_sent", - "ordinal": 31, - "type_info": "Bool" - }, - { - "name": "color", - "ordinal": 32, - "type_info": "Int4" - }, - { - "name": "loaders", - "ordinal": 33, - "type_info": "VarcharArray" - }, - { - "name": "game_versions", - "ordinal": 34, - "type_info": "VarcharArray" - }, - { - "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, - true, - true, - false, - true, - false, - 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.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user, m.webhook_sent, m.color,\n m.loaders loaders, m.game_versions 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_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 " - }, "adbe17a5ad3cea333b30b5d6111aff713a8f7dc79ded21f5ba942c4f1108aa8f": { "describe": { "columns": [ @@ -4754,62 +4383,6 @@ }, "query": "\n DELETE FROM report_types\n WHERE name = $1\n " }, - "bc43bbc55ed517c62f2caefea173ea87547061159c8e37282f1ae340dd1d84c5": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "team_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "role", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "permissions", - "ordinal": 3, - "type_info": "Int8" - }, - { - "name": "accepted", - "ordinal": 4, - "type_info": "Bool" - }, - { - "name": "payouts_split", - "ordinal": 5, - "type_info": "Numeric" - }, - { - "name": "ordering", - "ordinal": 6, - "type_info": "Int8" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT id, team_id, role, permissions, accepted, payouts_split, ordering\n FROM team_members\n WHERE user_id = $1\n ORDER BY ordering\n " - }, "bc605f80a615c7d0ca9c8207f8b0c5dc1b8f2ad0f9b3346a00078d59e5e3e253": { "describe": { "columns": [ @@ -4856,26 +4429,6 @@ }, "query": "\n DELETE FROM dependencies WHERE dependent_id = $1\n " }, - "bec1612d4929d143bc5d6860a57cc036c5ab23e69d750ca5791c620297953c50": { - "describe": { - "columns": [ - { - "name": "team_id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT team_id FROM mods WHERE id = $1\n " - }, "bee1abe8313d17a56d93b06a31240e338c3973bc7a7374799ced3df5e38d3134": { "describe": { "columns": [], @@ -4896,195 +4449,29 @@ "Left": [ "Int8", "Int8", - "Varchar", - "Varchar", - "Varchar", - "Timestamptz", - "Int4", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Int4", - "Int4", - "Varchar", - "Varchar", - "Text", - "Int4", - "Int4" - ] - } - }, - "query": "\n INSERT INTO mods (\n id, team_id, title, description, body,\n published, downloads, icon_url, issues_url,\n source_url, wiki_url, status, requested_status, discord_url,\n client_side, server_side, license_url, license,\n slug, project_type, color\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9,\n $10, $11, $12, $13, $14,\n $15, $16, $17, $18,\n LOWER($19), $20, $21\n )\n " - }, - "bf4afeda41a54e09a80a4cc505d1fbb72124c442ebaca731a291f022524daf1a": { - "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": "published", - "ordinal": 7, - "type_info": "Timestamptz" - }, - { - "name": "approved", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "updated", - "ordinal": 9, - "type_info": "Timestamptz" - }, - { - "name": "team_id", - "ordinal": 10, - "type_info": "Int8" - }, - { - "name": "license", - "ordinal": 11, - "type_info": "Varchar" - }, - { - "name": "slug", - "ordinal": 12, - "type_info": "Varchar" - }, - { - "name": "status_name", - "ordinal": 13, - "type_info": "Varchar" - }, - { - "name": "color", - "ordinal": 14, - "type_info": "Int4" - }, - { - "name": "client_side_type", - "ordinal": 15, - "type_info": "Varchar" - }, - { - "name": "server_side_type", - "ordinal": 16, - "type_info": "Varchar" - }, - { - "name": "project_type_name", - "ordinal": 17, - "type_info": "Varchar" - }, - { - "name": "username", - "ordinal": 18, - "type_info": "Varchar" - }, - { - "name": "categories", - "ordinal": 19, - "type_info": "VarcharArray" - }, - { - "name": "additional_categories", - "ordinal": 20, - "type_info": "VarcharArray" - }, - { - "name": "loaders", - "ordinal": 21, - "type_info": "VarcharArray" - }, - { - "name": "versions", - "ordinal": 22, - "type_info": "VarcharArray" - }, - { - "name": "gallery", - "ordinal": 23, - "type_info": "VarcharArray" - }, - { - "name": "featured_gallery", - "ordinal": 24, - "type_info": "VarcharArray" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - false, - true, - false, - true, - false, - false, - false, - false, - null, - null, - null, - null, - null, - null - ], - "parameters": { - "Left": [ - "TextArray", - "TextArray", - "Text" + "Varchar", + "Varchar", + "Varchar", + "Timestamptz", + "Int4", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Int4", + "Int4", + "Varchar", + "Varchar", + "Text", + "Int4", + "Int4" ] } }, - "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 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 != ANY($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 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 " + "query": "\n INSERT INTO mods (\n id, team_id, title, description, body,\n published, downloads, icon_url, issues_url,\n source_url, wiki_url, status, requested_status, discord_url,\n client_side, server_side, license_url, license,\n slug, project_type, color\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9,\n $10, $11, $12, $13, $14,\n $15, $16, $17, $18,\n LOWER($19), $20, $21\n )\n " }, "bf7f721664f5e0ed41adc41b5483037256635f28ff6c4e5d3cbcec4387f9c8ef": { "describe": { @@ -5207,62 +4594,6 @@ }, "query": "\n UPDATE versions\n SET version_type = $1\n WHERE (id = $2)\n " }, - "c276ad563ad7c8abcd4e42b4cb51be76e849fee352311a1b1fdb9fd6f2ccaed0": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "team_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "role", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "permissions", - "ordinal": 3, - "type_info": "Int8" - }, - { - "name": "accepted", - "ordinal": 4, - "type_info": "Bool" - }, - { - "name": "payouts_split", - "ordinal": 5, - "type_info": "Numeric" - }, - { - "name": "ordering", - "ordinal": 6, - "type_info": "Int8" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT id, team_id, role, permissions, accepted, payouts_split, ordering\n FROM team_members\n WHERE (user_id = $1 AND accepted = TRUE)\n ORDER BY ordering\n " - }, "c3f594d8d0ffcf5df1b36759cf3088bfaec496c5dfdbf496d3b05f0b122a5d0c": { "describe": { "columns": [], @@ -5375,16 +4706,34 @@ }, "query": "\n SELECT category FROM categories\n WHERE id = $1\n " }, - "c79e4f7d3ffbda57daaf58f61cc0397a423b7bc877d2abc975c262d668f41f70": { + "c89b70c1428f32d4247497e9a89f83155d724100741e292282dd4f3679b0c2bc": { "describe": { "columns": [ { - "name": "version_id", + "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": { @@ -5396,7 +4745,7 @@ ] } }, - "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 != ANY($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ANY($4)\n ORDER BY v.date_published ASC\n " + "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": { @@ -5430,6 +4779,116 @@ }, "query": "\n SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1)\n " }, + "ca27d38b06fd9dc60a9f127851de4e1054994e00f7f57a1ecd92f8cdb4d0e9fc": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "team_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "member_role", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "permissions", + "ordinal": 3, + "type_info": "Int8" + }, + { + "name": "accepted", + "ordinal": 4, + "type_info": "Bool" + }, + { + "name": "payouts_split", + "ordinal": 5, + "type_info": "Numeric" + }, + { + "name": "ordering", + "ordinal": 6, + "type_info": "Int8" + }, + { + "name": "user_id", + "ordinal": 7, + "type_info": "Int8" + }, + { + "name": "github_id", + "ordinal": 8, + "type_info": "Int8" + }, + { + "name": "user_name", + "ordinal": 9, + "type_info": "Varchar" + }, + { + "name": "avatar_url", + "ordinal": 10, + "type_info": "Varchar" + }, + { + "name": "username", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "bio", + "ordinal": 12, + "type_info": "Varchar" + }, + { + "name": "created", + "ordinal": 13, + "type_info": "Timestamptz" + }, + { + "name": "user_role", + "ordinal": 14, + "type_info": "Varchar" + }, + { + "name": "badges", + "ordinal": 15, + "type_info": "Int8" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + false, + true, + false, + false, + false + ], + "parameters": { + "Left": [ + "Int8Array" + ] + } + }, + "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.github_id github_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 " + }, "cb57ae673f1a7e50cc319efddb9bdc82e2251596bcf85aea52e8def343e423b8": { "describe": { "columns": [], @@ -5490,6 +4949,41 @@ }, "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": [], @@ -5508,12 +5002,41 @@ "nullable": [], "parameters": { "Left": [ - "Int8", - "Int4" + "Int8", + "Int4" + ] + } + }, + "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 INSERT INTO mods_categories (joining_mod_id, joining_category_id, is_additional)\n VALUES ($1, $2, FALSE)\n " + "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 " }, "ce46915d4ce10f3fc2d4328157b06016da838672c8336b3b8d27e09eeec979d3": { "describe": { @@ -5766,6 +5289,62 @@ }, "query": "\n UPDATE team_members\n SET accepted = TRUE\n WHERE (team_id = $1 AND user_id = $2)\n " }, + "d44a9ef55fa7988c53b6062e31da52c88845fc772f5d726a3e32db0b55f748b6": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8Array" + ] + } + }, + "query": "\n DELETE FROM signing_keys\n WHERE id = ANY($1)\n " + }, + "d4e545d7fdcaee6f621058d827bc199634cc3f71561ae5596d9cfa973d9b1129": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "owner_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "body_type", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "body", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "created", + "ordinal": 4, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + false, + false, + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT k.id, k.owner_id, k.body_type, k.body, k.created\n FROM signing_keys k\n WHERE k.owner_id = $1\n " + }, "d59a0ca4725d40232eae8bf5735787e1b76282c390d2a8d07fb34e237a0b2132": { "describe": { "columns": [], @@ -5800,6 +5379,29 @@ }, "query": "\n INSERT INTO donation_platforms (short, name)\n VALUES ($1, $2)\n ON CONFLICT (short) DO NOTHING\n RETURNING id\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": [ @@ -6097,26 +5699,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 " - }, "e29da865af4a0a110275b9756394546a3bb88bff40e18c66029651f515caed98": { "describe": { "columns": [ @@ -6213,65 +5795,38 @@ }, "query": "\n UPDATE mods\n SET flame_anvil_user = NULL\n WHERE (team_id = $1 AND flame_anvil_user = $2 )\n " }, - "e48c85a2b2e11691afae3799aa126bdd8b7338a973308bbab2760c18bb9cb0b7": { + "e4886e676d69fb325060618f6e9c48996da69d148422a6121dd9e4b10e2ab80b": { "describe": { - "columns": [], - "nullable": [], + "columns": [ + { + "name": "exists", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], "parameters": { "Left": [ - "Bool", "Int8" ] } }, - "query": "\n UPDATE versions\n SET featured = $1\n WHERE (id = $2)\n " + "query": "SELECT EXISTS(SELECT 1 FROM signing_keys WHERE id=$1)" }, - "e5bbcf58b8f4abb91757a7dea8d7151cbeaa79fe3aee6542476c1174e82fbe92": { + "e48c85a2b2e11691afae3799aa126bdd8b7338a973308bbab2760c18bb9cb0b7": { "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 - ], + "columns": [], + "nullable": [], "parameters": { "Left": [ - "TextArray", - "ByteaArray", - "Text", - "TextArray" + "Bool", + "Int8" ] } }, - "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 != ANY($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 != ANY($4)\n " + "query": "\n UPDATE versions\n SET featured = $1\n WHERE (id = $2)\n " }, "e673006d1355fa91ba5739d7cf569eec5e1ec501f7b1dc2b431f0b1c25ac07d5": { "describe": { @@ -6305,75 +5860,6 @@ }, "query": "SELECT EXISTS(SELECT 1 FROM team_members WHERE id=$1)" }, - "e845ace468e484025d7b052008f703dee66ce7fcc33862df7e9ccfe7553eef96": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "user_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "role", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "permissions", - "ordinal": 3, - "type_info": "Int8" - }, - { - "name": "accepted", - "ordinal": 4, - "type_info": "Bool" - }, - { - "name": "payouts_split", - "ordinal": 5, - "type_info": "Numeric" - }, - { - "name": "ordering", - "ordinal": 6, - "type_info": "Int8" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT id, user_id, role, permissions, accepted, payouts_split, ordering\n FROM team_members\n WHERE team_id = $1\n ORDER BY ordering\n " - }, - "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": [ @@ -6536,6 +6022,29 @@ }, "query": "\n UPDATE mods\n SET follows = follows + 1\n WHERE id = $1\n " }, + "ed46b2a7665471eedf72b1be8868d1e64f5fbf4a568d4c0b8b469ca388fd70e6": { + "describe": { + "columns": [ + { + "name": "project_id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "TextArray", + "Bytea", + "Text", + "TextArray" + ] + } + }, + "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 " + }, "ed5c72e789353869837e0653914c86d5d1002a4227d022567e02f280684d71a7": { "describe": { "columns": [], @@ -6754,19 +6263,6 @@ }, "query": "\n UPDATE users\n SET bio = $1\n WHERE (id = $2)\n " }, - "f6eae06931e9cde0f18e7031bc93c33fa689de4d9676c1a8a3fc14a182d5fb08": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Timestamptz", - "Text" - ] - } - }, - "query": "\n UPDATE users\n SET midas_expires = $1, is_overdue = FALSE\n WHERE (stripe_customer_id = $2)\n " - }, "f8be3053274b00ee9743e798886696062009c5f681baaf29dfc24cfbbda93742": { "describe": { "columns": [ diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index 1e150a94..5192eb7f 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; @@ -106,35 +106,47 @@ generate_ids!( NotificationId ); -#[derive(Copy, Clone, Debug, PartialEq, Eq, Type)] +generate_ids!( + pub generate_signing_key_id, + SigningKeyId, + 8, + "SELECT EXISTS(SELECT 1 FROM signing_keys WHERE id=$1)", + SigningKeyId +); + +#[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)] @@ -154,7 +166,9 @@ 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); @@ -169,6 +183,10 @@ pub struct NotificationId(pub i64); #[sqlx(transparent)] pub struct NotificationActionId(pub i32); +#[derive(Copy, Clone, Debug, Type, Deserialize)] +#[sqlx(transparent)] +pub struct SigningKeyId(pub i64); + use crate::models::ids; impl From for ProjectId { @@ -231,3 +249,13 @@ impl From for ids::NotificationId { ids::NotificationId(id.0 as u64) } } +impl From for SigningKeyId { + fn from(id: ids::SigningKeyId) -> Self { + SigningKeyId(id.0 as i64) + } +} +impl From for ids::SigningKeyId { + fn from(id: SigningKeyId) -> Self { + ids::SigningKeyId(id.0 as u64) + } +} diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs index d1df5538..c33de79e 100644 --- a/src/database/models/mod.rs +++ b/src/database/models/mod.rs @@ -9,6 +9,7 @@ pub mod ids; pub mod notification_item; pub mod project_item; pub mod report_item; +pub mod signing_key_item; pub mod team_item; pub mod user_item; pub mod version_item; @@ -26,6 +27,14 @@ pub enum DatabaseError { Database(#[from] sqlx::error::Error), #[error("Error while trying to generate random ID")] RandomId, + #[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), + #[error("Error while reading signing key")] + KeyBodyError(#[from] crate::util::keys::KeyError), #[error("A database request failed")] Other(String), } diff --git a/src/database/models/project_item.rs b/src/database/models/project_item.rs index 4ae28702..18258be8 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::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; // 1 day + +#[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, @@ -195,7 +204,7 @@ impl ProjectBuilder { Ok(self.project_id) } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Project { pub id: ProjectId, pub project_type: ProjectTypeId, @@ -236,7 +245,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 ( @@ -282,110 +291,28 @@ 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, flame_anvil_project, - flame_anvil_user, webhook_sent, color, loaders, game_versions - 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, - flame_anvil_project: m.flame_anvil_project, - flame_anvil_user: m.flame_anvil_user.map(UserId), - webhook_sent: m.webhook_sent, - color: m.color.map(|x| x as u32), - loaders: m.loaders, - game_versions: m.game_versions, - queued: m.queued, - })) - }) - .try_collect::>() - .await?; - - Ok(projects) - } - pub async fn remove_full( id: ProjectId, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - ) -> Result, sqlx::error::Error> { + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> { let result = sqlx::query!( " - SELECT team_id FROM mods WHERE id = $1 + SELECT team_id, slug FROM mods WHERE id = $1 ", id as ProjectId, ) .fetch_optional(&mut *transaction) .await?; - let team_id: TeamId = if let Some(id) = result { - TeamId(id.team_id) + let (team_id, slug) = if let Some(id) = result { + (TeamId(id.team_id), id.slug) } else { return Ok(None); }; + Project::clear_cache(id, slug, Some(true), redis).await?; + sqlx::query!( " DELETE FROM mod_follows @@ -460,7 +387,7 @@ impl Project { .await?; for version in versions { - super::Version::remove_full(version, transaction).await?; + super::Version::remove_full(version, redis, transaction).await?; } sqlx::query!( @@ -493,6 +420,8 @@ impl Project { .execute(&mut *transaction) .await?; + models::TeamMember::clear_cache(team_id, redis).await?; + sqlx::query!( " DELETE FROM team_members @@ -516,249 +445,367 @@ impl Project { Ok(Some(())) } - pub async fn get_full_from_slug<'a, 'b, E>( - slug: &str, + pub async fn get<'a, 'b, E>( + string: &str, 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>, { - 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_full(ProjectId(project_id.id), executor).await - } else { - Ok(None) - } + Project::get_many(&[string], executor, redis) + .await + .map(|x| x.into_iter().next()) } - pub async fn get_from_slug<'a, 'b, E>( - slug: &str, + pub async fn get_id<'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> + Copy, + E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - let id = sqlx::query!( - " - SELECT id FROM mods - WHERE slug = LOWER($1) - ", - slug + Project::get_many( + &[crate::models::ids::ProjectId::from(id)], + executor, + redis, ) - .fetch_optional(executor) - .await?; - - if let Some(project_id) = id { - Project::get(ProjectId(project_id.id), executor).await - } else { - Ok(None) - } + .await + .map(|x| x.into_iter().next()) } - pub async fn get_from_slug_or_project_id<'a, 'b, E>( - slug_or_project_id: &str, - executor: E, - ) -> Result, sqlx::error::Error> + pub async fn get_many_ids<'a, E>( + project_ids: &[ProjectId], + exec: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - let id_option = - crate::models::ids::base62_impl::parse_base62(slug_or_project_id) - .ok(); + let ids = project_ids + .iter() + .map(|x| crate::models::ids::ProjectId::from(*x)) + .collect::>(); + Project::get_many(&ids, exec, redis).await + } - if let Some(id) = id_option { - let mut project = - Project::get(ProjectId(id as i64), executor).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; - if project.is_none() { - project = Project::get_from_slug(slug_or_project_id, executor) - .await?; - } + if project_strings.is_empty() { + return Ok(Vec::new()); + } - Ok(project) - } else { - let project = - Project::get_from_slug(slug_or_project_id, executor).await?; + 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?; - Ok(project) + 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; + } + } } - } - 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 !remaining_strings.is_empty() { + let project_ids_parsed: Vec = remaining_strings + .iter() + .flat_map(|x| parse_base62(x).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.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user, m.webhook_sent, m.color, + m.loaders loaders, m.game_versions 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_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 mods_gallery mg ON mg.mod_id = m.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_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 { + id: ProjectId(id), + project_type: ProjectTypeId(m.project_type), + team_id: TeamId(m.team_id), + title: m.title.clone(), + description: m.description.clone(), + downloads: m.downloads, + body_url: None, + icon_url: m.icon_url.clone(), + published: m.published, + updated: m.updated, + issues_url: m.issues_url.clone(), + source_url: m.source_url.clone(), + wiki_url: m.wiki_url.clone(), + license_url: m.license_url.clone(), + discord_url: m.discord_url.clone(), + 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.clone(), + slug: m.slug.clone(), + body: m.body.clone(), + follows: m.follows, + moderation_message: m.moderation_message, + moderation_message_body: m.moderation_message_body, + approved: m.approved, + flame_anvil_project: m.flame_anvil_project, + flame_anvil_user: m.flame_anvil_user.map(UserId), + webhook_sent: m.webhook_sent, + color: m.color.map(|x| x as u32), + loaders: m.loaders, + game_versions: m.game_versions, + queued: m.queued, + }, + project_type: m.project_type_name, + 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, + } + + 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), + }})) + }) + .try_collect::>() + .await?; - if let Some(id) = id_option { - let mut project = - Project::get_full(ProjectId(id as i64), executor).await?; + 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?; - if project.is_none() { - project = - Project::get_full_from_slug(slug_or_project_id, executor) + 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); } - - 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> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - Project::get_many_full(&[id], executor) - .await - .map(|x| x.into_iter().next()) + Ok(found_projects) } - pub async fn get_many_full<'a, E>( - project_ids: &[ProjectId], + pub async fn get_dependencies<'a, E>( + id: ProjectId, exec: E, - ) -> Result, sqlx::Error> + redis: &deadpool_redis::Pool, + ) -> Result< + Vec<(Option, Option, Option)>, + DatabaseError, + > where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - use futures::TryStreamExt; + type Dependencies = + Vec<(Option, Option, Option)>; - let project_ids_parsed: Vec = - project_ids.iter().map(|x| x.0).collect(); - sqlx::query!( + 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?; + + if let Some(dependencies) = dependencies + .and_then(|x| serde_json::from_str::(&x).ok()) + { + return Ok(dependencies); + } + + let dependencies: Dependencies = 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.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user, m.webhook_sent, m.color, - m.loaders loaders, m.game_versions 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_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; + 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 ", - &project_ids_parsed, - &*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_listed()).map(|x| x.to_string()).collect::>() + id as ProjectId ) .fetch_many(exec) .try_filter_map(|e| async { - Ok(e.right().map(|m| { - let id = m.id; - - QueryProject { - inner: Project { - id: ProjectId(id), - project_type: ProjectTypeId(m.project_type), - team_id: TeamId(m.team_id), - title: m.title.clone(), - description: m.description.clone(), - downloads: m.downloads, - body_url: None, - icon_url: m.icon_url.clone(), - published: m.published, - updated: m.updated, - issues_url: m.issues_url.clone(), - source_url: m.source_url.clone(), - wiki_url: m.wiki_url.clone(), - license_url: m.license_url.clone(), - discord_url: m.discord_url.clone(), - 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.clone(), - slug: m.slug.clone(), - body: m.body.clone(), - follows: m.follows, - moderation_message: m.moderation_message, - moderation_message_body: m.moderation_message_body, - approved: m.approved, - flame_anvil_project: m.flame_anvil_project, - flame_anvil_user: m.flame_anvil_user.map(UserId), - webhook_sent: m.webhook_sent, - color: m.color.map(|x| x as u32), - loaders: m.loaders, - game_versions: m.game_versions, - queued: m.queued, - }, - project_type: m.project_type_name, - 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, - } - - 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), - }})) + 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 + .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 clear_cache( + id: ProjectId, + 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(()) } pub async fn update_game_versions( @@ -773,7 +820,7 @@ impl Project { 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 != ANY($2) + WHERE v.mod_id = mods.id AND v.status != ALL($2) ) WHERE id = $1 ", @@ -798,7 +845,7 @@ impl Project { 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 != ANY($2) + WHERE v.mod_id = mods.id AND v.status != ALL($2) ) WHERE id = $1 ", @@ -812,7 +859,7 @@ impl Project { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct QueryProject { pub inner: Project, pub project_type: String, diff --git a/src/database/models/signing_key_item.rs b/src/database/models/signing_key_item.rs new file mode 100644 index 00000000..467e350d --- /dev/null +++ b/src/database/models/signing_key_item.rs @@ -0,0 +1,146 @@ +use crate::util::keys::SigningKeyBody; + +use super::{ids::*, DatabaseError}; +use chrono::{DateTime, Utc}; +use futures::stream::{StreamExt, TryStreamExt}; + +pub struct SigningKey { + pub id: SigningKeyId, + pub owner_id: UserId, + pub body: SigningKeyBody, + pub created: DateTime, +} + +impl SigningKey { + pub async fn insert( + &self, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result<(), sqlx::error::Error> { + sqlx::query!( + " + INSERT INTO signing_keys ( + id, owner_id, body_type, body + ) + VALUES ( + $1, $2, $3, $4 + ) + ", + self.id as SigningKeyId, + self.owner_id as UserId, + self.body.type_str() as &'static str, + self.body.to_body() as String, + ) + .execute(&mut *transaction) + .await?; + + Ok(()) + } + + pub async fn get<'a, E>( + id: SigningKeyId, + exec: E, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + Self::get_many(&[id], exec) + .await + .map(|x| x.into_iter().next()) + } + + pub async fn get_many<'a, E>( + key_ids: &[SigningKeyId], + exec: E, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + let key_ids_parsed: Vec = key_ids.iter().map(|x| x.0).collect(); + let keys = sqlx::query!( + " + SELECT k.id, k.owner_id, k.body_type, k.body, k.created + FROM signing_keys k + WHERE k.id = ANY($1) + ORDER BY k.created DESC + ", + &key_ids_parsed + ) + .fetch_many(exec) + .map(|x| x.map_err(DatabaseError::from)) + .try_filter_map(|e| async { + Ok(e.right() + .map(|x| { + Ok::(SigningKey { + id: SigningKeyId(x.id), + owner_id: UserId(x.owner_id), + body: SigningKeyBody::parse(&x.body_type, &x.body)?, + created: x.created, + }) + }) + .transpose()?) + }) + .try_collect::>() + .await?; + + Ok(keys) + } + + pub async fn get_many_user<'a, E>( + user_id: UserId, + exec: E, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + sqlx::query!( + " + SELECT k.id, k.owner_id, k.body_type, k.body, k.created + FROM signing_keys k + WHERE k.owner_id = $1 + ", + user_id as UserId + ) + .fetch_many(exec) + .map(|x| x.map_err(DatabaseError::from)) + .try_filter_map(|e| async { + Ok(e.right() + .map(|x| { + Ok::(SigningKey { + id: SigningKeyId(x.id), + owner_id: UserId(x.owner_id), + body: SigningKeyBody::parse(&x.body_type, &x.body)?, + created: x.created, + }) + }) + .transpose()?) + }) + .try_collect::>() + .await + } + + pub async fn remove( + id: SigningKeyId, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result, sqlx::error::Error> { + Self::remove_many(&[id], transaction).await + } + + pub async fn remove_many( + key_ids: &[SigningKeyId], + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result, sqlx::error::Error> { + let key_ids_parsed: Vec = key_ids.iter().map(|x| x.0).collect(); + + sqlx::query!( + " + DELETE FROM signing_keys + WHERE id = ANY($1) + ", + &key_ids_parsed + ) + .execute(&mut *transaction) + .await?; + + Ok(Some(())) + } +} diff --git a/src/database/models/team_item.rs b/src/database/models/team_item.rs index f09f3e57..a88b67a7 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, @@ -91,6 +97,7 @@ pub struct TeamMember { } /// A member of a team +#[derive(Deserialize, Serialize)] pub struct QueryTeamMember { pub id: TeamMemberId, pub team_id: TeamId, @@ -104,226 +111,139 @@ pub struct QueryTeamMember { } impl TeamMember { - /// Lists the members of a team - pub async fn get_from_team<'a, 'b, E>( - id: TeamId, - executor: E, - ) -> Result, super::DatabaseError> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - use futures::stream::TryStreamExt; - - let team_members = sqlx::query!( - " - SELECT id, user_id, role, permissions, accepted, payouts_split, ordering - FROM team_members - WHERE team_id = $1 - ORDER BY ordering - ", - id as TeamId, - ) - .fetch_many(executor) - .try_filter_map(|e| async { - if let Some(m) = e.right() { - Ok(Some(Ok(TeamMember { - id: TeamMemberId(m.id), - team_id: id, - user_id: UserId(m.user_id), - role: m.role, - permissions: Permissions::from_bits(m.permissions as u64) - .unwrap_or_default(), - accepted: m.accepted, - payouts_split: m.payouts_split, - ordering: m.ordering, - }))) - } else { - Ok(None) - } - }) - .try_collect::>>() - .await?; - - let team_members = team_members - .into_iter() - .collect::, super::DatabaseError>>()?; - - Ok(team_members) - } - // Lists the full members of a team 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.github_id github_id, u.name user_name, u.email email, - 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, u.flame_anvil_key flame_anvil_key - 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() { - - 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, - 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, - flame_anvil_key: m.flame_anvil_key, - }, - payouts_split: m.payouts_split, - ordering: m.ordering, - }))) - } else { - Ok(None) - } - }) - .try_collect::>>() - .await?; - - let team_members = teams - .into_iter() - .collect::, super::DatabaseError>>()?; + let mut redis = redis.get().await?; - Ok(team_members) - } + let mut found_teams = Vec::new(); - /// Lists the team members for a user. Does not list pending requests. - pub async fn get_from_user_public<'a, 'b, E>( - id: UserId, - executor: E, - ) -> Result, super::DatabaseError> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - use futures::stream::TryStreamExt; + let teams = cmd("MGET") + .arg( + team_ids_parsed + .iter() + .map(|x| format!("{}:{}", TEAMS_NAMESPACE, x)) + .collect::>(), + ) + .query_async::<_, Vec>>(&mut redis) + .await?; - let team_members = sqlx::query!( - " - SELECT id, team_id, role, permissions, accepted, payouts_split, ordering - FROM team_members - WHERE (user_id = $1 AND accepted = TRUE) - ORDER BY ordering - ", - id as UserId, - ) - .fetch_many(executor) - .try_filter_map(|e| async { - if let Some(m) = e.right() { - Ok(Some(Ok(TeamMember { - id: TeamMemberId(m.id), - team_id: TeamId(m.team_id), - user_id: id, - role: m.role, - permissions: Permissions::from_bits(m.permissions as u64) - .unwrap_or_default(), - accepted: m.accepted, - payouts_split: m.payouts_split, - ordering: m.ordering, - }))) - } else { - Ok(None) + 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; } - }) - .try_collect::>>() - .await?; + } - let team_members = team_members - .into_iter() - .collect::, super::DatabaseError>>()?; + 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.github_id github_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: m.github_id, + 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, + flame_anvil_key: None, + }, + payouts_split: m.payouts_split, + ordering: m.ordering, + } + )) + }) + .try_collect::>() + .await?; - Ok(team_members) - } + for (id, members) in &teams.into_iter().group_by(|x| x.team_id) { + let mut members = members.collect::>(); - /// Lists the team members for a user. Includes pending requests. - pub async fn get_from_user_private<'a, 'b, E>( - id: UserId, - executor: E, - ) -> Result, super::DatabaseError> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, - { - use futures::stream::TryStreamExt; + cmd("SET") + .arg(format!("{}:{}", TEAMS_NAMESPACE, id.0)) + .arg(serde_json::to_string(&members)?) + .arg("EX") + .arg(DEFAULT_EXPIRY) + .query_async::<_, ()>(&mut redis) + .await?; - let team_members = sqlx::query!( - " - SELECT id, team_id, role, permissions, accepted, payouts_split, ordering - FROM team_members - WHERE user_id = $1 - ORDER BY ordering - ", - id as UserId, - ) - .fetch_many(executor) - .try_filter_map(|e| async { - if let Some(m) = e.right() { - Ok(Some(Ok(TeamMember { - id: TeamMemberId(m.id), - team_id: TeamId(m.team_id), - user_id: id, - role: m.role, - permissions: Permissions::from_bits(m.permissions as u64) - .unwrap_or_default(), - accepted: m.accepted, - payouts_split: m.payouts_split, - ordering: m.ordering, - }))) - } else { - Ok(None) + found_teams.append(&mut members); } - }) - .try_collect::>>() - .await?; + } - let team_members = team_members - .into_iter() - .collect::, super::DatabaseError>>()?; + Ok(found_teams) + } - Ok(team_members) + 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 14837a50..e12c63d7 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -1,8 +1,11 @@ use super::ids::{ProjectId, UserId}; +use crate::database::models::DatabaseError; use crate::models::users::{Badges, RecipientType, RecipientWallet}; use chrono::{DateTime, Utc}; use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; +#[derive(Deserialize, Serialize)] pub struct User { pub id: UserId, pub github_id: Option, @@ -371,7 +374,8 @@ impl User { pub async fn remove_full( id: UserId, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - ) -> Result, sqlx::error::Error> { + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> { use futures::TryStreamExt; let projects: Vec = sqlx::query!( " @@ -391,6 +395,7 @@ impl User { let _result = super::project_item::Project::remove_full( project_id, transaction, + redis, ) .await?; } diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index 82497643..4b4fb398 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -3,10 +3,16 @@ use super::DatabaseError; use crate::models::ids::base62_impl::parse_base62; use crate::models::projects::{FileType, VersionStatus, VersionType}; use chrono::{DateTime, Utc}; -use serde::Deserialize; +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 DEFAULT_EXPIRY: i64 = 1800; // 30 minutes + pub struct VersionBuilder { pub version_id: VersionId, pub project_id: ProjectId, @@ -245,7 +251,7 @@ impl VersionBuilder { } } -#[derive(Clone)] +#[derive(Clone, Deserialize, Serialize)] pub struct Version { pub id: VersionId, pub project_id: ProjectId, @@ -300,8 +306,9 @@ impl Version { pub async fn remove_full( id: VersionId, + redis: &deadpool_redis::Pool, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - ) -> Result, sqlx::Error> { + ) -> Result, DatabaseError> { let result = sqlx::query!( " SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1) @@ -315,6 +322,8 @@ impl Version { return Ok(None); } + Version::clear_cache(id, redis).await?; + sqlx::query!( " DELETE FROM reports @@ -478,6 +487,7 @@ impl Version { Ok(Some(())) } + // TODO: remove in future- replace with rust side filtering pub async fn get_project_versions<'a, E>( project_id: ProjectId, game_versions: Option>, @@ -518,6 +528,7 @@ impl Version { Ok(vec) } + // TODO: remove in future- replace with rust side filtering pub async fn get_projects_versions<'a, E>( project_ids: Vec, game_versions: Option>, @@ -571,11 +582,12 @@ impl Version { 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, { - Self::get_many(&[id], executor) + Self::get_many(&[id], executor, redis) .await .map(|x| x.into_iter().next()) } @@ -583,218 +595,221 @@ impl Version { 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> + Copy, { + if version_ids.is_empty() { + return Ok(Vec::new()); + } + use futures::stream::TryStreamExt; - let version_ids_parsed: Vec = + let mut version_ids_parsed: Vec = version_ids.iter().map(|x| x.0).collect(); - let versions = sqlx::query!( - " - SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number, - v.changelog, v.date_published, v.downloads, - v.version_type, v.featured, v.status, v.requested_status - FROM versions v - WHERE v.id = ANY($1) - ORDER BY v.date_published ASC - ", - &version_ids_parsed - ) - .fetch_many(exec) - .try_filter_map(|e| async { - Ok(e.right().map(|v| Version { - id: VersionId(v.id), - project_id: ProjectId(v.mod_id), - author_id: UserId(v.author_id), - name: v.name, - version_number: v.version_number, - changelog: v.changelog, - changelog_url: None, - date_published: v.date_published, - downloads: v.downloads, - featured: v.featured, - version_type: v.version_type, - status: VersionStatus::from_str(&v.status), - requested_status: v - .requested_status - .map(|x| VersionStatus::from_str(&x)), - })) - }) - .try_collect::>() - .await?; - Ok(versions) - } + let mut redis = redis.get().await?; - pub async fn get_full<'a, 'b, E>( - id: VersionId, - executor: E, - ) -> Result, sqlx::error::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { - Self::get_many_full(&[id], executor) - .await - .map(|x| x.into_iter().next()) - } + let mut found_versions = Vec::new(); - pub async fn get_many_full<'a, E>( - version_ids: &[VersionId], - exec: E, - ) -> Result, sqlx::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, - { - use futures::stream::TryStreamExt; + let versions = cmd("MGET") + .arg( + version_ids_parsed + .iter() + .map(|x| format!("{}:{}", VERSIONS_NAMESPACE, x)) + .collect::>(), + ) + .query_async::<_, Vec>>(&mut redis) + .await?; - 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(); + 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; + } + } - let files: Vec = serde_json::from_value( - v.files.unwrap_or_default(), - ) - .ok() - .unwrap_or_default(); + 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, + } - let mut files = files.into_iter().map(|x| { - let mut file_hashes = HashMap::new(); + #[derive(Deserialize)] + struct File { + pub id: FileId, + pub url: String, + pub filename: String, + pub primary: bool, + pub size: u32, + pub file_type: Option, + } - for hash in &hashes { - if hash.file_id == x.id { - file_hashes.insert( - hash.algorithm.clone(), - hash.hash.clone(), - ); + 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) + 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, } - }); - - 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(), + + 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(); - - 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 + .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 clear_cache( + id: VersionId, + redis: &deadpool_redis::Pool, + ) -> Result<(), DatabaseError> { + let mut redis = redis.get().await?; + cmd("DEL") + .arg(format!("{}:{}", VERSIONS_NAMESPACE, id.0)) + .query_async::<_, ()>(&mut redis) + .await?; + + Ok(()) } 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, { @@ -817,14 +832,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, @@ -834,7 +849,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, @@ -842,7 +857,7 @@ pub struct QueryDependency { pub dependency_type: String, } -#[derive(Clone)] +#[derive(Clone, Deserialize, Serialize)] pub struct QueryFile { pub id: FileId, pub url: String, diff --git a/src/main.rs b/src/main.rs index 67a8dd06..9a981df1 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; @@ -75,6 +76,14 @@ 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()); @@ -157,6 +166,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!"); @@ -256,7 +266,7 @@ async fn main() -> std::io::Result<()> { } } - Ok::<(), crate::routes::ApiError>(()) + Ok::<(), routes::ApiError>(()) }; if let Err(e) = do_steps.await { @@ -359,6 +369,7 @@ async fn main() -> std::io::Result<()> { .app_data(web::JsonConfig::default().error_handler(|err, _req| { 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())) @@ -411,6 +422,8 @@ fn check_env_vars() -> bool { failed |= check_var::("MEILISEARCH_KEY"); failed |= check_var::("BIND_ADDR"); + failed |= check_var::("REDIS_URL"); + failed |= check_var::("STORAGE_BACKEND"); let storage_backend = dotenvy::var("STORAGE_BACKEND").ok(); @@ -449,9 +462,6 @@ fn check_env_vars() -> bool { 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 02202078..3cd8ee90 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::signing_keys::SigningKeyId; pub use super::teams::TeamId; pub use super::users::UserId; @@ -109,6 +110,7 @@ base62_id_impl!(VersionId, VersionId); base62_id_impl!(TeamId, TeamId); base62_id_impl!(ReportId, ReportId); base62_id_impl!(NotificationId, NotificationId); +base62_id_impl!(SigningKeyId, SigningKeyId); pub mod base62_impl { use serde::de::{self, Deserializer, Visitor}; diff --git a/src/models/mod.rs b/src/models/mod.rs index a4608452..15934f3f 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -4,5 +4,6 @@ pub mod notifications; pub mod pack; pub mod projects; pub mod reports; +pub mod signing_keys; pub mod teams; pub mod users; diff --git a/src/models/signing_keys.rs b/src/models/signing_keys.rs new file mode 100644 index 00000000..4a8c04ba --- /dev/null +++ b/src/models/signing_keys.rs @@ -0,0 +1,33 @@ +use super::ids::Base62Id; +use crate::models::ids::UserId; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "Base62Id")] +#[serde(into = "Base62Id")] +pub struct SigningKeyId(pub u64); + +#[derive(Serialize, Deserialize)] +pub struct SigningKey { + pub id: SigningKeyId, + pub owner: UserId, + #[serde(rename = "type")] + pub key_type: String, + pub body: String, + pub created: DateTime, +} + +use crate::database::models::signing_key_item::SigningKey as DBSigningKey; + +impl From for SigningKey { + fn from(value: DBSigningKey) -> Self { + SigningKey { + id: value.id.into(), + owner: value.owner_id.into(), + key_type: value.body.type_str().to_string(), + body: value.body.to_body(), + created: value.created, + } + } +} diff --git a/src/routes/maven.rs b/src/routes/maven.rs index a21cef0a..0c8f7e92 100644 --- a/src/routes/maven.rs +++ b/src/routes/maven.rs @@ -66,13 +66,11 @@ 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 @@ -82,7 +80,7 @@ pub async fn maven_metadata( let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); - if !is_authorized(&data, &user_option, &pool).await? { + if !is_authorized(&data.inner, &user_option, &pool).await? { return Ok(HttpResponse::NotFound().body("")); } @@ -93,7 +91,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()) @@ -121,7 +119,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(), @@ -135,7 +133,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(), }, }; @@ -190,14 +188,11 @@ 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?; + database::models::Project::get(&project_id, &**pool, &redis).await?; let project = if let Some(data) = project_data { data @@ -229,9 +224,10 @@ pub async fn version_file( return Ok(HttpResponse::NotFound().body("")); }; - let version = if let Some(version) = database::models::Version::get_full( + let version = if let Some(version) = database::models::Version::get( database::models::ids::VersionId(vid.id), &**pool, + &redis, ) .await? { @@ -279,14 +275,11 @@ 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?; + database::models::Project::get(&project_id, &**pool, &redis).await?; let project = if let Some(data) = project_data { data @@ -318,9 +311,10 @@ pub async fn version_file_sha1( return Ok(HttpResponse::NotFound().body("")); }; - let version = if let Some(version) = database::models::Version::get_full( + let version = if let Some(version) = database::models::Version::get( database::models::ids::VersionId(vid.id), &**pool, + &redis, ) .await? { @@ -340,14 +334,11 @@ 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?; + database::models::Project::get(&project_id, &**pool, &redis).await?; let project = if let Some(data) = project_data { data @@ -379,9 +370,10 @@ pub async fn version_file_sha512( return Ok(HttpResponse::NotFound().body("")); }; - let version = if let Some(version) = database::models::Version::get_full( + let version = if let Some(version) = database::models::Version::get( database::models::ids::VersionId(vid.id), &**pool, + &redis, ) .await? { diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 193ecea3..9b183a86 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -60,8 +60,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}")] @@ -70,6 +68,8 @@ pub enum ApiError { Decoding(#[from] crate::models::ids::DecodingError), #[error("Image Parsing Error: {0}")] ImageError(#[from] image::ImageError), + #[error("Key Error: {0}")] + KeysError(#[from] crate::util::keys::KeyError), } impl actix_web::ResponseError for ApiError { @@ -88,11 +88,11 @@ 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, ApiError::ImageError(..) => StatusCode::BAD_REQUEST, + ApiError::KeysError(..) => StatusCode::BAD_REQUEST, } } @@ -113,11 +113,11 @@ 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", ApiError::ImageError(..) => "invalid_image", + ApiError::KeysError(..) => "keys_error", }, description: &self.to_string(), }, diff --git a/src/routes/updates.rs b/src/routes/updates.rs index b0ad0a0e..59235b63 100644 --- a/src/routes/updates.rs +++ b/src/routes/updates.rs @@ -21,39 +21,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) - .await? - .ok_or_else(|| ApiError::InvalidInput(ERROR.to_string()))?; + 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(); - 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/midas.rs b/src/routes/v2/midas.rs deleted file mode 100644 index e191c0cc..00000000 --- a/src/routes/v2/midas.rs +++ /dev/null @@ -1,346 +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 573b588b..1cfc0e6d 100644 --- a/src/routes/v2/mod.rs +++ b/src/routes/v2/mod.rs @@ -1,11 +1,11 @@ mod admin; mod auth; -mod midas; mod moderation; mod notifications; pub(crate) mod project_creation; mod projects; mod reports; +mod signing_keys; mod statistics; mod tags; mod teams; @@ -21,17 +21,24 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) { actix_web::web::scope("v2") .configure(admin::config) .configure(auth::config) - .configure(midas::config) .configure(moderation::config) .configure(notifications::config) .configure(project_creation::config) + .configure(signing_keys::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) + // 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 90458b2d..74cb98bb 100644 --- a/src/routes/v2/moderation.rs +++ b/src/routes/v2/moderation.rs @@ -29,6 +29,7 @@ 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?; @@ -53,7 +54,7 @@ pub async fn get_projects( .await?; let projects: Vec<_> = - database::Project::get_many_full(&project_ids, &**pool) + database::Project::get_many_ids(&project_ids, &**pool, &redis) .await? .into_iter() .map(crate::models::projects::Project::from) diff --git a/src/routes/v2/projects.rs b/src/routes/v2/projects.rs index 740a4202..e0efadfc 100644 --- a/src/routes/v2/projects.rs +++ b/src/routes/v2/projects.rs @@ -45,10 +45,12 @@ pub fn config(cfg: &mut web::ServiceConfig) { .service(project_unfollow) .service(project_schedule) .service(super::teams::team_members_get_project) - .service(web::scope("{project_id}") - .service(super::versions::version_list) - .service(super::versions::version_project_get) - .service(dependency_list)), + .service( + web::scope("{project_id}") + .service(super::versions::version_list) + .service(super::versions::version_project_get) + .service(dependency_list), + ), ); } @@ -71,6 +73,7 @@ pub struct RandomProjects { pub async fn random_projects_get( web::Query(count): web::Query, pool: web::Data, + redis: web::Data, ) -> Result { count.validate().map_err(|err| { ApiError::Validation(validation_errors_to_string(err, None)) @@ -91,7 +94,7 @@ pub async fn random_projects_get( .await?; let projects_data = - database::models::Project::get_many_full(&project_ids, &**pool) + database::models::Project::get_many_ids(&project_ids, &**pool, &redis) .await? .into_iter() .map(Project::from) @@ -110,15 +113,11 @@ 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_full(&project_ids, &**pool).await?; + database::models::Project::get_many(&ids, &**pool, &redis).await?; let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); @@ -133,14 +132,12 @@ 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?; + database::models::Project::get(&string, &**pool, &redis).await?; let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); @@ -157,52 +154,16 @@ 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 = models::ids::base62_impl::parse_base62(&slug).ok(); - - 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) - }; + let project_data = + database::models::Project::get(&slug, &**pool, &redis).await?; - 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("")) @@ -220,53 +181,25 @@ 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(); 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 + let dependencies = database::Project::get_dependencies( + project.inner.id, + &**pool, + &redis, ) - .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 project_ids = dependencies @@ -289,8 +222,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?; @@ -421,6 +354,7 @@ 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?; @@ -429,10 +363,8 @@ pub async fn project_edit( })?; 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; @@ -914,9 +846,14 @@ 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)) ", slug @@ -1082,7 +1019,8 @@ pub async fn project_edit( if let Some(moderation_message) = &new_project.moderation_message { if !user.role.is_mod() - && project_item.inner.status != ProjectStatus::Approved + && (!project_item.inner.status.is_approved() + || moderation_message.is_some()) { return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the moderation message of this project!" @@ -1107,7 +1045,8 @@ pub async fn project_edit( &new_project.moderation_message_body { if !user.role.is_mod() - && project_item.inner.status != ProjectStatus::Approved + && (!project_item.inner.status.is_approved() + || moderation_message_body.is_some()) { return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the moderation message body of this project!" @@ -1149,6 +1088,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 { @@ -1230,6 +1177,7 @@ 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?; @@ -1244,7 +1192,8 @@ pub async fn projects_edit( .collect(); let projects_data = - database::models::Project::get_many_full(&project_ids, &**pool).await?; + database::models::Project::get_many_ids(&project_ids, &**pool, &redis) + .await?; if let Some(id) = project_ids .iter() @@ -1261,7 +1210,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, + &team_ids, &**pool, &redis, ) .await?; @@ -1274,10 +1223,10 @@ pub async fn projects_edit( for project in projects_data { if !user.role.is_mod() { - if let Some(member) = team_members - .iter() - .find(|x| x.team_id == project.inner.team_id) - { + if let Some(member) = team_members.iter().find(|x| { + x.team_id == project.inner.team_id + && x.user.id == user.id.into() + }) { if !member.permissions.contains(Permissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthentication( format!("You do not have the permissions to bulk edit project {}!", project.inner.title), @@ -1549,6 +1498,14 @@ 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?; @@ -1567,6 +1524,7 @@ 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?; @@ -1585,21 +1543,19 @@ 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, ) .await?; - if user.role.is_mod() - || team_member + if !user.role.is_mod() + && !team_member .map(|x| x.permissions.contains(Permissions::EDIT_DETAILS)) .unwrap_or(false) { @@ -1616,11 +1572,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("")) @@ -1638,6 +1602,7 @@ 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 { @@ -1649,19 +1614,17 @@ pub async fn project_icon_edit( let string = info.into_inner().0; let project_item = - database::models::Project::get_from_slug_or_project_id( - &string, &**pool, - ) - .await? - .ok_or_else(|| { - ApiError::InvalidInput( - "The specified project does not exist!".to_string(), - ) - })?; + database::models::Project::get(&string, &**pool, &redis) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "The specified project does not exist!".to_string(), + ) + })?; 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, ) @@ -1681,7 +1644,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 { @@ -1699,7 +1662,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, @@ -1718,11 +1681,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("")) @@ -1739,24 +1710,23 @@ 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 string = info.into_inner().0; - let project_item = database::models::Project::get_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 !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, ) @@ -1777,7 +1747,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 { @@ -1793,11 +1763,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("")) @@ -1814,12 +1792,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 { @@ -1835,15 +1815,13 @@ pub async fn add_gallery_item( 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(), - ) - })?; + 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( @@ -1927,6 +1905,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("")) @@ -1966,6 +1952,7 @@ 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 string = info.into_inner().0; @@ -1974,19 +1961,17 @@ pub async fn edit_gallery_item( ApiError::Validation(validation_errors_to_string(err, None)) })?; - let project_item = database::models::Project::get_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 !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, ) @@ -2034,7 +2019,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) @@ -2093,6 +2078,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("")) @@ -2109,24 +2102,23 @@ 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 string = info.into_inner().0; - let project_item = database::models::Project::get_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 !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, ) @@ -2183,6 +2175,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("")) @@ -2193,25 +2193,24 @@ 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 string = info.into_inner().0; - let project = database::models::Project::get_from_slug_or_project_id( - &string, &**pool, - ) - .await? - .ok_or_else(|| { - ApiError::InvalidInput( - "The specified project does not exist!".to_string(), - ) - })?; + let project = database::models::Project::get(&string, &**pool, &redis) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "The specified project does not exist!".to_string(), + ) + })?; 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, ) @@ -2235,13 +2234,16 @@ 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_full( + 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("")) @@ -2255,22 +2257,25 @@ 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 string = info.into_inner().0; - let result = database::models::Project::get_from_slug_or_project_id( - &string, &**pool, - ) - .await? - .ok_or_else(|| { - ApiError::InvalidInput( - "The specified project does not exist!".to_string(), - ) - })?; + 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.inner, &Some(user), &pool).await? { + return Ok(HttpResponse::NotFound().body("")); + } let following = sqlx::query!( " @@ -2324,22 +2329,21 @@ 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 string = info.into_inner().0; - let result = database::models::Project::get_from_slug_or_project_id( - &string, &**pool, - ) - .await? - .ok_or_else(|| { - ApiError::InvalidInput( - "The specified project does not exist!".to_string(), - ) - })?; + 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/signing_keys.rs b/src/routes/v2/signing_keys.rs new file mode 100644 index 00000000..fd63637a --- /dev/null +++ b/src/routes/v2/signing_keys.rs @@ -0,0 +1,117 @@ +use crate::models::signing_keys::SigningKey; +use crate::routes::ApiError; +use crate::util::auth::get_user_from_headers; +use crate::{database, models::ids::SigningKeyId}; +use actix_web::{delete, get, web, HttpRequest, HttpResponse}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; + +use database::models::signing_key_item::SigningKey as DBSigningKey; +use database::models::SigningKeyId as DBSigningKeyId; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(keys_get); + cfg.service(keys_delete); + + cfg.service(web::scope("key").service(key_get).service(key_delete)); +} + +#[derive(Serialize, Deserialize)] +pub struct SigningKeyIds { + pub ids: String, +} + +#[get("keys")] +pub async fn keys_get( + web::Query(ids): web::Query, + pool: web::Data, +) -> Result { + let key_ids: Vec = + serde_json::from_str::>(ids.ids.as_str())? + .into_iter() + .map(DBSigningKeyId::from) + .collect(); + + let keys_data: Vec = + DBSigningKey::get_many(&key_ids, &**pool).await?; + + let keys: Vec = + keys_data.into_iter().map(SigningKey::from).collect(); + + Ok(HttpResponse::Ok().json(keys)) +} + +#[get("{id}")] +pub async fn key_get( + info: web::Path<(SigningKeyId,)>, + pool: web::Data, +) -> Result { + let id = info.into_inner().0; + + let Some(key_data) = DBSigningKey::get(id.into(), &**pool).await? else { + return Ok(HttpResponse::NotFound().body("")); + }; + + Ok(HttpResponse::Ok().json(SigningKey::from(key_data))) +} + +#[delete("{id}")] +pub async fn key_delete( + req: HttpRequest, + info: web::Path<(SigningKeyId,)>, + pool: web::Data, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + + let id = info.into_inner().0; + + let Some(data) = DBSigningKey::get(id.into(), &**pool).await? else { + return Ok(HttpResponse::NotFound().body("")); + }; + + if data.owner_id == user.id.into() || user.role.is_admin() { + let mut transaction = pool.begin().await?; + + DBSigningKey::remove(id.into(), &mut transaction).await?; + + transaction.commit().await?; + + Ok(HttpResponse::NoContent().body("")) + } else { + Err(ApiError::CustomAuthentication( + "You are not authorized to delete this key!".to_string(), + )) + } +} + +#[delete("keys")] +pub async fn keys_delete( + req: HttpRequest, + web::Query(ids): web::Query, + pool: web::Data, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + + let key_ids = serde_json::from_str::>(&ids.ids)? + .into_iter() + .map(|x| x.into()) + .collect::>(); + + let mut transaction = pool.begin().await?; + + let keys_data = DBSigningKey::get_many(&key_ids, &**pool).await?; + + let mut keys: Vec = Vec::new(); + + for key in keys_data { + if key.owner_id == user.id.into() || user.role.is_admin() { + keys.push(key.id); + } + } + + DBSigningKey::remove_many(&keys, &mut transaction).await?; + + transaction.commit().await?; + + Ok(HttpResponse::NoContent().body("")) +} diff --git a/src/routes/v2/teams.rs b/src/routes/v2/teams.rs index c2ce692c..67d30e12 100644 --- a/src/routes/v2/teams.rs +++ b/src/routes/v2/teams.rs @@ -31,29 +31,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?; + crate::database::models::Project::get(&string, &**pool, &redis).await?; if let Some(project) = project_data { - let members_data = - TeamMember::get_from_team_full(project.team_id, &**pool).await?; + let members_data = TeamMember::get_from_team_full( + project.inner.team_id, + &**pool, + &redis, + ) + .await?; let current_user = get_user_from_headers(req.headers(), &**pool).await.ok(); if let Some(user) = current_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 @@ -84,18 +82,18 @@ 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?; + TeamMember::get_from_team_full(id.into(), &**pool, &redis).await?; let current_user = get_user_from_headers(req.headers(), &**pool).await.ok(); if let Some(user) = current_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 @@ -126,6 +124,7 @@ pub async fn teams_get( req: HttpRequest, web::Query(ids): web::Query, pool: web::Data, + redis: web::Data, ) -> Result { use itertools::Itertools; @@ -135,26 +134,27 @@ pub async fn teams_get( .collect::>(); let teams_data = - TeamMember::get_from_team_full_many(&team_ids, &**pool).await?; + 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 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| { + 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) }); @@ -163,7 +163,8 @@ pub async fn teams_get( 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)); @@ -178,6 +179,7 @@ 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?; @@ -210,6 +212,8 @@ pub async fn join_team( ) .await?; + TeamMember::clear_cache(team_id, &redis).await?; + transaction.commit().await?; } else { return Err(ApiError::InvalidInput( @@ -247,6 +251,7 @@ 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(); @@ -373,6 +378,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("")) @@ -392,6 +399,7 @@ 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(); @@ -471,6 +479,8 @@ pub async fn edit_team_member( ) .await?; + TeamMember::clear_cache(id, &redis).await?; + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) @@ -487,6 +497,7 @@ 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; @@ -558,6 +569,8 @@ pub async fn transfer_ownership( ) .await?; + TeamMember::clear_cache(id.into(), &redis).await?; + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) @@ -568,6 +581,7 @@ 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(); @@ -625,6 +639,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/users.rs b/src/routes/v2/users.rs index c91a30ff..bff6fe50 100644 --- a/src/routes/v2/users.rs +++ b/src/routes/v2/users.rs @@ -1,13 +1,16 @@ +use crate::database; use crate::database::models::User; use crate::file_hosting::FileHost; use crate::models::notifications::Notification; use crate::models::projects::Project; +use crate::models::signing_keys::SigningKey; 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::keys::SigningKeyBody; 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}; @@ -22,6 +25,8 @@ use std::sync::Arc; use tokio::sync::Mutex; use validator::Validate; +use crate::database::models::signing_key_item::SigningKey as DBSigningKey; + pub fn config(cfg: &mut web::ServiceConfig) { cfg.service(user_auth_get); cfg.service(users_get); @@ -34,6 +39,8 @@ pub fn config(cfg: &mut web::ServiceConfig) { .service(user_edit) .service(user_icon_edit) .service(user_notifications) + .service(user_keys) + .service(user_add_key) .service(user_follows) .service(user_payouts) .service(user_payouts_request), @@ -106,6 +113,7 @@ 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(); @@ -121,13 +129,16 @@ 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 { @@ -546,6 +557,7 @@ pub async fn user_delete( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, removal_type: web::Query, ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await?; @@ -562,7 +574,7 @@ 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? + User::remove_full(id, &mut transaction, &redis).await? } else { User::remove(id, &mut transaction).await? }; @@ -584,6 +596,7 @@ 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 = crate::database::models::User::get_id_from_username_or_id( @@ -616,12 +629,15 @@ 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 { @@ -636,32 +652,122 @@ pub async fn user_notifications( pool: web::Data, ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await?; - let id_option = crate::database::models::User::get_id_from_username_or_id( + let Some(id) = crate::database::models::User::get_id_from_username_or_id( &info.into_inner().0, &**pool, ) - .await?; + .await? else { + return Ok(HttpResponse::NotFound().body("")); + }; + + 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(), + )); + } - if let Some(id) = id_option { - 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(), - )); - } + let mut notifications: Vec = + crate::database::models::notification_item::Notification::get_many_user(id, &**pool) + .await? + .into_iter() + .map(Into::into) + .collect(); + + notifications.sort_by(|a, b| b.created.cmp(&a.created)); + + Ok(HttpResponse::Ok().json(notifications)) +} + +#[get("{id}/keys")] +pub async fn user_keys( + info: web::Path<(String,)>, + pool: web::Data, +) -> Result { + let Some(id) = crate::database::models::User::get_id_from_username_or_id( + &info.into_inner().0, + &**pool, + ) + .await? else { + return Ok(HttpResponse::NotFound().body("")); + }; + + let keys: Vec = + crate::database::models::signing_key_item::SigningKey::get_many_user( + id, &**pool, + ) + .await? + .into_iter() + .map(Into::into) + .collect(); - let mut notifications: Vec = - crate::database::models::notification_item::Notification::get_many_user(id, &**pool) - .await? - .into_iter() - .map(Into::into) - .collect(); + Ok(HttpResponse::Ok().json(keys)) +} + +#[derive(Deserialize)] +pub struct CreateKey { + pub key_type: String, + pub body: String, +} + +#[post("{id}/keys")] +pub async fn user_add_key( + req: HttpRequest, + info: web::Path<(String,)>, + req_body: web::Json, + pool: web::Data, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + let Some(id) = crate::database::models::User::get_id_from_username_or_id( + &info.into_inner().0, + &**pool, + ) + .await? else { + return Ok(HttpResponse::NotFound().body("")); + }; + + if user.id != id.into() { + return Err(ApiError::CustomAuthentication( + "You do not have permission to add keys for this user.".to_string(), + )); + } + + let mut key_body = + SigningKeyBody::parse(&req_body.key_type, &req_body.body)?; + + key_body.scrub(); + + let mut transaction = pool.begin().await?; - notifications.sort_by(|a, b| b.created.cmp(&a.created)); + let result = do_add_key(key_body, id, &mut transaction).await; - Ok(HttpResponse::Ok().json(notifications)) + if result.is_err() { + if let Err(e) = transaction.rollback().await { + return Err(e.into()); + } } else { - Ok(HttpResponse::NotFound().body("")) + transaction.commit().await?; } + + result +} + +pub async fn do_add_key( + body: SigningKeyBody, + owner: database::models::UserId, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result { + let key_id = database::models::generate_signing_key_id(transaction).await?; + let key = DBSigningKey { + id: key_id, + owner_id: owner, + body: body, + created: Utc::now(), + }; + + key.insert(transaction).await?; + + Ok(HttpResponse::NoContent().body("")) } #[derive(Serialize)] diff --git a/src/routes/v2/version_creation.rs b/src/routes/v2/version_creation.rs index 9d037140..230525af 100644 --- a/src/routes/v2/version_creation.rs +++ b/src/routes/v2/version_creation.rs @@ -81,6 +81,7 @@ pub async fn version_create( req: HttpRequest, mut payload: Multipart, client: Data, + redis: web::Data, file_host: Data>, ) -> Result { let mut transaction = client.begin().await?; @@ -90,6 +91,7 @@ pub async fn version_create( req, &mut payload, &mut transaction, + &redis, &***file_host, &mut uploaded_files, ) @@ -118,6 +120,7 @@ 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, ) -> Result { @@ -475,6 +478,7 @@ async fn version_create_inner( 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)) } @@ -486,6 +490,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?; @@ -498,6 +503,7 @@ pub async fn upload_file_to_version( &mut payload, client, &mut transaction, + redis, &***file_host, &mut uploaded_files, version_id, @@ -529,6 +535,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, @@ -540,7 +547,7 @@ async fn upload_file_to_version_inner( let user = get_user_from_headers(req.headers(), &mut *transaction).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, @@ -552,8 +559,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 254dfb72..4e677d58 100644 --- a/src/routes/v2/version_file.rs +++ b/src/routes/v2/version_file.rs @@ -52,6 +52,7 @@ fn default_multiple() -> bool { pub async fn get_version_from_hash( info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, hash_query: web::Query, ) -> Result { let hash = info.into_inner().0.to_lowercase(); @@ -61,9 +62,9 @@ pub async fn get_version_from_hash( 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 != ANY($1) + 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 != ANY($4) + WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ALL($4) ORDER BY v.date_published ASC ", &*crate::models::projects::VersionStatus::iterator() @@ -85,7 +86,8 @@ pub async fn get_version_from_hash( .map(|x| database::models::VersionId(x.version_id)) .collect::>(); let versions_data = - database::models::Version::get_many_full(&version_ids, &**pool).await?; + database::models::Version::get_many(&version_ids, &**pool, &redis) + .await?; if let Some(first) = versions_data.first() { if hash_query.multiple { @@ -123,9 +125,9 @@ pub async fn download_version( " 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 != ANY($1) + 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 != ANY($4) + 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::>(), @@ -153,6 +155,7 @@ 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?; @@ -247,6 +250,12 @@ pub async fn delete_file( .execute(&mut *transaction) .await?; + database::models::Version::clear_cache( + database::models::ids::VersionId(row.version_id), + &redis, + ) + .await?; + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) @@ -265,6 +274,7 @@ pub struct UpdateData { pub async fn get_update_from_hash( info: web::Path<(String,)>, pool: web::Data, + redis: web::Data, hash_query: web::Query, update_data: web::Json, ) -> Result { @@ -278,9 +288,9 @@ pub async fn get_update_from_hash( " 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 != ANY($1) + 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 != ANY($4) + WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ALL($4) ORDER BY v.date_published ASC ", &*crate::models::projects::VersionStatus::iterator() @@ -325,7 +335,7 @@ pub async fn get_update_from_hash( if let Some(version_id) = version_ids.first() { let version_data = - database::models::Version::get_full(*version_id, &**pool) + database::models::Version::get(*version_id, &**pool, &redis) .await?; ok_or_not_found::(version_data) @@ -348,6 +358,7 @@ pub struct FileHashes { #[post("")] pub async fn get_versions_from_hashes( pool: web::Data, + redis: web::Data, file_data: web::Json, ) -> Result { let hashes_parsed: Vec> = file_data @@ -360,9 +371,9 @@ pub async fn get_versions_from_hashes( " 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 != ANY($1) + 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 != ANY($4) + 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(), @@ -377,7 +388,8 @@ pub async fn get_versions_from_hashes( .map(|x| database::models::VersionId(x.version_id)) .collect::>(); let versions_data = - database::models::Version::get_many_full(&version_ids, &**pool).await?; + database::models::Version::get_many(&version_ids, &**pool, &redis) + .await?; let response: Result, ApiError> = result .into_iter() @@ -421,9 +433,9 @@ pub async fn download_files( " 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 != ANY($1) + 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 != ANY($4) + 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(), @@ -461,6 +473,7 @@ pub struct ManyUpdateData { #[post("update")] pub async fn update_files( pool: web::Data, + redis: web::Data, update_data: web::Json, ) -> Result { let hashes_parsed: Vec> = update_data @@ -475,9 +488,9 @@ pub async fn update_files( " 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 != ANY($1) + 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 != ANY($4) + 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(), @@ -532,9 +545,12 @@ pub async fn update_files( } let query_version_ids = version_ids.keys().copied().collect::>(); - let versions = - database::models::Version::get_many_full(&query_version_ids, &**pool) - .await?; + let versions = database::models::Version::get_many( + &query_version_ids, + &**pool, + &redis, + ) + .await?; let mut response = HashMap::new(); diff --git a/src/routes/v2/versions.rs b/src/routes/v2/versions.rs index cd58633c..05b4a14a 100644 --- a/src/routes/v2/versions.rs +++ b/src/routes/v2/versions.rs @@ -36,8 +36,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")] @@ -46,43 +46,52 @@ 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(); 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_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, + 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?; + .await? + .into_iter() + .skip(filters.offset.unwrap_or(0)) + .take(filters.limit.unwrap_or(usize::MAX)) + .filter(|x| { + let mut bool = true; + + 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() @@ -159,11 +168,13 @@ 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?; + let version_data = 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(); @@ -188,6 +199,7 @@ 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)? @@ -195,7 +207,8 @@ pub async fn versions_get( .map(|x| x.into()) .collect::>(); let versions_data = - database::models::Version::get_many_full(&version_ids, &**pool).await?; + database::models::Version::get_many(&version_ids, &**pool, &redis) + .await?; let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); @@ -210,10 +223,11 @@ 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?; + database::models::Version::get(id.into(), &**pool, &redis).await?; let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); @@ -269,6 +283,7 @@ 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?; @@ -280,18 +295,19 @@ 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( + let project_item = 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, + database::models::TeamMember::get_from_user_id_project( + version_item.inner.project_id, user.id.into(), &**pool, ) @@ -638,6 +654,18 @@ pub async fn version_edit( } } + database::models::Version::clear_cache( + version_item.inner.id, + &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 { @@ -661,6 +689,7 @@ 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?; @@ -680,19 +709,19 @@ pub async fn version_schedule( let string = info.into_inner().0; let result = - database::models::Version::get_full(string.into(), &**pool).await?; + 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, + database::models::TeamMember::get_from_user_id_project( + version_item.inner.project_id, user.id.into(), &**pool, ) .await?; - if user.role.is_mod() - || team_member + if !user.role.is_mod() + && !team_member .map(|x| x.permissions.contains(Permissions::EDIT_DETAILS)) .unwrap_or(false) { @@ -701,6 +730,7 @@ pub async fn version_schedule( )); } + let mut transaction = pool.begin().await?; sqlx::query!( " UPDATE versions @@ -711,9 +741,13 @@ 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.inner.id, &redis) + .await?; + transaction.commit().await?; + Ok(HttpResponse::NoContent().body("")) } else { Ok(HttpResponse::NotFound().body("")) @@ -725,13 +759,22 @@ 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 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, ) @@ -756,9 +799,20 @@ 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/search/indexing/local_import.rs b/src/search/indexing/local_import.rs index 2db614c9..4dd4499a 100644 --- a/src/search/indexing/local_import.rs +++ b/src/search/indexing/local_import.rs @@ -27,7 +27,7 @@ pub async fn index_local( FROM mods m LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id - LEFT OUTER JOIN versions v ON v.mod_id = m.id AND v.status != ANY($1) + LEFT OUTER JOIN versions v ON v.mod_id = m.id AND v.status != ALL($1) LEFT OUTER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id LEFT OUTER JOIN game_versions gv ON gvv.game_version_id = gv.id LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id diff --git a/src/util/auth.rs b/src/util/auth.rs index f4b0875b..feea1d48 100644 --- a/src/util/auth.rs +++ b/src/util/auth.rs @@ -22,6 +22,8 @@ pub enum AuthenticationError { Github(#[from] reqwest::Error), #[error("Invalid Authentication Credentials")] InvalidCredentials, + #[error("GitHub Token from incorrect Client ID")] + InvalidClientId, } #[derive(Serialize, Deserialize, Debug)] @@ -37,7 +39,7 @@ pub struct GitHubUser { pub async fn get_github_user_from_token( access_token: &str, ) -> Result { - Ok(reqwest::Client::new() + let response = reqwest::Client::new() .get("https://api.github.com/user") .header(reqwest::header::USER_AGENT, "Modrinth") .header( @@ -45,9 +47,22 @@ pub async fn get_github_user_from_token( format!("token {access_token}"), ) .send() - .await? - .json() - .await?) + .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(github_user) } pub async fn get_user_from_token<'a, 'b, E>( diff --git a/src/util/keys.rs b/src/util/keys.rs new file mode 100644 index 00000000..b31b2f9e --- /dev/null +++ b/src/util/keys.rs @@ -0,0 +1,64 @@ +use std::io::Cursor; + +use pgp::Deserializable; +use thiserror::Error; + +pub enum SigningKeyBody { + PGP(pgp::SignedPublicKey), + SSH(ssh_key::PublicKey), +} + +#[derive(Error, Debug)] +pub enum KeyError { + #[error("Error while deserializing PGP key")] + Pgp(#[from] pgp::errors::Error), + #[error("Error while deserializing SSH key")] + SshKey(#[from] ssh_key::Error), + #[error("`{0}` is not a valid key type")] + UnknownKeyType(String), +} + +impl SigningKeyBody { + pub fn parse( + key_type: &str, + body: &str, + ) -> Result { + match key_type { + "pgp" => { + let cursor = Cursor::new(key_type.as_bytes()); + let key = pgp::SignedPublicKey::from_armor_single(cursor)?.0; + Ok(SigningKeyBody::PGP(key)) + } + "openssh" => { + let key = ssh_key::PublicKey::from_openssh(&body)?; + Ok(SigningKeyBody::SSH(key)) + } + _ => Err(KeyError::UnknownKeyType(key_type.to_string())), + } + } + + pub fn scrub(&mut self) { + match self { + SigningKeyBody::PGP(key) => { + key.details.direct_signatures = vec![]; + } + SigningKeyBody::SSH(key) => key.set_comment(""), + } + } + + pub fn type_str(&self) -> &'static str { + match self { + SigningKeyBody::PGP(..) => "pgp", + SigningKeyBody::SSH(..) => "openssh", + } + } + + pub fn to_body(&self) -> String { + match self { + SigningKeyBody::PGP(key) => { + key.to_armored_string(None).expect("PGP key writing failed") + } + SigningKeyBody::SSH(key) => key.to_string(), + } + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 46456c61..a020cabd 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -3,6 +3,7 @@ pub mod env; pub mod ext; pub mod guards; pub mod img; +pub mod keys; pub mod routes; pub mod validate; pub mod webhook;