diff --git a/Cargo.lock b/Cargo.lock index f7b9b27..92cdb28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "ahash" version = "0.7.6" @@ -22,6 +28,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + [[package]] name = "ansi_term" version = "0.12.1" @@ -55,6 +67,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "ascii-canvas" version = "3.0.0" @@ -64,6 +82,27 @@ dependencies = [ "term", ] +[[package]] +name = "async-stream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171374e7e3b2504e0e5236e3b59260560f9fe94bfe9ac39ba5e4e929c5590625" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "648ed8c8d2ce5409ccd57453d9d1b214b342a0d69376a6feda1fd6cae3299308" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.52" @@ -97,9 +136,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" @@ -127,7 +166,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-util", + "tokio-util 0.6.9", "tower", "tower-http", "tower-layer", @@ -148,6 +187,19 @@ dependencies = [ "mime", ] +[[package]] +name = "bae" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b8de67cc41132507eeece2584804efcb15f85ba516e34c944b7667f480397a" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "base32" version = "0.4.0" @@ -197,7 +249,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.5.2", "constant_time_eq", ] @@ -211,6 +263,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + [[package]] name = "block-padding" version = "0.2.1" @@ -237,9 +298,9 @@ checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.0.72" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" [[package]] name = "cfg-if" @@ -274,7 +335,7 @@ dependencies = [ "memchr", "pin-project-lite", "tokio", - "tokio-util", + "tokio-util 0.6.9", ] [[package]] @@ -285,9 +346,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "core-foundation" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ "core-foundation-sys", "libc", @@ -335,9 +396,9 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b979d76c9fcb84dffc80a73f7290da0f83e4c95773494674cb44b76d13a7a110" +checksum = "4dd435b205a4842da59efd07628f921c096bc1cc0a156835b4fa0bcb9a19bcce" dependencies = [ "cfg-if", "crossbeam-utils", @@ -345,9 +406,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120" +checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" dependencies = [ "cfg-if", "lazy_static", @@ -359,6 +420,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "crypto-mac" version = "0.10.1" @@ -394,6 +465,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", +] + [[package]] name = "dirs" version = "4.0.0" @@ -471,6 +552,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "entity" +version = "0.1.0" +dependencies = [ + "bincode", + "oso", + "redis", + "sea-orm", + "serde", +] + [[package]] name = "fastrand" version = "1.7.0" @@ -480,6 +572,26 @@ dependencies = [ "instant", ] +[[package]] +name = "fieldfilter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbed33889eeeb85517e46db210481cd7404bcdfc1ba4816d011a2f7e4def2ade" +dependencies = [ + "fieldfilter-derive", +] + +[[package]] +name = "fieldfilter-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9912bbdf2e79e87cfd6b5a27e2ce12db39a2898c3fbc51c4f7531a5557e747e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "fixedbitset" version = "0.2.0" @@ -519,9 +631,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" dependencies = [ "futures-channel", "futures-core", @@ -534,9 +646,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", "futures-sink", @@ -544,15 +656,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-executor" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" dependencies = [ "futures-core", "futures-task", @@ -572,15 +684,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-macro" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ "proc-macro2", "quote", @@ -589,21 +701,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-util" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ "futures-channel", "futures-core", @@ -642,9 +754,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9de88456263e249e241fcd211d3954e2c9b0ef7ccfc235a444eb367cae3689" +checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" dependencies = [ "bytes", "fnv", @@ -655,7 +767,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util", + "tokio-util 0.6.9", "tracing", ] @@ -679,9 +791,9 @@ dependencies = [ [[package]] name = "headers" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c84c647447a07ca16f5fbd05b633e535cc41a08d2d74ab1e08648df53be9cb89" +checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" dependencies = [ "base64", "bitflags", @@ -690,7 +802,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha-1", + "sha-1 0.10.0", ] [[package]] @@ -733,7 +845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" dependencies = [ "crypto-mac 0.10.1", - "digest", + "digest 0.9.0", ] [[package]] @@ -743,7 +855,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" dependencies = [ "crypto-mac 0.11.1", - "digest", + "digest 0.9.0", ] [[package]] @@ -787,9 +899,9 @@ checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" [[package]] name = "httparse" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" +checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" [[package]] name = "httpdate" @@ -799,9 +911,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.16" +version = "0.14.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55" +checksum = "043f0e083e9901b6cc658a77d1eb86f4fc650bbb977a4337dd63192826aa85dd" dependencies = [ "bytes", "futures-channel", @@ -812,7 +924,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 0.4.8", + "itoa 1.0.1", "pin-project-lite", "socket2", "tokio", @@ -840,9 +952,9 @@ checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" [[package]] name = "impl-trait-for-tuples" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5dacb10c5b3bb92d46ba347505a9041e676bb20ad220101326bffb0c93031ee" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" dependencies = [ "proc-macro2", "quote", @@ -975,9 +1087,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.113" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9" +checksum = "06e509672465a0504304aa87f9f176f2b2b716ed8fb105ebe5c02dc6dce96a94" [[package]] name = "libreauth" @@ -994,7 +1106,7 @@ dependencies = [ "nom 6.1.2", "pbkdf2", "rust-argon2", - "sha-1", + "sha-1 0.9.8", "sha2", "sha3", "unicode-normalization", @@ -1002,9 +1114,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" dependencies = [ "scopeguard", ] @@ -1047,9 +1159,9 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "matchit" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b6f41fdfbec185dd3dff58b51e323f5bc61692c0de38419a957b0dcfccca3c" +checksum = "9376a4f0340565ad675d11fc1419227faf5f60cd7ac9cb2e7185a471f30af833" [[package]] name = "md-5" @@ -1057,8 +1169,8 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" dependencies = [ - "block-buffer", - "digest", + "block-buffer 0.9.0", + "digest 0.9.0", "opaque-debug", ] @@ -1082,9 +1194,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "0.7.14" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2" dependencies = [ "libc", "log", @@ -1108,9 +1220,10 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", - "bincode", "chrono", "dotenv", + "entity", + "fieldfilter", "lazy_static", "lettre", "libreauth", @@ -1120,9 +1233,9 @@ dependencies = [ "rand", "redis", "regex", + "sea-orm", "serde", "serde_json", - "sqlx", "thiserror", "tokio", "tower", @@ -1199,13 +1312,24 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -1305,6 +1429,30 @@ dependencies = [ "syn", ] +[[package]] +name = "ouroboros" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71643f290d126e18ac2598876d01e1d57aed164afc78fdb6e2a0c6589a1f6662" +dependencies = [ + "aliasable", + "ouroboros_macro", + "stable_deref_trait", +] + +[[package]] +name = "ouroboros_macro" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9a247206016d424fe8497bc611e510887af5c261fbbf977877c4bb55ca4d82" +dependencies = [ + "Inflector", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "oxide-auth" version = "0.5.1" @@ -1388,9 +1536,9 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" dependencies = [ "siphasher", ] @@ -1555,7 +1703,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "tokio", - "tokio-util", + "tokio-util 0.6.9", "url", ] @@ -1646,6 +1794,17 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rust_decimal" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4214023b1223d02a4aad9f0bb9828317634a56530870a2eaf7200a99c0c10f68" +dependencies = [ + "arrayvec 0.7.2", + "num-traits", + "serde", +] + [[package]] name = "rustversion" version = "1.0.6" @@ -1674,11 +1833,97 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sea-orm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd24380b48dacd3ed1c3d467c7b17ffa5818555a2c04066f4a0a9e17d830abc9" +dependencies = [ + "async-stream", + "async-trait", + "chrono", + "futures", + "futures-util", + "once_cell", + "ouroboros", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-strum", + "serde", + "serde_json", + "sqlx", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c199fa8630b1e195d7aef24ce8944af8f4ced67c4eccffd8926453b59f2565a1" +dependencies = [ + "bae", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sea-query" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9088ff96158860a75d98a85a654fdd9d97b10515773af6d87339bfc48258c800" +dependencies = [ + "chrono", + "rust_decimal", + "sea-query-derive", + "serde_json", + "uuid", +] + +[[package]] +name = "sea-query-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34cdc022b4f606353fe5dc85b09713a04e433323b70163e81513b141c6ae6eb5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", + "thiserror", +] + +[[package]] +name = "sea-strum" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391d06a6007842cfe79ac6f7f53911b76dfd69fc9a6769f1cf6569d12ce20e1b" +dependencies = [ + "sea-strum_macros", +] + +[[package]] +name = "sea-strum_macros" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b4397b825df6ccf1e98bcdabef3bbcfc47ff5853983467850eeab878384f21" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "security-framework" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09d3c15d814eda1d6a836f2f2b56a6abc1446c8a34351cb3180d3db92ffe4ce" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" dependencies = [ "bitflags", "core-foundation", @@ -1689,9 +1934,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e90dd10c41c6bfc633da6e0c659bd25d31e0791e5974ac42970267d59eba87f7" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" dependencies = [ "core-foundation-sys", "libc", @@ -1747,23 +1992,34 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if", "cpufeatures", - "digest", + "digest 0.9.0", "opaque-debug", ] +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.3", +] + [[package]] name = "sha2" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if", "cpufeatures", - "digest", + "digest 0.9.0", "opaque-debug", ] @@ -1773,8 +2029,8 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" dependencies = [ - "block-buffer", - "digest", + "block-buffer 0.9.0", + "digest 0.9.0", "keccak", "opaque-debug", ] @@ -1817,9 +2073,9 @@ checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "socket2" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f82496b90c36d70af5fcd482edaa2e0bd16fade569de1330405fecbbdac736b" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ "libc", "winapi", @@ -1884,13 +2140,15 @@ dependencies = [ "log", "md-5", "memchr", + "num-bigint", "once_cell", "parking_lot", "percent-encoding", "rand", + "rust_decimal", "serde", "serde_json", - "sha-1", + "sha-1 0.9.8", "sha2", "smallvec", "sqlformat", @@ -1935,11 +2193,17 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "string_cache" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6" +checksum = "33994d0838dc2d152d17a62adf608a869b5e846b65b389af7f3dbc1de45c5b26" dependencies = [ "lazy_static", "new_debug_unreachable", @@ -2085,9 +2349,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.16.1" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" dependencies = [ "bytes", "libc", @@ -2097,6 +2361,7 @@ dependencies = [ "once_cell", "pin-project-lite", "signal-hook-registry", + "socket2", "tokio-macros", "winapi", ] @@ -2147,18 +2412,32 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64910e1b9c1901aaf5375561e35b9c057d95ff41a44ede043a03e09279eabaf1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5651b5f6860a99bd1adb59dbfe1db8beb433e73709d9032b413a77e2fb7c066a" +checksum = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e" dependencies = [ "futures-core", "futures-util", "pin-project", "pin-project-lite", "tokio", - "tokio-util", + "tokio-util 0.7.0", "tower-layer", "tower-service", "tracing", @@ -2197,9 +2476,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d8d93354fe2a8e50d5953f5ae2e47a3fc2ef03292e7ea46e3cc38f549525fb9" +checksum = "f6c650a8ef0cd2dd93736f033d21cbd1224c5a967aa0c258d00fcf7dafef9b9f" dependencies = [ "cfg-if", "log", @@ -2242,9 +2521,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74786ce43333fcf51efe947aed9718fbe46d5c7328ec3f1029e818083966d9aa" +checksum = "9e0ab7bdc962035a87fba73f3acca9b8a8d0034c2e6f60b84aeaaddddc155dce" dependencies = [ "ansi_term", "lazy_static", @@ -2299,9 +2578,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] name = "unicode-xid" diff --git a/Cargo.toml b/Cargo.toml index 6112a67..8bddff0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,12 +6,16 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace] +members = [".", "entity"] + [dependencies] anyhow = "1.0.53" axum = { version = "0.4.5", features = ["headers", "http2", "multipart"] } -bincode = "1.3.3" chrono = { version = "0.4.19", features = ["serde"] } dotenv = "0.15.0" +entity = { path = "entity" } +fieldfilter = "0.1.0" lazy_static = "1.4.0" lettre = { version = "0.10.0-rc.4", features = [ "tokio1", @@ -32,26 +36,24 @@ redis = { version = "0.21.5", features = [ "connection-manager", ], default-features = false } regex = "1.5.4" +sea-orm = { version = "0.6.0", features = [ + "macros", + "debug-print", + "runtime-tokio-native-tls", + "sqlx-postgres", +], default-features = false } serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.79" -sqlx = { version = "0.5.10", features = [ - "runtime-tokio-native-tls", - "postgres", - "uuid", - "chrono", - "json", - "macros", -] } thiserror = "1.0.30" -tokio = { version = "1.16.1", features = ["rt-multi-thread", "macros", "sync"] } -tower = "0.4.11" +tokio = { version = "1.17.0", features = ["rt-multi-thread", "macros", "sync"] } +tower = "0.4.12" tower-http = { version = "0.2.2", features = [ "add-extension", "trace", "cors", ] } -tracing = "0.1.30" -tracing-subscriber = { version = "0.3.8", features = ["env-filter"] } +tracing = "0.1.31" +tracing-subscriber = { version = "0.3.9", features = ["env-filter"] } ulid = { version = "0.5.0", features = ["serde", "uuid"] } uuid = { version = "0.8.2", features = ["serde", "v4"] } validator = { version = "0.14.0", features = ["derive"] } diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 0000000..4413b2f --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,33 @@ +[tasks.format] +install_crate = "rustfmt" +command = "cargo" +args = ["fmt", "--", "--emit=files"] + +[tasks.clean] +command = "cargo" +args = ["clean"] + +[tasks.build] +command = "cargo" +args = ["build"] +dependencies = ["clean"] + +[tasks.test] +command = "cargo" +args = ["test"] +dependencies = ["clean"] + +[tasks.create-db] +command = "sqlx" +args = ["database", "create"] +workspace = false + +[tasks.drop-db] +command = "sqlx" +args = ["database", "drop"] +workspace = false + +[tasks.generate-entities] +command = "sea-orm-cli" +args = ["generate", "entity", "-o", "entity/src", "--with-serde", "both"] +workspace = false diff --git a/entity/Cargo.toml b/entity/Cargo.toml new file mode 100644 index 0000000..4f3da89 --- /dev/null +++ b/entity/Cargo.toml @@ -0,0 +1,28 @@ + +[package] +name = "entity" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "entity" +path = "src/mod.rs" + +[dependencies] +bincode = "1.3.3" +oso = { git = "https://github.com/fairingrey/oso", branch = "rust-addl-interface", features = [ + "uuid-07", +] } +redis = { version = "0.21.5", features = [ + "aio", + "tokio-comp", + "connection-manager", +], default-features = false } +sea-orm = { version = "0.6.0", features = [ + "macros", + "debug-print", + "runtime-tokio-native-tls", + "sqlx-postgres", +], default-features = false } +serde = { version = "1.0.136", features = ["derive"] } diff --git a/src/macros.rs b/entity/src/macros.rs similarity index 100% rename from src/macros.rs rename to entity/src/macros.rs diff --git a/entity/src/mod.rs b/entity/src/mod.rs new file mode 100644 index 0000000..bd87764 --- /dev/null +++ b/entity/src/mod.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 + +pub mod macros; +pub mod prelude; + +pub mod sea_orm_active_enums; +pub mod user_account; diff --git a/entity/src/prelude.rs b/entity/src/prelude.rs new file mode 100644 index 0000000..c531f11 --- /dev/null +++ b/entity/src/prelude.rs @@ -0,0 +1,3 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 + +pub use super::user_account::Entity as UserAccount; diff --git a/entity/src/sea_orm_active_enums.rs b/entity/src/sea_orm_active_enums.rs new file mode 100644 index 0000000..bf5527c --- /dev/null +++ b/entity/src/sea_orm_active_enums.rs @@ -0,0 +1,24 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 + +use oso::PolarClass; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, PolarClass, +)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "user_role")] +pub enum UserRole { + #[sea_orm(string_value = "admin")] + Admin, + #[sea_orm(string_value = "contributor")] + Contributor, + #[sea_orm(string_value = "creator")] + Creator, + #[sea_orm(string_value = "maintainer")] + Maintainer, + #[sea_orm(string_value = "member")] + Member, + #[sea_orm(string_value = "moderator")] + Moderator, +} diff --git a/entity/src/user_account.rs b/entity/src/user_account.rs new file mode 100644 index 0000000..938b043 --- /dev/null +++ b/entity/src/user_account.rs @@ -0,0 +1,41 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 + +use super::sea_orm_active_enums::UserRole; +use oso::PolarClass; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, PolarClass)] +#[sea_orm(table_name = "user_account")] +#[polar(class_name = "User")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + #[polar(attribute)] + pub id: Uuid, + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, + #[sea_orm(column_type = "Text")] + #[polar(attribute)] + pub name: String, + #[polar(attribute)] + pub email: String, + #[polar(attribute)] + pub role: UserRole, + #[sea_orm(column_type = "Text")] + pub password: String, + #[polar(attribute)] + pub verified: bool, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation {} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + panic!("No RelationDef") + } +} + +impl ActiveModelBehavior for ActiveModel {} + +crate::impl_redis_rv!(Model); diff --git a/migrations/20220211233308_create_user_account.down.sql b/migrations/20220211233308_create_user_account.down.sql new file mode 100644 index 0000000..dad796a --- /dev/null +++ b/migrations/20220211233308_create_user_account.down.sql @@ -0,0 +1,4 @@ +-- Add down migration script here +DROP TABLE IF EXISTS user_account; + +DROP TYPE IF EXISTS user_role; diff --git a/migrations/20220211233308_create_users.up.sql b/migrations/20220211233308_create_user_account.up.sql similarity index 61% rename from migrations/20220211233308_create_users.up.sql rename to migrations/20220211233308_create_user_account.up.sql index 6a3a989..689712f 100644 --- a/migrations/20220211233308_create_users.up.sql +++ b/migrations/20220211233308_create_user_account.up.sql @@ -1,16 +1,16 @@ -- Add up migration script here -CREATE TYPE role AS ENUM ('admin', 'moderator', 'maintainer', 'creator', 'contributor', 'member'); +CREATE TYPE user_role AS ENUM ('admin', 'moderator', 'maintainer', 'creator', 'contributor', 'member'); -CREATE TABLE users ( +CREATE TABLE user_account ( id UUID PRIMARY KEY NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp, updated_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp, name TEXT NOT NULL, email VARCHAR(254) NOT NULL, UNIQUE (name, email), - role role NOT NULL DEFAULT 'member', + role user_role NOT NULL DEFAULT 'member', password TEXT NOT NULL, verified BOOLEAN NOT NULL DEFAULT FALSE ); -SELECT manage_updated_at('users'); +SELECT manage_updated_at('user_account'); diff --git a/migrations/20220211233308_create_users.down.sql b/migrations/20220211233308_create_users.down.sql deleted file mode 100644 index f384ada..0000000 --- a/migrations/20220211233308_create_users.down.sql +++ /dev/null @@ -1,4 +0,0 @@ --- Add down migration script here -DROP TABLE IF EXISTS users; - -DROP TYPE IF EXISTS role; diff --git a/polar/users.polar b/polar/users.polar index a710560..63bd806 100644 --- a/polar/users.polar +++ b/polar/users.polar @@ -1,55 +1,46 @@ # User rules ## admins and mods can read all of a user's fields except passwords -allow_field(user: User, "READ", _other_user: User, field) if +allow_field(user: User, _: Read, _other_user: User, field) if user.role in [Role::Admin, Role::Moderator] and - field in ["name", "created_at", "updated_at", "role", "email"]; + field in ["created_at", "updated_at", "name", "email", "role"]; ## users can read all of their own fields except passwords -allow_field(user: User, "READ", other_user: User, field) if +allow_field(user: User, _: Read, other_user: User, field) if user.id == other_user.id and - field in ["name", "created_at", "updated_at", "role", "email"]; + field in ["created_at", "updated_at", "name", "email", "role"]; -## anyone can read ids, names, created_at, and role of other users -allow_field(_, "READ", _other_user: User, field: String) if - field in ["name", "created_at", "role"]; +## anyone can read names, created_at, and role of other users +allow_field(_, _: Read, _other_user: User, field: String) if + field in ["created_at", "name", "role"]; -## admins can change user names or emails -allow_field(user: User, "UPDATE", _other_user: User, field: String) if +## admins can change everything for a user except the password +allow(user: User, update: UpdateUser, _other_user: User) if user.role = Role::Admin and - field in ["name", "email"]; + update.password = nil; ## moderators can do the same but only to other users of role below them -allow_field(user: User, "UPDATE", other_user: User, field: String) if +## they cannot assign roles higher than or equal to themselves +allow(user: User, update: UpdateUser, other_user: User) if user.role = Role::Moderator and other_user.role in [Role::Maintainer, Role::Creator, Role::Contributor, Role::Member] and - field in ["name", "email"]; + update.role in [Role::Maintainer, Role::Creator, Role::Contributor, Role::Member, nil] and + update.password = nil; + +## users can update themselves but not their role +allow(user: User, update: UpdateUser, other_user: User) if + user.id = other_user.id and + changes.role = nil; ## admins can delete other users -allow(user: User, "DELETE", _other_user: User) if +allow(user: User, _: Delete, _other_user: User) if user.role = Role::Admin; ## moderators can also, but again only to other users of role below them -allow(user: User, "DELETE", other_user: User) if +allow(user: User, _: Delete, other_user: User) if user.role = Role::Moderator and other_user.role in [Role::Maintainer, Role::Creator, Role::Contributor, Role::Member]; -# role specific stuff - -## admins can assign any role -allow_assign_role(user: User, _role: Role) if - user.role = Role::Admin; - -## Moderators can only assign roles below them -allow_assign_role(user: User, role: Role) if - user.role = Role::Moderator and - role in [Role::Maintainer, Role::Creator, Role::Contributor, Role::Member]; - -## users can update themselves but only on certain fields -allow_field(user: User, "UPDATE", other_user: User, field) if - field in ["name", "email", "password"] and - user.id = other_user.id; - ## users can delete themselves -allow(user: User, "DELETE", other_user: User) if +allow(user: User, _: Delete, other_user: User) if user.id = other_user.id; diff --git a/src/actions.rs b/src/actions.rs new file mode 100644 index 0000000..32c087b --- /dev/null +++ b/src/actions.rs @@ -0,0 +1,61 @@ +//! CRUD action-like resources +use anyhow::Result; +use entity::sea_orm_active_enums::UserRole; +use oso::{Oso, PolarClass}; +use serde::Deserialize; +use validator::Validate; + +use crate::constants::RE_USERNAME; + +/// The "READ" action. Because there is no data pertinent to this action it is a unit struct. +#[derive(Debug, Clone, Copy, PolarClass)] +pub(crate) struct Read; + +/// The "DELETE" action. Because there is no data pertinent to this action it is a unit struct. +#[derive(Debug, Clone, Copy, PolarClass)] +pub(crate) struct Delete; + +/// The action by which a user is updated. Can be understood as a sort of changeset. +/// +/// This struct in particular doubles up for multiple use cases. It's used for PUT `/user/:id` form responses, +/// in authorization rules, and also for updates to the ORM. +#[derive(Debug, Clone, Validate, Deserialize, PolarClass)] +pub(crate) struct UpdateUser { + #[validate( + length( + min = 5, + max = 32, + message = "Minimum length is 5 characters, maximum is 32" + ), + regex( + path = "RE_USERNAME", + message = "Can only contain letters, numbers, dashes (-), periods (.), and underscores (_)" + ) + )] + #[polar(attribute)] + pub(crate) name: Option, + #[validate(email(message = "Must be a valid email address."))] + #[polar(attribute)] + pub(crate) email: Option, + #[polar(attribute)] + pub(crate) role: Option, +} + +/// Attempt to create a new oso instance for managing authorization schemes. +pub(crate) fn try_register_oso() -> Result { + let mut oso = Oso::new(); + + // NOTE: load classes here + oso.register_class(entity::user_account::Model::get_polar_class())?; + oso.register_class(UserRole::get_polar_class())?; + + // action classes in this module should be loaded here too + oso.register_class(Read::get_polar_class())?; + oso.register_class(Delete::get_polar_class())?; + oso.register_class(UpdateUser::get_polar_class())?; + + // NOTE: load oso rule files here + oso.load_files(vec!["polar/users.polar"])?; + + Ok(oso) +} diff --git a/src/auth.rs b/src/auth.rs index c0fce96..1649084 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -8,7 +8,6 @@ use redis::AsyncCommands; use crate::{ constants::{SESSION_COOKIE_NAME, SESSION_DURATION_SECS, SESSION_KEY_PREFIX}, error::MixiniError, - models::User, server::State, }; @@ -18,7 +17,7 @@ use crate::{ /// with the value being the unprefixed key. #[derive(Debug)] pub(crate) enum Auth { - KnownUser(User), + KnownUser(entity::user_account::Model), UnknownUser, } diff --git a/src/constants.rs b/src/constants.rs index a3cc699..e3873ee 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,10 +1,14 @@ //! Constants use lazy_static::lazy_static; +use regex::Regex; lazy_static! { pub(crate) static ref DOMAIN: String = std::env::var("DOMAIN").expect("DOMAIN is not set in env"); + pub(crate) static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9\.\-_]+$").unwrap(); + pub(crate) static ref RE_PASSWORD: Regex = + Regex::new(r"^[a-zA-Z0-9]*[0-9][a-zA-Z0-9]*$").unwrap(); } // for authorized sessions diff --git a/src/error.rs b/src/error.rs index 674e41b..343e041 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,9 +13,6 @@ pub(crate) enum MixiniError { #[error(transparent)] AxumFormRejection(#[from] axum::extract::rejection::FormRejection), - #[error(transparent)] - BincodeError(#[from] bincode::Error), - #[error(transparent)] JsonError(#[from] serde_json::Error), @@ -29,10 +26,10 @@ pub(crate) enum MixiniError { RedisError(#[from] redis::RedisError), #[error(transparent)] - SmtpError(#[from] lettre::transport::smtp::Error), + DatabaseError(#[from] sea_orm::DbErr), #[error(transparent)] - SqlxError(#[from] sqlx::Error), + SmtpError(#[from] lettre::transport::smtp::Error), #[error(transparent)] ValidationError(#[from] validator::ValidationErrors), @@ -45,13 +42,6 @@ impl IntoResponse for MixiniError { fn into_response(self) -> Response { match self { MixiniError::AxumFormRejection(_) => (StatusCode::BAD_REQUEST, self.to_string()), - MixiniError::BincodeError(e) => { - tracing::debug!("Bincode error occurred: {:?}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - INTERNAL_SERVER_ERROR_MESSAGE.into(), - ) - } MixiniError::JsonError(e) => { tracing::debug!("Json error occurred: {:?}", e); ( @@ -87,10 +77,10 @@ impl IntoResponse for MixiniError { INTERNAL_SERVER_ERROR_MESSAGE.into(), ) } - MixiniError::SqlxError(ref e) => match e { - sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, self.to_string()), + MixiniError::DatabaseError(ref e) => match e { + sea_orm::DbErr::RecordNotFound(_) => (StatusCode::NOT_FOUND, self.to_string()), _ => { - tracing::debug!("Sqlx error occurred: {:?}", e); + tracing::debug!("SeaORM error occurred: {:?}", e); ( StatusCode::INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR_MESSAGE.into(), diff --git a/src/handlers/login.rs b/src/handlers/login.rs index d3258e8..4187899 100644 --- a/src/handlers/login.rs +++ b/src/handlers/login.rs @@ -4,17 +4,21 @@ use axum::{ headers::Cookie, http::{header, Response, StatusCode}, }; +use entity::{prelude::*, user_account}; use libreauth::pass::HashBuilder; use redis::AsyncCommands; +use sea_orm::{entity::*, prelude::*}; use serde::Deserialize; use std::sync::Arc; use validator::Validate; use crate::{ - constants::{DOMAIN, SESSION_COOKIE_NAME, SESSION_DURATION_SECS, SESSION_KEY_PREFIX}, + constants::{ + DOMAIN, RE_PASSWORD, RE_USERNAME, SESSION_COOKIE_NAME, SESSION_DURATION_SECS, + SESSION_KEY_PREFIX, + }, error::MixiniError, - handlers::{ValidatedForm, RE_PASSWORD, RE_USERNAME}, - models::User, + handlers::ValidatedForm, server::State, utils::{ pass::{HASHER, PWD_SCHEME_VERSION}, @@ -53,23 +57,25 @@ pub(crate) struct LoginForm { /// Handler for `POST /login` pub(crate) async fn login( - ValidatedForm(input): ValidatedForm, + ValidatedForm(login): ValidatedForm, state: Extension>, ) -> Result, MixiniError> { - let mut db_conn = state.db_pool.acquire().await?; - - let user = sqlx::query_as!( - User, - r#"SELECT id, created_at, updated_at, name, email, role as "role:_", password, verified - FROM users WHERE name = $1"#, - input.name - ) - .fetch_one(&mut db_conn) - .await?; + let user = if let Some(user) = UserAccount::find() + .filter(user_account::Column::Name.eq(login.name)) + .one(&state.db) + .await? + { + user + } else { + return Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::empty()) + .unwrap()); + }; let checker = HashBuilder::from_phc(&user.password).unwrap(); - if !checker.is_valid(&input.password) { + if !checker.is_valid(&login.password) { return Ok(Response::builder() .status(StatusCode::UNAUTHORIZED) .body(Body::empty()) @@ -77,15 +83,10 @@ pub(crate) async fn login( }; if checker.needs_update(Some(PWD_SCHEME_VERSION)) { // password needs to be updated - let hashed_password = HASHER.hash(&input.password).expect("hasher failed hashing"); - sqlx::query_as!( - User, - r#"UPDATE users SET password = $2 WHERE id = $1"#, - user.id, - hashed_password - ) - .execute(&mut db_conn) - .await?; + let hashed_password = HASHER.hash(&login.password).expect("hasher failed hashing"); + let mut user: user_account::ActiveModel = user.clone().into(); + user.password = Set(hashed_password); + user.update(&state.db).await?; } // create session entry in redis diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index de8abed..25fc62b 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -4,8 +4,6 @@ use axum::{ extract::{Form, FromRequest, RequestParts}, BoxError, }; -use lazy_static::lazy_static; -use regex::Regex; use serde::de::DeserializeOwned; use validator::Validate; @@ -17,12 +15,6 @@ pub(crate) mod user; pub(crate) use login::*; pub(crate) use user::*; -lazy_static! { - pub(crate) static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9\.\-_]+$").unwrap(); - pub(crate) static ref RE_PASSWORD: Regex = - Regex::new(r"^[a-zA-Z0-9]*[0-9][a-zA-Z0-9]*$").unwrap(); -} - /// A validated form with some input. #[derive(Debug, Clone, Copy, Default)] pub(crate) struct ValidatedForm(pub(crate) T); diff --git a/src/handlers/user.rs b/src/handlers/user.rs index 459cfb3..4ecc9c4 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -5,8 +5,10 @@ use axum::{ headers::Cookie, http::{Response, StatusCode}, }; -use chrono::{DateTime, Utc}; +use entity::{prelude::*, sea_orm_active_enums::UserRole, user_account}; +use fieldfilter::FieldFilterable; use redis::AsyncCommands; +use sea_orm::{entity::*, prelude::*, query::*}; use serde::{Deserialize, Serialize}; use std::{collections::HashSet, sync::Arc}; use ulid::Ulid; @@ -14,20 +16,21 @@ use uuid::Uuid; use validator::Validate; use crate::{ + actions::{Delete, Read, UpdateUser}, auth::Auth, constants::{ - SESSION_COOKIE_NAME, SESSION_KEY_PREFIX, VERIFY_EXPIRY_SECONDS, VERIFY_KEY_PREFIX, + RE_PASSWORD, RE_USERNAME, SESSION_COOKIE_NAME, SESSION_KEY_PREFIX, VERIFY_EXPIRY_SECONDS, + VERIFY_KEY_PREFIX, }, error::MixiniError, - handlers::{ValidatedForm, RE_PASSWORD, RE_USERNAME}, - models::{Role, User}, + handlers::ValidatedForm, server::State, utils::{mail::send_email_verification_request, pass::HASHER, RKeys}, }; /// The form input for `POST /user` #[derive(Debug, Validate, Deserialize)] -pub(crate) struct CreateUserForm { +pub(crate) struct CreateUser { /// The provided username. #[validate( length( @@ -59,38 +62,6 @@ pub(crate) struct CreateUserForm { pub(crate) password: String, } -/// The form input for `PUT /user/:id` -#[derive(Debug, Validate, Deserialize)] -pub(crate) struct UpdateUserForm { - #[validate( - length( - min = 5, - max = 32, - message = "Minimum length is 5 characters, maximum is 32" - ), - regex( - path = "RE_USERNAME", - message = "Can only contain letters, numbers, dashes (-), periods (.), and underscores (_)" - ) - )] - pub(crate) name: Option, - #[validate(email(message = "Must be a valid email address."))] - pub(crate) email: Option, - pub(crate) role: Option, - #[validate( - length( - min = 8, - max = 128, - message = "Minimum length is 8 characters, maximum is 128" - ), - regex( - path = "RE_PASSWORD", - message = "Must be alphanumeric and contain at least one number." - ) - )] - pub(crate) password: Option, -} - /// The form input for `PUT /user/verify` #[derive(Debug, Validate, Deserialize)] pub(crate) struct VerifyForm { @@ -101,63 +72,58 @@ pub(crate) struct VerifyForm { pub(crate) key: String, } -/// The response output for `GET /user/:name` -#[derive(Debug, Serialize)] -pub(crate) struct UserResponse { +/// The response for `GET /user/:id` +#[derive(Debug, Serialize, FieldFilterable)] +#[field_filterable_on(user_account::Model)] +pub(crate) struct GetUserResponse { id: Uuid, - created_at: Option>, - updated_at: Option>, - name: String, + created_at: Option, + updated_at: Option, + name: Option, email: Option, - role: Role, + role: Option, } /// Handler for `POST /user` pub(crate) async fn create_user( - ValidatedForm(input): ValidatedForm, + ValidatedForm(create_user): ValidatedForm, state: Extension>, ) -> Result, MixiniError> { // check if either this username or email already exist in our database - let mut db_conn = state.db_pool.acquire().await?; - - // shadow - let (name, email, password) = (input.name, input.email, input.password); - - let conflicts = sqlx::query!( - r#"SELECT id FROM users WHERE name = $1 OR email = $2"#, - name, - email, - ) - .fetch_optional(&mut db_conn) - .await?; + let conflicts = UserAccount::find() + .filter( + Condition::any() + .add(user_account::Column::Name.eq(create_user.name.to_owned())) + .add(user_account::Column::Email.eq(create_user.email.to_owned())), + ) + .all(&state.db) + .await?; - if conflicts.is_some() { - let res = Response::builder() + if !conflicts.is_empty() { + Ok(Response::builder() .status(StatusCode::CONFLICT) .body(Body::from("A user with this name or email already exists.")) - .unwrap(); - return Ok(res); + .unwrap()) + } else { + // create new user account in db + let id = Uuid::from(Ulid::new()); + let password = HASHER + .hash(&create_user.password) + .expect("hasher failed hashing"); + + let new_account = user_account::ActiveModel { + id: Set(id), + name: Set(create_user.name), + email: Set(create_user.email), + password: Set(password), + ..Default::default() + }; + new_account.insert(&state.db).await?; + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::empty()) + .unwrap()) } - - // create new user in db - let id = Uuid::from(Ulid::new()); - let password = HASHER.hash(&password).expect("hasher failed hashing"); - - sqlx::query_as!( - User, - r#"INSERT INTO users (id, name, email, password) VALUES ($1, $2, $3, $4)"#, - id, - name, - email, - password, - ) - .execute(&mut db_conn) - .await?; - - Ok(Response::builder() - .status(StatusCode::OK) - .body(Body::empty()) - .unwrap()) } /// Handler for `GET /user/:id` @@ -166,59 +132,29 @@ pub(crate) async fn get_user( state: Extension>, auth: Auth, ) -> Result, MixiniError> { - let mut db_conn = state.db_pool.acquire().await?; + let maybe_user = UserAccount::find_by_id(id).one(&state.db).await?; - match sqlx::query_as!( - User, - r#"SELECT id, created_at, updated_at, name, email, role as "role:_", password, verified - FROM users WHERE id = $1"#, - id - ) - .fetch_optional(&mut db_conn) - .await? - { + match maybe_user { Some(user) => { let authorized_fields: HashSet = if let Auth::KnownUser(this_user) = auth { state .oso .lock() .await - .authorized_fields(this_user, "READ", user.to_owned())? + .authorized_fields(this_user, Read, user.to_owned())? } else { state .oso .lock() .await - .authorized_fields("guest", "READ", user.to_owned())? + .authorized_fields("guest", Read, user.to_owned())? }; - let created_at = if authorized_fields.contains("created_at") { - Some(user.created_at) - } else { - None - }; - let updated_at = if authorized_fields.contains("updated_at") { - Some(user.updated_at) - } else { - None - }; - let email = if authorized_fields.contains("email") { - Some(user.email) - } else { - None - }; + let res_body: GetUserResponse = FieldFilterable::field_filter(user, authorized_fields); - let user_response = UserResponse { - id: user.id, - created_at, - updated_at, - name: user.name, - email, - role: user.role, - }; Ok(Response::builder() .status(StatusCode::OK) - .body(Body::from(serde_json::to_vec(&user_response)?)) + .body(Body::from(serde_json::to_vec(&res_body)?)) .unwrap()) } None => Ok(Response::builder() @@ -231,80 +167,49 @@ pub(crate) async fn get_user( /// Handler for `PUT /user/:id` pub(crate) async fn update_user( Path(id): Path, - ValidatedForm(input): ValidatedForm, + ValidatedForm(update_user): ValidatedForm, state: Extension>, auth: Auth, ) -> Result, MixiniError> { match auth { Auth::KnownUser(this_user) => { - let mut db_conn = state.db_pool.acquire().await?; - - let user = if let Some(user) = sqlx::query_as!( - User, - r#"SELECT id, created_at, updated_at, name, email, role as "role:_", password, verified - FROM users WHERE id = $1"#, - id - ) - .fetch_optional(&mut db_conn) - .await? { user } else { - return Ok(Response::builder() - .status(StatusCode::FORBIDDEN) - .body(Body::empty()) - .unwrap()); - }; - - let mut values = Vec::new(); + let user = if let Some(user) = UserAccount::find_by_id(id).one(&state.db).await? { + user + } else { + return Ok(Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::empty()) + .unwrap()); + }; - if let Some(role) = input.role { - if state - .oso - .lock() - .await - .query_rule("allow_assign_role", (this_user.to_owned(), role))? - .next() - .is_some() - { - values.push(format!("role = '{}'", role)); - } else { - return Ok(Response::builder() - .status(StatusCode::FORBIDDEN) - .body(Body::empty()) - .unwrap()); + if state.oso.lock().await.is_allowed( + this_user, + update_user.to_owned(), + user.to_owned(), + )? { + // TODO: When UpdateUser -> ActiveModel works, change this + // https://github.com/SeaQL/sea-orm/issues/547 + let mut user: user_account::ActiveModel = user.into(); + if let Some(name) = update_user.name { + user.name = Set(name); } - } - - let authorized_fields: HashSet = - state - .oso - .lock() - .await - .authorized_fields(this_user, "READ", user.to_owned())?; - - if let Some(name) = input.name { - if authorized_fields.contains("name") { - values.push(format!("name = {}", name)); + if let Some(email) = update_user.email { + user.email = Set(email); } - } - - if let Some(email) = input.email { - if authorized_fields.contains("email") { - values.push(format!("email = {}", email)); + if let Some(role) = update_user.role { + user.role = Set(role); } + user.update(&state.db).await?; + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::empty()) + .unwrap()) + } else { + Ok(Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::empty()) + .unwrap()) } - - let values = values.join(","); - - // TODO: Requires testing, this is a dynamic query - sqlx::query(r#"UPDATE users SET $2 WHERE id = $1"#) - .bind(id) - .bind(values) - .execute(&mut db_conn) - .await?; - - Ok(Response::builder() - .status(StatusCode::OK) - .body(Body::empty()) - .unwrap()) } Auth::UnknownUser => Ok(Response::builder() .status(StatusCode::UNAUTHORIZED) @@ -322,16 +227,9 @@ pub(crate) async fn delete_user( ) -> Result, MixiniError> { match auth { Auth::KnownUser(this_user) => { - let mut db_conn = state.db_pool.acquire().await?; - - let user = if let Some(user) = sqlx::query_as!( - User, - r#"SELECT id, created_at, updated_at, name, email, role as "role:_", password, verified - FROM users WHERE id = $1"#, - id - ) - .fetch_optional(&mut db_conn) - .await? { user } else { + let user = if let Some(user) = UserAccount::find_by_id(id).one(&state.db).await? { + user + } else { return Ok(Response::builder() .status(StatusCode::FORBIDDEN) .body(Body::empty()) @@ -342,11 +240,9 @@ pub(crate) async fn delete_user( .oso .lock() .await - .is_allowed(this_user, "DELETE", user.to_owned())? + .is_allowed(this_user, Delete, user.to_owned())? { - sqlx::query!(r#"DELETE FROM users WHERE id = $1"#, user.id) - .execute(&mut db_conn) - .await?; + user.delete(&state.db).await?; // also delete cookie in store let base_key = cookie.get(SESSION_COOKIE_NAME).expect("cookie monster!?"); @@ -417,26 +313,30 @@ pub(crate) async fn create_verify_user( /// Handler for `PUT /user/verify` pub(crate) async fn update_verify_user( - ValidatedForm(input): ValidatedForm, + ValidatedForm(verify): ValidatedForm, state: Extension>, ) -> Result, MixiniError> { // value is user id - let prefixed_key = format!("{}{}", VERIFY_KEY_PREFIX, &input.key); + let prefixed_key = format!("{}{}", VERIFY_KEY_PREFIX, &verify.key); let maybe_id: Option = state.redis_manager.to_owned().get(&prefixed_key).await?; match maybe_id { Some(id) => { let id: Uuid = Uuid::parse_str(&id).map_err(|e| format_err!(e))?; - let mut db_conn = state.db_pool.acquire().await?; + // NOTE: Normally this should always be Some(user) but better safe than sorry + let user = if let Some(user) = UserAccount::find_by_id(id).one(&state.db).await? { + user + } else { + return Ok(Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::empty()) + .unwrap()); + }; + let mut user: user_account::ActiveModel = user.into(); - sqlx::query_as!( - User, - r#"UPDATE users SET verified = TRUE WHERE id = $1"#, - id - ) - .execute(&mut db_conn) - .await?; + user.verified = Set(true); + user.update(&state.db).await?; Ok(Response::builder() .status(StatusCode::OK) diff --git a/src/main.rs b/src/main.rs index cf2edb3..5977df9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,12 +6,11 @@ rust_2021_compatibility )] +pub(crate) mod actions; pub(crate) mod auth; pub(crate) mod constants; pub(crate) mod error; pub(crate) mod handlers; -pub(crate) mod macros; -pub(crate) mod models; pub(crate) mod server; pub(crate) mod utils; diff --git a/src/models/mod.rs b/src/models/mod.rs deleted file mode 100644 index b3e3c5c..0000000 --- a/src/models/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Database models -//! -//! Note that these may have to be updated by hand. - -pub(crate) mod user; - -pub(crate) use user::*; diff --git a/src/models/user.rs b/src/models/user.rs deleted file mode 100644 index 7da53e0..0000000 --- a/src/models/user.rs +++ /dev/null @@ -1,79 +0,0 @@ -use serde::{Deserialize, Serialize}; -use sqlx::types::chrono::{DateTime, Utc}; -use std::fmt; -use uuid::Uuid; - -use crate::{handlers::UpdateUserForm, impl_redis_rv}; - -/// User roles -#[derive( - Debug, Clone, Copy, Eq, PartialEq, sqlx::Type, oso::PolarClass, Serialize, Deserialize, -)] -#[sqlx(type_name = "role", rename_all = "lowercase")] -pub(crate) enum Role { - Admin, - Moderator, - Maintainer, - Creator, - Contributor, - Member, -} - -impl fmt::Display for Role { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = format!("{:?}", self).to_ascii_lowercase(); - write!(f, "{}", s) - } -} - -/// User model -#[derive(Debug, Clone, sqlx::FromRow, oso::PolarClass, Serialize, Deserialize)] -pub(crate) struct User { - #[polar(attribute)] - pub(crate) id: Uuid, - pub(crate) created_at: DateTime, - pub(crate) updated_at: DateTime, - #[polar(attribute)] - pub(crate) name: String, - #[polar(attribute)] - pub(crate) email: String, - #[polar(attribute)] - pub(crate) role: Role, - /// The password in hashed PHC form, as represented in the database - #[polar(attribute)] - pub(crate) password: String, - #[polar(attribute)] - pub(crate) verified: bool, -} - -/// User model but all fields are options -#[derive(Debug, Clone, oso::PolarClass, Serialize, Deserialize)] -pub(crate) struct UserOptional { - #[polar(attribute)] - pub(crate) name: Option, - pub(crate) created_at: Option>, - pub(crate) updated_at: Option>, - #[polar(attribute)] - pub(crate) email: Option, - #[polar(attribute)] - pub(crate) role: Option, - #[polar(attribute)] - pub(crate) password: Option, - pub(crate) verified: Option, -} - -impl From for UserOptional { - fn from(form: UpdateUserForm) -> Self { - Self { - name: form.name, - email: form.email, - created_at: None, - updated_at: None, - role: form.role, - password: form.password, - verified: None, - } - } -} - -impl_redis_rv!(User, Role); diff --git a/src/server.rs b/src/server.rs index b0bbac8..79b502d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -7,8 +7,8 @@ use lettre::{ transport::smtp::authentication::{Credentials, Mechanism}, AsyncSmtpTransport, Tokio1Executor, }; -use oso::{Oso, PolarClass}; -use sqlx::PgPool; +use oso::Oso; +use sea_orm::{Database, DatabaseConnection}; use std::{str::FromStr, sync::Arc}; use tokio::sync::Mutex; use tower::ServiceBuilder; @@ -17,12 +17,12 @@ use tower_http::{ trace::TraceLayer, }; -use crate::handlers; +use crate::{actions::try_register_oso, handlers}; #[derive(Clone)] pub(crate) struct State { pub(crate) oso: Arc>, - pub(crate) db_pool: PgPool, + pub(crate) db: DatabaseConnection, pub(crate) redis_manager: redis::aio::ConnectionManager, pub(crate) mailsender: AsyncSmtpTransport, } @@ -31,7 +31,7 @@ impl State { /// Attempt to create a new State instance pub(crate) async fn try_new() -> Result { let oso = Arc::new(Mutex::new(try_register_oso()?)); - let db_pool = PgPool::connect(&std::env::var("DATABASE_URL")?).await?; + let db = Database::connect(&std::env::var("DATABASE_URL")?).await?; let redis_manager = redis::Client::open(std::env::var("REDIS_URL")?)? .get_tokio_connection_manager() .await?; @@ -48,29 +48,13 @@ impl State { Ok(State { oso, - db_pool, + db, redis_manager, mailsender, }) } } -/// Attempt to create a new oso instance for managing authorization schemes. -fn try_register_oso() -> Result { - use crate::models::*; - - let mut oso = Oso::new(); - - // NOTE: load classes here - oso.register_class(User::get_polar_class())?; - oso.register_class(Role::get_polar_class())?; - - // NOTE: load oso rule files here - oso.load_files(vec!["polar/users.polar"])?; - - Ok(oso) -} - /// Attempt to setup the CORS layer. fn try_cors_layer() -> Result { use axum::http::Method;