diff --git a/Cargo.lock b/Cargo.lock index 1d0587a..50e20d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,7 +35,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", "once_cell", "version_check", "zerocopy", @@ -141,6 +140,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "arrayvec" version = "0.7.6" @@ -298,7 +303,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -315,7 +320,7 @@ checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -465,13 +470,16 @@ dependencies = [ [[package]] name = "bigdecimal" -version = "0.3.1" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" +checksum = "8f850665a0385e070b64c38d2354e6c104c8479c59868d1e48a0c13ee2c7a1c1" dependencies = [ + "autocfg", + "libm", "num-bigint", "num-integer", "num-traits", + "serde", ] [[package]] @@ -547,7 +555,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", "syn_derive", ] @@ -689,7 +697,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -715,14 +723,13 @@ dependencies = [ [[package]] name = "config" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" dependencies = [ "async-trait", "convert_case 0.6.0", "json5", - "lazy_static", "nom", "pathdiff", "ron", @@ -730,7 +737,7 @@ dependencies = [ "serde", "serde_json", "toml", - "yaml-rust", + "yaml-rust2", ] [[package]] @@ -835,6 +842,40 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.85", +] + [[package]] name = "der" version = "0.7.9" @@ -866,7 +907,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -903,24 +944,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] -name = "educe" -version = "0.5.11" +name = "either" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4bd92664bf78c4d3dba9b7cdafce6fa15b13ed3ed16175218196942e99168a8" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 2.0.77", + "serde", ] [[package]] -name = "either" -version = "1.13.0" +name = "encoding_rs" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "serde", + "cfg-if", ] [[package]] @@ -934,26 +972,6 @@ dependencies = [ "slug", ] -[[package]] -name = "enum-ordinalize" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" -dependencies = [ - "enum-ordinalize-derive", -] - -[[package]] -name = "enum-ordinalize-derive" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -1126,7 +1144,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1215,12 +1233,6 @@ dependencies = [ "ahash 0.7.8", ] -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" - [[package]] name = "hashbrown" version = "0.14.5" @@ -1240,6 +1252,15 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "headers" version = "0.4.0" @@ -1269,9 +1290,6 @@ name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] [[package]] name = "heck" @@ -1429,6 +1447,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1457,7 +1481,7 @@ checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1475,6 +1499,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -1548,21 +1581,15 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libsqlite3-sys" -version = "0.27.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", "vcpkg", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1769,19 +1796,19 @@ dependencies = [ [[package]] name = "ordered-multimap" -version = "0.6.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ "dlv-list", - "hashbrown 0.13.2", + "hashbrown 0.14.5", ] [[package]] name = "ouroboros" -version = "0.17.2" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2ba07320d39dfea882faa70554b4bd342a5f273ed59ba7c1c6b4c840492c954" +checksum = "944fa20996a25aded6b4795c6d63f10014a7a83f8be9828a11860b08c5fc4a67" dependencies = [ "aliasable", "ouroboros_macro", @@ -1790,15 +1817,16 @@ dependencies = [ [[package]] name = "ouroboros_macro" -version = "0.17.2" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec4c6225c69b4ca778c0aea097321a64c421cf4577b331c61b229267edabb6f8" +checksum = "39b0deead1528fd0e5947a8546a9642a9777c25f6e1e26f34c97b204bbb465bd" dependencies = [ "heck 0.4.1", - "proc-macro-error", + "itertools", "proc-macro2", + "proc-macro2-diagnostics", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1904,7 +1932,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1935,7 +1963,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -2060,6 +2088,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", + "version_check", + "yansi", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -2265,9 +2306,9 @@ dependencies = [ [[package]] name = "rust-ini" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" dependencies = [ "cfg-if", "ordered-multimap", @@ -2319,31 +2360,41 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.12" +version = "0.23.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" dependencies = [ + "once_cell", "ring", + "rustls-pki-types", "rustls-webpki", - "sct", + "subtle", + "zeroize", ] [[package]] name = "rustls-pemfile" -version = "1.0.4" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.21.7", + "rustls-pki-types", ] +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", + "rustls-pki-types", "untrusted", ] @@ -2365,16 +2416,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sea-bae" version = "0.2.0" @@ -2385,14 +2426,14 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] name = "sea-orm" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea1fee0cf8528dbe6eda29d5798afc522a63b75e44c5b15721e6e64af9c7cc4b" +checksum = "4c4872675cc5d5d399a2a202c60f3a393ec8d3f3307c36adb166517f348e4db5" dependencies = [ "async-stream", "async-trait", @@ -2418,9 +2459,9 @@ dependencies = [ [[package]] name = "sea-orm-cli" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0b8869c75cf3fbb1bd860abb025033cd2e514c5f4fa43e792697cb1fe6c882" +checksum = "0aefbd960c9ed7b2dfbab97b11890f5d8c314ad6e2f68c7b36c73ea0967fcc25" dependencies = [ "chrono", "clap", @@ -2435,23 +2476,23 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8737b566799ed0444f278d13c300c4c6f1a91782f60ff5825a591852d5502030" +checksum = "85f714906b72e7265c0b2077d0ad8f235dabebda513c92f1326d5d40cef0dd01" dependencies = [ "heck 0.4.1", "proc-macro2", "quote", "sea-bae", - "syn 2.0.77", + "syn 2.0.85", "unicode-ident", ] [[package]] name = "sea-orm-migration" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216643749e26ce27ab6c51d3475f2692981d4a902d34455bcd322f412900df5c" +checksum = "aa7bbfbe3bec60b5925193acc9c98b9f8ae9853f52c8004df0c1ea5193c01ea0" dependencies = [ "async-trait", "clap", @@ -2466,13 +2507,12 @@ dependencies = [ [[package]] name = "sea-query" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e5073b2cfed767511a57d18115f3b3d8bcb5690bf8c89518caec6cb22c0cd74" +checksum = "ff504d13b5e4b52fffcf2fb203d0352a5722fa5151696db768933e41e1e591bb" dependencies = [ "bigdecimal", "chrono", - "educe", "inherent", "ordered-float", "rust_decimal", @@ -2484,9 +2524,9 @@ dependencies = [ [[package]] name = "sea-query-binder" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754965d4aee6145bec25d0898e5c931e6c22859789ce62fd85a42a15ed5a8ce3" +checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" dependencies = [ "bigdecimal", "chrono", @@ -2500,22 +2540,23 @@ dependencies = [ [[package]] name = "sea-query-derive" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a82fcb49253abcb45cdcb2adf92956060ec0928635eb21b4f7a6d8f25ab0bc" +checksum = "9834af2c4bd8c5162f00c89f1701fb6886119a88062cf76fe842ea9e232b9839" dependencies = [ + "darling", "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", "thiserror", ] [[package]] name = "sea-schema" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad52149fc81836ea7424c3425d8f6ed8ad448dd16d2e4f6a3907ba46f3f2fd78" +checksum = "aab1592d17860a9a8584d9b549aebcd06f7bdc3ff615f71752486ba0b05b1e6e" dependencies = [ "futures", "sea-query", @@ -2531,7 +2572,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -2548,29 +2589,29 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -2751,6 +2792,9 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -2793,9 +2837,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2806,11 +2850,10 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" dependencies = [ - "ahash 0.8.11", "atoi", "bigdecimal", "byteorder", @@ -2819,13 +2862,14 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener 2.5.3", + "event-listener 5.3.1", "futures-channel", "futures-core", "futures-intrusive", "futures-io", "futures-util", - "hashlink", + "hashbrown 0.14.5", + "hashlink 0.9.1", "hex", "indexmap", "log", @@ -2853,26 +2897,26 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 1.0.109", + "syn 2.0.85", ] [[package]] name = "sqlx-macros-core" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" dependencies = [ "dotenvy", "either", - "heck 0.4.1", + "heck 0.5.0", "hex", "once_cell", "proc-macro2", @@ -2884,7 +2928,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 1.0.109", + "syn 2.0.85", "tempfile", "tokio", "url", @@ -2892,12 +2936,12 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" dependencies = [ "atoi", - "base64 0.21.7", + "base64 0.22.1", "bigdecimal", "bitflags", "byteorder", @@ -2939,12 +2983,12 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" dependencies = [ "atoi", - "base64 0.21.7", + "base64 0.22.1", "bigdecimal", "bitflags", "byteorder", @@ -2983,9 +3027,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" dependencies = [ "atoi", "chrono", @@ -2999,11 +3043,11 @@ dependencies = [ "log", "percent-encoding", "serde", + "serde_urlencoded", "sqlx-core", "time", "tracing", "url", - "urlencoding", "uuid", ] @@ -3055,9 +3099,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -3073,7 +3117,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -3109,22 +3153,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -3194,9 +3238,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "bytes", @@ -3218,7 +3262,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -3362,7 +3406,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -3492,12 +3536,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "utf8parse" version = "0.2.2" @@ -3506,9 +3544,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", "serde", @@ -3572,7 +3610,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", "wasm-bindgen-shared", ] @@ -3606,7 +3644,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3629,9 +3667,12 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.4" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "whoami" @@ -3841,14 +3882,22 @@ dependencies = [ ] [[package]] -name = "yaml-rust" -version = "0.4.5" +name = "yaml-rust2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" dependencies = [ - "linked-hash-map", + "arraydeque", + "encoding_rs", + "hashlink 0.8.4", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.7.35" @@ -3867,7 +3916,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d7764d2..f0c3fe4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,22 +13,22 @@ axum = "0.7.7" axum-extra = { version = "0.9.4", features = ["typed-header"] } bcrypt = "0.15.1" chrono = { version = "0.4.38", features = ["serde"] } -config = { version = "0.14.0", features = ["yaml"] } +config = { version = "0.14.1", features = ["yaml"] } dotenvy = "0.15.7" futures = "0.3.31" jsonwebtoken = "9.3.0" once_cell = "1.20.2" parking_lot = "0.12.3" -sea-orm = { version = "1.0.1", features = [ +sea-orm = { version = "1.1.0", features = [ "macros", "runtime-tokio-rustls", "sqlx-postgres", ] } -sea-orm-migration = "1.0.1" -serde = { version = "1.0.210", features = ["derive"] } -serde_json = "1.0.128" -thiserror = "1.0.64" -tokio = { version = "1.40.0", features = ["full"] } +sea-orm-migration = "1.1.0" +serde = { version = "1.0.214", features = ["derive"] } +serde_json = "1.0.132" +thiserror = "1.0.65" +tokio = { version = "1.41.0", features = ["full"] } tower-http = { version = "0.6.1", features = [ "trace", "compression-br", @@ -39,6 +39,6 @@ tower-http = { version = "0.6.1", features = [ tracing = "0.1.40" tracing-subscriber = "0.3.18" uaparser = "0.6.4" -uuid = { version = "1.10.0", features = ["v7"] } +uuid = { version = "1.11.0", features = ["v7"] } rand = "0.8.5" base64 = "0.22.1" diff --git a/assets/images/flow-charts/1-shield-start-transparent.png b/assets/images/flow-charts/1-shield-start-transparent.png deleted file mode 100644 index c37b243..0000000 Binary files a/assets/images/flow-charts/1-shield-start-transparent.png and /dev/null differ diff --git a/assets/images/flow-charts/1-shield-start-transparent.svg b/assets/images/flow-charts/1-shield-start-transparent.svg deleted file mode 100644 index 2227ce6..0000000 --- a/assets/images/flow-charts/1-shield-start-transparent.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1dW1PbyNZ9n1/hynk5X9Wg9P0yL6e4QyBcXFx1MDAwMiGQYYpcdTAwMTK2sFx1MDAxNWTJkWRcdTAwMWM4Nf/97Fx1MDAxNlx1MDAxN0nWxTLYxORcdTAwMWJP1cTIUner1WvttXfvbv33t1brXXw7cN790Xrn/GjbnttcdO3Ru9/N8Vx1MDAxYieM3MCHn0jyd1x1MDAxNFxmw3ZyZi+OXHUwMDA30Vx1MDAxZu/fp1dY7aB/f5XjOX3HjyM470/4u9X6b/L/bD2e51x1MDAwZVwiJzk9+SGtXGIrPn50L/CTSrXWlGnF1NNcdK7fcX6YXHUwMDAybZqW5UZr0ILY6cBcdTAwMGZXtlx1MDAxNznpL+bQu/7+dV/sXGZHdFx0n4y+bHhic33fTy+/cj3vKL71koZGXHUwMDAx3Fxc+ltcdTAwMTSHwbXzxe3Evcc+yVx1MDAxY6+6Klxmht2e70SmQ/DT0WBgt9341lx1MDAxY0Po6ajtd5My0iPm/lx1MDAxNEk7xVxcQeh43auBXHUwMDE3hKbuf2HH/JfWfmm3r7vQXHUwMDA0v/N0Tlx1MDAxY9p+NLBDeEjpeaPHu6JpXHUwMDFie47b7cVwUOi0PifpWckoXHUwMDExSIv0YZhKXHUwMDA2253ksf+Vdmdo951tc4U/9Lxsn/idhz55XHUwMDFjXHUwMDFl6Vx1MDAwMCFcdTAwMGZH/k7vwpy/Pj6wsoMrN8Bi50d6a5lnv7YpTj6eXFyc9b5tSCdcdTAwMTh9+Vx1MDAxMoqD3XdP5/39e3mx91x1MDAxN+99+9zRu1x1MDAwN1x1MDAwN+s36uzo7ItcdTAwMWJvXFywUb6Wx/rtMFxmRplyXHUwMDFmvqXdMlx1MDAxY3Ts+yGKJdGUY8JcdTAwMDRm6VP1XFz/erzPvKB9nY7q3zJcci5cdTAwMDAsd/9cdTAwMTl06SpsYcxcdGeEaFFcdTAwMDIu1lx1MDAxY1xc5Vx1MDAxZLzQ4NJcYluKKK0x3L2SelxmakxbXHUwMDE4MYVcdTAwMDViXHUwMDFhXHSG51x1MDAwNjwpLYRcdTAwMTDlSHOCmWKsXGJDylx1MDAwYjAkgmKEXHUwMDE1ezlcZnM/XHUwMDE08FY3dCklapqhm7Yq8OMj9y6BvMpcdTAwMWTdsPuuZ/o/PZyMaTNcdTAwMDBiO8x0nzm87LldM7zftaG5TphcdTAwMWL5sVx1MDAwYlx1MDAxNurphL7b6XiZh9SGumzXd8LtJrYhXGLdruvb3nF5U+xhXHUwMDFjfHKi+1x1MDAxYorDoZPtXHUwMDEzZ+vxKWKL8Fr03rNHXHR8maRVXHUwMDAwJohxwbHKXGb3XHUwMDE0wLw5gOtJbjFcdTAwMDFM09FvrqCIjdU9O5BiSytFiZZCS4a1zoiRXGZKLVlcdTAwMDAqoERTijSaXHUwMDAxUp9lMGtcdTAwMDAsOaOKZlx1MDAwN2tTXHUwMDAwJ1x1MDAwMFiBgeb63XzDXHUwMDFlRGBcdTAwMTNYJZBvXHUwMDBmTSuRhYywYExcdTAwMDBcdTAwMGZrpFx1MDAxOWaZs7r2wHSvhcBSMcG4ZpJILkW2IPeH0zlcYlxcP364g0LHOH5ncoPx5sHog/6x/Mnpfbg8+bJyODywv5Q1eFx0WowkXHUwMDAxXG5mXFxRXHUwMDE4XHUwMDFhmmhdaLK0Ulx1MDAwMdeokZ5cdTAwMWTFq0G/78Zx/sx8zy9cdTAwMWKi6Dl2YbTATWZ/XHUwMDFiZ5SBKTEvo9JvrVx1MDAxNF7JXHUwMDFmT9//+r307GpImE9cdTAwMDKGtIiCXHUwMDEwcrzLYNRQ1YROO76ngDJuZNXiXHUwMDA2mlx1MDAwNfzIWFx1MDAxOTeK5txYPypcdTAwMTaSXHUwMDFiXHUwMDE1lnluZHKs7lx1MDAxOXpcdTAwMGWMWDr7KXEkMqrmgVx1MDAxODUhXHUwMDE4xkza7Fx1MDAxOdJcIp1Mi+WCf5JcdTAwMWaxTdBcdTAwMDH96rudIDy5jlx1MDAwZpb8nYPe2Sv4XHUwMDExteVG7Vx1MDAwM/vw636/s/n5MtRHt9HGYCOclX9cIpmSXHUwMDE52zUn/4RmXFygcVx1MDAxMFOttORcdTAwMTm6z4BYNlx1MDAwN3H5o1tsXHUwMDEwc2YpjpBcdTAwMDJcciFcdTAwMTl43XlIc2HxuYFcdTAwMWFcdTAwMGIwXlorylx04kgqTIqgJkW1XHUwMDAzp3NEaObs1/dLplx1MDAxZbKlflx0ylx1MDAxZH3yS9JbfvRLdlx1MDAwM7vTcvyb1o1cdTAwMWS69mXWSs3UR5lghcZ9lJpmzcpfqbXLolx1MDAwNtJcdTAwMTh+ZCA/0zMykN5sjumvUf/oemNrJ/zAXFz8jVx1MDAxY95+tK9XXHUwMDE2XHUwMDFj0ziPYsbn57RcdTAwMDBtmNBC+kkp9Fx0w7yIYaZcdTAwMTghaiaxhVx1MDAxOZrm+yd+c7ZxXHUwMDEyf+yw4d6Fq06/7XnHnd1eua1L8Pl2TXNtuVx1MDAwN9c7S2G3vXK758VR1/t8dMiZ/bZMPufV/CBNXHUwMDEzMFJlul2eNOeH+tGymPwgiCVcYviW4LxgTDPhsYQtslx1MDAwMflZW3yOwWXTikvof0lcdTAwMTktXHRxXHUwMDE0Lb5cIuBicaJmMFx1MDAxZvBcXINPXHUwMDEwxTTjXHUwMDA1ztvgb/tu7NpcdTAwMWVcXNbygm43a9NnavAnWLdxg1/TrFcx+LJ6XHUwMDA2TyRcdTAwMTal3Fx1MDAwZped5nhW+Gb4XHUwMDAx2/xw93Ck+idrzsn2LVtwPKM0wJZcZieuxuqeob1cdTAwMTc674g3MfdSS42o4Kk2WFx1MDAxY2tcdTAwMWbHh2ffTrqbS8fa6excdTAwMWbc7mzozvo/1n5cbmtfW+5yuLY9OvpCz9rBLnGHe3erl1x1MDAxZPS2VIRElTMjmFx0XCKUUKgsb0B6zVmnflx1MDAxNC4m62BhSYkwXHUwMDA2XHUwMDE5RalUeZ+DZ6aT5lx1MDAxMVxmZFxcS0GgXHUwMDE2+KNR2IApXHUwMDA16kPpXHUwMDE5cNBbiVx1MDAxYaxHMTjlbtRrra20Vlx1MDAwM99cdTAwMDfTaob0fKTEXHUwMDA0wzkuJSY27lVcdTAwMDRcdTAwMDXOjvMxcCtcdTAwMTCoXHUwMDAyKZI+sFxmtlx1MDAwN82x/Xn1Yvdyy75cdTAwMTk4X09XRrY8WD5Yj1x1MDAxNlx1MDAxYttcdTAwMTigkoMzQHx+cEZ4eklcdTAwMDGem2FcdTAwMWU6nyShXHUwMDE3aorPa/vHJ58vls/CnnNcdTAwMTWyo/7mMNj8R1MsgKaoLfdmf7C6NfixcrrV3jr4dtX/OpSBO4Ny7d5xcNL7eHu5vnz5Y335Y/dw5/OHXHUwMDE5aSBcdTAwMDZIXHUwMDE1mbS6OWkgjEj1XHUwMDE0qFBcbklOy0Ot8Vx1MDAxNERZXHUwMDBim1x1MDAwNSVKISxtYilcZqSg0pnwZ0KbZH6eXHUwMDE4lsjCJpaCoXJcdTAwMDJcXJhyZN3siWJSXHUwMDExkLQ/cfaEmH+wmGLMvkxcdTAwMDd9XHUwMDFh+kZk9N1uaM9RXHUwMDAwTbDz41x1MDAwMqi6VXPP9Vx1MDAwMtVeKXpcdTAwMDRcdTAwMDctj1SZQ7PcXHUwMDFjyvUmZSGhrCm1JFx1MDAwMpFOwVVQWOejolQzi8xxKtRcIohLkymrXHUwMDA1J6osT5qLstQvLlxiw5rNwq2ZeepcdTAwMTfTwEzPQXnD1K/GmVTIQoxK4ElcdTAwMDFcdTAwMGU7XCKKaFaW+zVlXCJVo2yv+lx1MDAwMGsrn+1FXHUwMDE4XHUwMDAzW1wiKFx1MDAxMYpx8KyLbZyqgW8r06tcdTAwMDJcdTAwMDDmk1xm/bSEgiiaJtGrklx1MDAxNFx1MDAxNa+MLWNpXGYmxrpM4aw0Z8V6QbygrCgsXHUwMDE4i0pLiVxynvP6huP56Vx1MDAxYmRcdGFmXSTFjCpcclx1MDAwMqfIiUDZXHUwMDE0PEFcIjRnSmYzxZ+SRahcdTAwMTBcXMqFTI2dNz825lx1MDAxZUM9XHUwMDE0aehvXHUwMDA0/Vxi3a1Zllxu77knm3o6I3KsXHUwMDBmXHUwMDE5jZEjwlx1MDAxY1xignFccq3E0FxcXCJcdTAwMGJNXHUwMDE0loCBXCIpYlx1MDAxY1x1MDAxMzPF+MvSZTU4zKdcdTAwMDCLXHUwMDE5cWfHtfuB3yl1XHUwMDEwha52XHUwMDEwiVx1MDAxMoxcdTAwMTFUXHUwMDFhSVttTp+36Pg4XFz/uHt9N+jiU7HWXHUwMDFiXHUwMDFlZlx0Zlx1MDAxMelTofySXHUwMDFmwZklXHUwMDEwpyBBsKZg7irp01FcXKN2LX3+6+qqrdu6JKImS0Li4C9cdTAwMTbVo0RSguH9Werx6ZqpMmSvv32nO4huu/0jXHUwMDFiXW3hYXdgf/hVg0er11x1MDAxN99cdTAwMDZX3a9cdTAwMTeRg6/u7rr+XHUwMDE23/SblVvAesFcdTAwMTFnXHUwMDAySHKarIZaeqhcblx1MDAxZamMlVx1MDAxYqNcdTAwMDbwXcDjxKTM3Vxca85cZuUjYrGZQSpD0khcYkKAxFmeJyRcIlx1MDAxNp5cdTAwMTVP1HuehFtUwlOQnFx1MDAxYlx1MDAxZkmXrFxyLPqdyrhRP3c2bfrBm7bqOVEkN2rFPSd0WrZ/2zr3XHUwMDA3TqJuTFxi59x/XG7i/Dv6v//MKbw0wfqNh5dyzc00tqKpc485aV5cdTAwMTlzwiBcdTAwMWUkJ0KU6YP15ixQT+1cdTAwMGLJXHUwMDAymubDTFx1MDAwMpOG+uDFs25cdTAwMTbVRrRppUBEiqw4S1fUIEtcdTAwMGLFwCkwMVsqM87eXHUwMDAzXHJQ+Fx1MDAxMVx09rNW6k+IMT+PXHUwMDFkXHUwMDFheleNnVx1MDAxN/Bd4EEqXG5cdTAwMWFYU5P2mMlkfHJdsrGeXHUwMDE5eVf1hNFcdTAwMWFcdTAwMGKPaVx03oRZXHUwMDAwgqH7SDE8hi1qkrZcdTAwMTFcIlx1MDAxY1x1MDAwNqdG+td1rqqhYT5cdTAwMDVQpMVcdTAwMTVcdTAwMDTXTFx1MDAwMlNcdTAwMTiTylx1MDAxY1x1MDAwNVBOyCyNJGVpj1OscqhXr1x1MDAwYkmdXHUwMDE4XHRtgXBBTHCuZMblSVx1MDAwNFx1MDAxNEviqVQxzYU59SVEWuloSWlhzYRcdTAwMTJmiTxWtGxcbs6iXHUwMDA0+Fx1MDAxY5qoldlcdTAwMDejuEqRUsklZfhtOWGato/2XHUwMDE5/e6fXGK5qYjjXHUwMDFjtT8uTT9z/ay9XHUwMDFkXHUwMDFhMnRjXHUwMDAyNOElYDQziY0oXGJyXHUwMDEwwsWF1nNg6PpcdTAwMTnDsVx1MDAwNlx1MDAxMlwiuVx1MDAxOWxml1wiKpEqRuhYsl5cXEhChUSa6193XTitwp35XHUwMDE0XHUwMDExN6m4Slx1MDAxOJuPSU5cIlppJsEuUqGImFx1MDAxMeFXrlqpJnsqXHThqHRcdTAwMWaOzSnWrJRjd8HZXilge1wiXHUwMDExXHUwMDA3XcfyK88lXHUwMDA1X5ppKY1Rzlx1MDAwZfyZkj1cdTAwMTFcdTAwMTZDJkJcdTAwMGXCXFwwJEhcdTAwMTnbXHUwMDE3YmySKClcYuY/c7WqhjGTmal5nqecsU9cdTAwMTN20TlzMmlcdTAwMGYz9YEnyJRxXHUwMDFmONeQ+WdVZHy5goeLXHUwMDE0XHUwMDAy9Vxc6uFuN1x1MDAwN259MHAhgWsmXHUwMDEwXHUwMDE1k1xcMFwiQa3yvL9cdTAwMGJsanGj35RcdTAwMDJUIT5cdTAwMWaZRi1cZrxcdTAwMGWi3WRqMa5EWYxcdTAwMGJZUjGssJl+5pRcdTAwMTd2peNcZlx1MDAwMcdoPVx1MDAwMyC/okw7ZatLu0M2OPnQ3l267Vx1MDAxZm7wwz00VYyYXG6c3Vx1MDAxNuAnyjTjp4I5XHUwMDA379Q4qpTIdL63ldnBXHUwMDA3pD9TJsdDgCXIbfIzI9lG7o5i9X2fic12Zzm6dUffe3q9osFUgVx1MDAxY5NSUFx1MDAwZVxmwGVxznI6WfmWRFx1MDAxYa5EnflcdTAwMTA1hreJmq+2uFx1MDAwMnznLNIqZzSw0Ci/sj1l+lx1MDAwZs2Zvlx1MDAxY7dcdTAwMGLO9Fx1MDAwNGQ0sDxjnEhG8zNcdTAwMWHJRiNz12hYWsLsdFxiTlx1MDAxY0aalubEXHUwMDE2p0FcdTAwMTG44IijWYQxn6/RiMD8pTmxzTXaXjAniTZBooxLtGw7ZqXQ6tJcdTAwMTQyXHUwMDE0XFzc5lx1MDAxMLSK5rJcdTAwMTS6O82hW29cdTAwMWVcdTAwMTZcdTAwMTK6itzHXHUwMDBlJPgp8D2/56HSXHUwMDAyjKpcdTAwMTaIYbCromaXr1x1MDAxN+UslOW8luUsXGKqhXpz4bJcdTAwMGad/nqM1vfo5SU6Pd25u1x0L1x1MDAxY+dcdTAwMTVyXHUwMDAwast9wcKU2nI3LntcdTAwMWLDwene6JRd7qxcdTAwMGbvVo6Wo68zKPf7bbTE2npv/0sgV9fi06Od5d7RjHIhqKJI8HS4zWsxMamZXHUwMDA2lWBcdTAwMDeoUOlcdTAwMTkp/+w255/yobbY/KOUxaSk2Fx1MDAwNOxMSluOgDSjXHUwMDE2nztcdTAwMDFhRCzOtUYmXHUwMDBiXHUwMDEyyinbm6QkXHUwMDAxn1x1MDAxMsqEkLNwXHUwMDBin51cdFx1MDAwMW5cdTAwMGXBr5pcdNFxruyhXHUwMDE3t8790LnfuT+C785cdTAwMGY3iqN5JUBMsKslXHRcdTAwMTCPrUzbWGjh3KNClIjxo5m9QynKRVx1MDAxNFK8f2yO93pcdTAwMTZfSLxjs/V5peBcdTAwMDBcdTAwMWahoeB4UVCobKsyjDKVPe5VXHUwMDA2viZcdTAwMTXZKdZFSXKY/1x1MDAxNNpUM1TK7Fx1MDAwNqG5iedRUZxAI/mHyuawt3LjyMxcdTAwMTJcdTAwMDCamFx1MDAxOT+hTPY0XHUwMDExJVx0XHUwMDBm0qJSwkBcdTAwMTWKXHSJMu/FaNTet1x1MDAxNKlZojp7vm5wXHUwMDAxXHUwMDEy2StcdTAwMDA26SVcdTAwMDWpNZssiEwor7BoUVx1MDAwYq1yJjAl0r3mRFovW1x1MDAxN5JIQaWYLVRcdPi14LxcIp1fnoORYFx1MDAxNjfJKlx1MDAwNJlcYlx1MDAwM69mUnKlXHUwMDFkxp7HpNziXHUwMDFhM5LEfEwmX5kjp7DFuFx1MDAwNCZcdTAwMDWtojBcYqZcdTAwMDLNXCJgXHUwMDA1glmGgN+CX3d9a2/rlbWVk/jqXHUwMDEz7+xcdTAwMWOgweaVbO53MFx1MDAxOLTm/SDz5PBpwtWJVVx1MDAxMTCqmOBcdTAwMTJcdP3SeHUj1j5cXPI6q3ux8+lGLNt7R52z79dbR1VGRmNiVixQJDPbmrbSXHUwMDE1QJxcdTAwMTFcdTAwMDQ/XG4zS1xmtPTr5kDkVkhcdTAwMWFcdTAwMWHgZmZcXJhcdTAwMWRrpJiY8lCJ2aToXHUwMDAyWmfE7lVecbVEXHUwMDE2htyQyCz+S6l9f4pcdTAwMDVcdTAwMDKlMF1oajdbXHUwMDEzWaBO4Fx1MDAxOVEzpaDHnGKJX4HaKbZcdTAwMTTWMKxcdTAwMTRcdTAwMTKVOVx1MDAwZkWnWCCqtFQkbfLrO8VcdTAwMTJcYkI/680jz3KK55f0MEGWvErSw4TXY1THtKTgildskHfUXHUwMDFjv1x1MDAwM49cdTAwMWQjple+XHUwMDFl3LJcdTAwMGY/Vruj40tWJc3aYVx1MDAxMEVLPTtu91x1MDAxNlx1MDAwMcWU1Cg0pZtcbrQmrq6jnbZzVVx1MDAxMlvPhNFT2OpCioNQYFdxZqefRdhSa5JcdTAwMDI7ZJ07//KQLMWdjY2ts7X45vunzitElGvLvdnq3nnBzVl0csbXVv3e5qdBjzYrd4JiXHUwMDA0fYY0ZVN5/c+zybomcGWWiWug+Vx1MDAxMlRcdTAwMWY3R3X5o1twq0xlLZ71a+CZa1x1MDAwYknCXHUwMDExXHUwMDE4Zl5uk1Vh4owobJJnxFxm0P1cXJvMXGI161xmX2iTqya5izb53F9cclx1MDAxZGhB6yrwvGBkVsA9hYL/OPfPfWy1Pjm2128ttT6C9HfCc59YrVXPhXuBY+3ky7lPrdbnyFx0WyM37rXW+7brtWy/01x1MDAxYdhRNFxuwk7r3IdaXHUwMDA3YXDjdpxOy/XNOzHOfWZcbr+vrbVpulx1MDAxYUp8iEif+zz9MYLjf9pt+Fx1MDAxMrXi4L6wx8h10rrfXHUwMDFmW5TUn/xud/pQTVx1MDAxOHjOX/D3n1x1MDAwNyE4XHUwMDE2LTtqjVx1MDAxY88z/1x1MDAxZdk3jln/Z85uh05cdTAwMDeudqE/XHUwMDFmyrfee0E3ev9QyYU5wfpcdTAwMTZcdTAwMDX+X1x1MDAxNVxuxnOu4lx1MDAxYf1cdTAwMTJcdTAwMDeDSvEywXCPi5d/ntizn9jcZzIwqcxM14JcYipFmcj70txcdTAwMWPUXHUwMDFi40VcdTAwMTZ5iFx1MDAxM4tJRlx1MDAwNEJcdTAwMWFLhfNGQStkXHUwMDAxUXPoIKrZXHUwMDBipzOqbIIsyWmlltBcZivGXGKWyGygktnE9UHxwc1JJN5YMsVgh3WWwuuLq5NRLG+2r77Tg4vr6SSU5FrM87WUzWdcIszWNmCVmdnHxeg6gWUxrXVcdTAwMGVRt3pmbmXDgkbvIK1hsEhcdTAwMTgspLgvXHUwMDE5t1x1MDAxNGFcdTAwMWFGOMJcbqRcdTAwMDf6dfcpI3qq02UuSbWIx/T6wpida5Yqg7pcdTAwMTHjtOyFd6dTuOWlQHxcdTAwMTOMLbVcdTAwMDWPgnDwjYVcdFx1MDAxNuVcdTAwMTlbvlx1MDAwMmNcdTAwMTNkmaW9mJmc2KrX31x1MDAxNfPfKFx1MDAwMckvXHSZwY5mz9XxVFGgq2k2XHUwMDFkfpmOn1uy6lx1MDAwNMXxXHUwMDFhyarVclx1MDAwYmXc6/GX3TCumM7OoqXwPWtcdTAwMGXf+ijFXCLDl1x1MDAwMHxN1pOgYHdcdTAwMDDBefhcdTAwMDKqXHUwMDAwW0RyYraBXHUwMDAzkL9ol8JKXHUwMDAwg622XGI1Ke9gszHLvCYkVWAmflx1MDAwZa1gONmSjpHCvKdWXFwrKeTPWlZUNzNJocHzXFz501iCLJn97sxcIkqGlNCIi5LVz8Jk75k9OrHWYFvnsO6n+TwlNrbFZFx1MDAxMYKhN/OVxf0+gPyp4oyad41cdTAwMTNQVvjXnahcXKJcdTAwMTJ0XHUwMDBmYVRcdTAwMDJcZjhcdTAwMTH5l3hcdTAwMTNlIYooNZmfyrzmjU8ur1xud+ZTRNxcdTAwMGI1ltvg2T+xOTjz7iDK8Ml9dr2wJEVm71x1MDAxNCGRpONkXHUwMDA1ziOIMaOgqUliSZkgXXmMoVx1MDAwMC2pYFQjUbaqRUuLXHRcdTAwMDF1wElcdTAwMWFcdTAwMTRCXG7bXCKDPm+C0GlrvVi2YSqifDKQlyjIVTdcdTAwMTV9PokwQbhJ3ixMfCU+deX7ooC8XHUwMDA0lyyT31xcbaJn5CvTtb1I2nd8K1jaXG4+f/u466v+4c9O5J/f9Ehitlx1MDAwNMfTiNNcdTAwMWHYl/deXHUwMDAx9nn/6357P2RxkcBZcTNcdTAwMTc+hnkzey/BuaDAXG4g/mVcdTAwMDHzWFLzVlx1MDAxY8mTXFz0XHQvd/hcdTAwMDfj988sxfigXHUwMDFj43lcdTAwMTP5XGJmTVx1MDAxMcjE4rslXHUwMDEzR7n65W9cdTAwMWGblFcunoPmXFxDikNcdTAwMThcdTAwMGKOnuVfPbpMR8b8J/HkyFx0b7I+UoPshvxywebeVt1EwFx1MDAwNFx1MDAwMzrualW3/6WO1z2w5cb2xWlwXHUwMDEy3Og9smxcdTAwMDe7w883O0EjYFx1MDAwYtCi2kh2XHUwMDAygkXw/LZTmGhcdTAwMDSOiXklXHUwMDAzSFx1MDAxMLNcdTAwMTlPXHUwMDEx2JpaJkSmzP5hTJW+XHUwMDAzl9U5QOPQVkxQrv7/QPt7c2hDP2tQ2MU1n8Y1q96wXHUwMDFhXHUwMDFjXHQw7ylcdTAwMWQsXHUwMDBlrP9cdTAwMWPBg2t5blx1MDAxNDt+y47P/V5cdTAwMWNcdTAwMGb+eP9cdTAwMWUus71eXHUwMDEwxX9cdTAwMWPsfzr+q1x1MDAxNO3NXHUwMDE2XHUwMDA3z1x1MDAwNO25W1x1MDAxYYf21PcwNeJ/e9BcdTAwMDfv7MFcdTAwMDCIJHae1N27XHUwMDFi11x1MDAxOa2UXHUwMDAyxHyMzkj4wlx1MDAwMMRJROHfv/39P1x1MDAwMmh7/SJ9 - - - - - startLoad env variableInitialize loggerEstablish DB ConnectionRun DB migrationis there any pending DB migration(s)?YesNois default resources exists?YesCreate following resources:1. Realm - Master2. Client - client3. User with Email and password provided in env4. Resource Group - default5. Resources - [access to default Realm, Client with admin role] [Print as well as Save the credentials to ./logs/default_cred.json]NoStart the server[will listen athttp://localhost:PORT] \ No newline at end of file diff --git a/assets/images/flow-charts/1-shield-start.png b/assets/images/flow-charts/1-shield-start.png deleted file mode 100644 index 3c0edde..0000000 Binary files a/assets/images/flow-charts/1-shield-start.png and /dev/null differ diff --git a/assets/images/flow-charts/2-admin-login-transparent.png b/assets/images/flow-charts/2-admin-login-transparent.png deleted file mode 100644 index 68bcc04..0000000 Binary files a/assets/images/flow-charts/2-admin-login-transparent.png and /dev/null differ diff --git a/assets/images/flow-charts/2-admin-login-transparent.svg b/assets/images/flow-charts/2-admin-login-transparent.svg deleted file mode 100644 index d0686f5..0000000 --- a/assets/images/flow-charts/2-admin-login-transparent.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO19WXNcIsmS9fv9XHUwMDE1spqX+cyuclwiPPZ5mU9IpaW078vUmFxmsVxiJFx1MDAxNolF29j97+OOVFwiISNcdTAwMTNcdTAwMTJcdTAwMTKKrlx1MDAxNm3dXVx1MDAwNShcdTAwMTWZXHUwMDEx5/jxXGJf/vdcdTAwMWZLS986r1x1MDAwZqVv/7n0rfRSyNeqxVb++ds/6f2nUqtdbTbwI+j9vd3stlxuvW9WOp2H9n/+x3/0f1wiKDTr7z9VqpXqpUanjd/7b/z70tL/9v6Ln1SL9LOP1c3tU9Y8OeD7T3x/91ooUS/2frT3pc/B1GrVh3ap/8FcdTAwMGK+y7WSgTXaKfyTc4apz49f8WPlTFx1MDAwMFx1MDAxYVx1MDAwNGdMOvxcdTAwMTb//PS5WuxU6EZcdTAwMThcdTAwMDRcbqxiUnGrjOLw+ZVKqXpb6eB3XGZcdTAwMGJAamFcdTAwMDS31uJXzOdX8o3bXHUwMDFhXHKOfb7T7rSa96XVZq3ZokH/W4lcdC54f9A3+cL9bavZbVx1MDAxNPvfKZdLXHUwMDA15/rfKVdrtePOa+/K31xurWa7vVxcyXdcbpVvQ7/l/Nc9XGa9//mz7SbORf+n8NfeVlx1MDAxYaU2zUT/STRcdTAwMWbyhWqHnlx1MDAxNj6lz3dpjFx1MDAwZlvF3qT9T39krXy9tEWz1ujWap9vV1x1MDAxYsVcdTAwMTLNxrert4Hf1ih+/LZfM96fTvh451/9sZdKdGEtJFeay/5cdTAwMDD7q044NfzuXrPRW4HcSoNzXHUwMDA0sv+Nantccpdep3fVcr7WLvUngYb2fXhZhpfmwMrrlF46n/dcdTAwMTVauC/XbPl2d13li/y8rcpHt1fmqPvt83v/+qf/su8/fNV6rnQ3XHUwMDBmzvZW+Y/zSrtcYvbHhVx1MDAxZPwtv35/vtVqPoeu+/Gn/rR0XHUwMDFmivn3++RcdTAwMDacXHUwMDA0Zpy2wn1+Xqs27ofnrNYs3PdcdTAwMWbNP0JcdTAwMDNcdTAwMWWCp/8uI/BcdTAwMWN4SO/YNFxcXHUwMDA3VmhphbJCSKtcdTAwMDewqTlcIk86rVx1MDAwNIBcdTAwMTbOyVxiNo1cciRjXFxzI1x1MDAwNdeS6Sg0gX1hMVx1MDAwNot55sfiwLc/QFx1MDAwN6C0dVJcdFx1MDAwZug4RN79XHUwMDA0XHUwMDFkx7lF7rV8XHUwMDAy0FxyjGN4XHUwMDA1c1x0XHUwMDA2qTnFXG7uL0haiHj/ksml74icVmgym43OcfWtN3Q98O56vl6t0XO3XHUwMDAz11mpVW9cdTAwMWK9mcdBl0JXwlx1MDAwN9Gpoqn7/EKn+dD/tIBXzFdcdTAwMWKl1tY4XHUwMDA2rtmq3lZcdTAwMWL52kn8wPPdTvOo1H5cdTAwMWZ6p9UthVx1MDAxZkxp81x1MDAxN1x1MDAxYXhcdTAwMDAqXHUwMDAxyY9GLZ9cdTAwMWTq8vPZ2VvLnuyttrqPO6lcZq3kRiFmXHUwMDE5wqg/N73Hxk3ArOPCMfqC63PPl6GdXHK4N6a2tFx1MDAxY81cdTAwMDTNl1x1MDAwZvSKy+F3f4FcdTAwMWWMYtyi2srO0L4vz7NLaJ+9sof13N3xdWPtSF+ubLz4LWJcdTAwMGbjsze0idc9f1x1MDAxNLvc5nZcdTAwMGX2XHUwMDE4a4qVXHUwMDFme2yv85SVXHUwMDAxt1x1MDAwMMB4XHUwMDE45ZNcdTAwMWLw5Oc60oAzXHUwMDBl1kguXHUwMDE0c2JcdTAwMTDzXHUwMDAyXHUwMDAyYdC8XHUwMDAzXGLgXCLE1l9cdTAwMDY8XHUwMDFijJ+Nb8GVYNohcUZcdTAwMDQy3ZKEODBzpoWwKLfNXHUwMDA0aFx1MDAxZWHBuTNmOlx1MDAwYlx1MDAwZWJRLHiy5YxY8OjAs7Hgl+3V67VboXPPT7q4/33n7Pj2qj62XHUwMDA11zzgXHUwMDBllEMvSSs1XGJmzsBcdTAwMDRoXHUwMDBmmEZcdTAwMTdcbiCk1b9cZvhswF2c2oBrZ5VcdTAwMDMtfVx1MDAwNlxcm1jMW+eQj53M3ICv3vzYXHUwMDEzXHUwMDA3cnXnrVXMXHUwMDFmdiud7/Ba+CNccnjidU+2zN7GQa580znfK4G7vYZ6Yy8zYYBzXHUwMDE3sqNTXHSD5PlKXHUwMDE0XHUwMDA2IFx1MDAwMq6Ag+LauWFngDMp0Fx1MDAxYmCSKVxy6Fx1MDAxNOgv1z5r8qiNr1xmemZcdTAwMTicUGaYXHUwMDEw8EPJXCL7bL9oQmkupGF2klxytVHKQJjQaCby7Vx1MDAxN0VcdTAwMTmMsMhR335GyuD53lx1MDAxNo4qXHUwMDA1U7uQ7c3lveplTq0+jq1cZlxmXHUwMDA0XGJaa4VlRkozJFxyjJCBsYKhXHUwMDA3YJQ2LFxu5y9tkC2861NrXHUwMDAzQFLWxjFcdTAwMWboXHUwMDFkt8PvfvpcdTAwMDPOop5cdTAwMDCmJ/FcdTAwMDdcdTAwMTLFwc5RZeP6asdcdTAwMWG9d3HFm7dPqlpzX+IghThIvG632j03T2uV2r7acPc3/KJ2vccyXHUwMDEyXHUwMDFknHFueH9VTyU6ktdBouhAXHUwMDFhYlpKroxgXHUwMDE2VJ+G3llKsYBcdTAwMTmGwlx1MDAxNpWJYCy6XHUwMDA1+SU63t+dlJVcdTAwMWHji1x1MDAwZYdcdTAwMGZGaOm8romMPcVD15SMXHUwMDEwzOA8gU6AZYolvMCaY4Stn5vmeNiX23flN6he67X64V7hle1vbGSkOYTmgUb3gXOFbi6EPv7SXHUwMDFjs0H3w9SagyvHmUKvwXd2byVcdTAwMWJ+91x1MDAxN+qlYEZLgMw1x+PxjzW99YD2QL7lisWjVSdcdTAwMWVrX5ojK82xdnH/+vokr6qtjc3rZbWeO2097We10eG0Q+hmozmS18FcdTAwMTSaQ1x1MDAxOFx1MDAxZDgplKLgXHUwMDEzqUx/XHUwMDA1f2mObFipM77m4NziNFx1MDAxOWd9Po/S8T6PXHUwMDAxtCO44Po2JjvVoZycUnUsSlx1MDAxNMNcYms/hyiGWEFcdTAwMTGa5ngrY41cdTAwMTXO2dBueX+ZXX1cdTAwMWJj6t+5RLq9Rzg0W2+Pr9uHP5rN01x1MDAwM/58XHUwMDE0g7UhvMxcdTAwMWVlUf7osZjTSFNGM6214WBcdTAwMDZjJFx1MDAwMXRgXHUwMDE4uvxKKWc4sm4s+fBcdTAwMTL9k0g+nVa+0X7It3Cl9b/3KdlE/1x1MDAwNj7pT/dcdTAwMWS1X0DWoLVA96FcdTAwMGbkVPSRVr5kXHUwMDE0XHUwMDBleF3IbV7L9Z3j/dvW43p9ez+n1q7HtaXt68dcXGe3e1LYfnh6XFy+LNxcdTAwMTbPdl+zsaVcXDOFs5uGhlx1MDAxMuE3aCZD0ZlGx2GPc4lcdTAwMTaQ6bDE+1x1MDAwNF9+fPD5XHUwMDFm8UKDXHUwMDBm6DDE4lxmKFxuyuRKXHUwMDBlRkFC71NtLc5cdTAwMTIxlNAzQ58xXHUwMDAxY4ziOFx1MDAxNFomXHUwMDFiskufWFx1MDAxNGpcdTAwMTiL1iFcZplS0yNx4INcYuSyXFy9PktcdHbg3Vx1MDAxOEtJz/C4k2+Fnl46XHUwMDAzWq9cdTAwMTaLtVKsXHJcdTAwMWRhPIZt6NBQsrKf71x1MDAwNOJBMISX81x1MDAxMIS1Q1x1MDAxMEtcdTAwMWQyXHUwMDFlfVx1MDAwNN+Mj+BknltQXHUwMDA0o8pcdTAwMTdcdTAwMDLQNFx1MDAwMlx1MDAxZL5cdTAwMGWZT2fRfCorXHUwMDFkkMq3ZnZcdTAwMDBmgXJAXHUwMDA23DEjrOOhXHUwMDEz/D6CReCQRIwzoLlFc1x1MDAxZjGugFxcY3nYXHUwMDE0z9e4JiPdTCaX24SUXHUwMDFjrshq43ZwYFx1MDAxZtkn4+Cvx1xyhW6796iZNESTXG5cdTAwMTWj1YLpfmZcdD2x/Fx1MDAwMz3rgClcdOT4OWlQVvWnvreSX0rFg2a10fm4hciTKTWKY4z4oXq60767uu50j3PNQ/bSOjx+9o14XHUwMDE5h8xcZu3VS2WFtZJZXHUwMDExXHUwMDE5MSdcdTAwMGLQf4GVqUZcXMu3O6vNer3a6VxmfnNwXHUwMDFlVohfKqV8ZO3gXHUwMDFkhz9cdTAwMWImolx1MDAwN7rioFx1MDAwMOv/aamPyt5fPv/8P//0fns5XHUwMDFlLfSK4KR/vYieKtVums9jiqNWqdB5p1x1MDAxMVx1MDAwZr9aXHUwMDFiXHUwMDFmSo9LiVxms/DxayGFe5K4Xlx1MDAxNpJfuZMscFxcXGLGKItcdTAwMDenZYBgXHUwMDA1TpVcdTAwMTb4aCx61EKHpMhcdTAwMDT8XHUwMDFh2Vx1MDAxYvl0TbRccsBYLqxcIlx1MDAxMndcdTAwMWVxxIFcdTAwMDfKKvyOtEZolFDD3GqMtoI5N1x1MDAxM2pcdTAwMTWjqdXvYIzyW95KR0/VyrYq79Y2OvvbNV7vXHUwMDE2z+fgtyReXHUwMDE3XHUwMDBlXHUwMDFhm93czeHhydUmO7g24nZfPWfkXHUwMDBmKZwjXHUwMDBlU59njvKHcCXFop1SXHUwMDE1UVKEvtFHe3F8tPunbsHRrkzAXHUwMDE5mU1GoeNDcahCykBcbvSKlLaaSVx1MDAxZEo+yn43Qlx1MDAwNdY5S3lcdTAwMTBoNpX2XHUwMDFjIIXv51x1MDAxN8hcdPpIRr/RJUq/gL0uXHUwMDExXHUwMDFiePfTJerf2C+X6CD/Wmvmi8hKP1x1MDAxYj+/ler5au0nvv/z22uz+/97f6Vs35/f/klcdTAwMWY/5Nvt52ar+P6NXHUwMDE3fC3Tf35++9n416zcqmSjN+xWzeB2snLNXHUwMDEy5Vx1MDAwM1x1MDAxYaD4TWxcdTAwMGLKOuOUTz/cjs8oxVx1MDAxYnOWl8/HXHUwMDFipc3adnP78lx1MDAwMI5BLzqjIGegT0OJpoxrXGLZZbqAdDJALYxcXOuY1kzPcHtTu8CFX1x1MDAxZffMXHUwMDA21qCrqI1WMHDA9StuS2rF6Uh8oSTEbG196eRcdTAwMDe85fna4XLnMdfdedjY6rxkce5ZuFxcfy6+Nu/uzOtV/pZ3WOnu8XL6499ZSlx1MDAxM1xyjuzLrKVcdOcqlkneT65cdTAwMWP4zkkq41x1MDAxM0nyw19QXCJBqkBcdTAwMTZBl1CCjlx1MDAxMIli6D5cdTAwMDMznFxcXHUwMDExUDC7jVx1MDAxZW5YIFx1MDAwNJ1Vhbbb+ufFkU1aLlx1MDAxNHosUv5GTVx1MDAwMsBcdTAwMThzociuWWuS9VKnUFlayy1cdTAwMWSVXG5onmekLEaYw2FlXHUwMDExO6is9EGxmq83XHUwMDFiRVx1MDAxZqaROVwiXHUwMDA3331/XHUwMDAzhGNcdTAwMDNR931Q18dcdTAwMDd1+Ufncru2Xn5dOYbc2+350dPpztuig1qh285cdTAwMDQlSjlt7Vx1MDAxMKZcclx1MDAwNFx1MDAxNphSTpM/puM3XHUwMDE3SlY5VkjE9L+Vy1x1MDAwNVfwbS4oXHUwMDE2OCGtlejVSCE8wiAs5z/DqVxmd1xcc/67dmr9RnDUdkKz0LbVle7R1m53r7FXXjVXm99cdTAwMGK/W1xudL/fva1ddm7t8fGDWy4u3++e64dcZq47wyouXFxcdTAwMGLBwyems9JcdTAwMDLGxOb6Wy1cdTAwMDA9TelzKlx1MDAxYePThn9JLDRtXHUwMDAw4y5cdTAwMTDWXHUwMDE4XHUwMDA2yjAt+tPQq13DTHa0kSxcdTAwMDU4Slx1MDAxMucs1dzQhlufIFBcdTAwMTHqXHUwMDAwOkvmioVE3PzPbVMv3+lcdTAwMDRBsVlqL7V6dvdno/RSbXfa/zUjVTDCXGZcdTAwMGWrgtDIllwiXHUwMDAzm4MyoKPLWGWgOXBjnfQpg8bG+Fx1MDAxON862X8rP65cdTAwMTm7UzldyXXa9Y2n0/XFxjh3OkFcdTAwMWFwQWlvTuNcdTAwMDOSgkG4fNVcdTAwMDJoXHUwMDAzoZXmlunF0lx1MDAwNlx1MDAxZuG23fLxea7zdldcdTAwMTavl3b9df8ud7TjN4pp/O6/mjaYXCI8OvG69avKgWrYyk7n+WQnZ7ZrV4eQRfh56+32qlW4foH6225xXHUwMDBi2tt7+Vx1MDAxYpmRlrGWWTZ9RbpRWlx1MDAwNq127Fx0q+lcdTAwMTVcdTAwMTVTzHfk0jhcdTAwMWKf6JJX90JcdTAwMTJcdTAwMWQwSFx1MDAxMDPIJS47opuBmlFcdTAwMGW/LdTMxUxSLDeOVSo3fVx1MDAxONpcIsqZXHUwMDExpnvR5IxcdTAwMTQ61mHhXHUwMDEy1zhcZlYt6KM8xclq+35XXV1cdTAwMTbWzl+Kq5WV3c03KVe+LzbKuVx1MDAxMyZcdTAwMTAoIazVSmophvSMVui0OMuNYEY6fEwz0TOC20Bx1UtcdTAwMDRcdTAwMDPK54xcIlx1MDAxY6J6hoo9Mtp3XkA5s94+q+zYeq4odmD37Ll+fLb62vmSM3+wnEm87k1z2zZV44V11tz+6cnW22m5XHUwMDE4s1x1MDAxY+aVwZ94XclcdTAwMGZcbur14lx1MDAxMDZaN0fb8lF215ZNRrLOOYGyYeaZXHUwMDA1YFx1MDAxM1x1MDAxY1grqWhF+Fx1MDAxYiHGr43P+MkwX0jGR9JcdTAwMDR0YFx1MDAwMSjFXHUwMDEwX264cFx1MDAwYs+Q8ZOFnTToxTornVVCjVx1MDAxZEvDjcZcdTAwMDVkQGdw3D25tHPSXHUwMDE5y9NcdTAwMWO6Tiftqu2lamephsNe6lTyjaWfjXr+ZVx0RVmh26LH+7PRxs9wjS/lazWKPP3ZKDdbS4VaXHUwMDE1P5yVXGJcdTAwMWMheIZF4PA9XGbewdLQXHIsecc/83xcdTAwMDZhdSxvOG1cdTAwMTjFl/ncweb4rJFsxVx1MDAxN5Q1jFxyXGJ36Fx1MDAxMHJtJVx1MDAxYnRcdTAwMDeVsFx1MDAwMTDLgVwi341ms4uXYchdylwifzGg1GxcdTAwMWIq8tBcdTAwMGaYXHUwMDAxdFx1MDAxOLm16Lg6ims3kYhcdTAwMTmq9lwitFx1MDAxNVwig8PvrFx1MDAxM1x1MDAxYXpb4Da8rDNOaEg+n15cdTAwMWFIaOCKaWdcdTAwMDUueXRcdTAwMTJcdTAwMWPT0fRcdTAwMDBcdTAwMTWAdE5xpGWr+Vxmslx1MDAxOZI3zlx1MDAwN4eLQ5HMOYZcIkNcdTAwMDEu1uhwcWloJ6gkopJMXHQpUlxy+K+UzFx1MDAxMI9cdTAwMTR6RTHSv15EwqVJZoglVir0XHUwMDExR6zogKLgXGIzb59YW+NcdTAwMTNrslx1MDAxYrOoxEqhzY5aKSC85GCxXGJcXKqBxU+lQd9YhZ/PZHlcZn73m1x1MDAwN1x1MDAwMj1vjthxQkkpjUeIaVx1MDAxM0inuTJcdTAwMWGUXHUwMDE0hkfiiVx1MDAwMNGkNFx1MDAxMyyDXHUwMDFktzlGXHUwMDFltG9cdTAwMWVWNo6rzdV1/bB5Vds6efy+uZ/qxFx1MDAxZK2iY1x1MDAxM8UwjcnXY1x1MDAxM2AvnYsh5FGnSIlKXHUwMDFlXHUwMDEw++FsrY+Eruw5Os9FXlx1MDAxZIJ+695fr3Ol9NlN4SCOo7m0XHUwMDE2XHUwMDE3kMZcdTAwMDXd29OdcoB/LU5mXHUwMDFjJ8ZJXHUwMDA1XHUwMDA2n4FRoMI/XHUwMDBmXGIyitKnaqeW0+71qFx1MDAwYsZDl15cdTAwMTHQZsTxsWEhsVx1MDAwMaJgqc2S8MaStVPssHrRuuBcdTAwMDSvdaA5zqZQqEzcUO6KkSwwXHUwMDE2XGZcdTAwMDOqlipCXHUwMDFlZZZcZlx1MDAwZpoqXG4xhaukV1BcYnwhotGIXHUwMDEwKlx1MDAwN2qkXHRcdTAwMDWlzz9thdJcdTAwMTPURC1cdTAwMDFS17yhR3hZXG4tx0yd5Vx1MDAxMeJk2FlcdTAwMWVcdTAwMTjIPE5IVHx4N5fGKJSI4NNnnfHhm2wmXHUwMDE2XHUwMDEyvrRdXHUwMDE5oK9cdTAwMDDOaOVgONHUKFx1MDAxNzCqk40+XHUwMDFi5WCYoYFlcz5Csd3OWJKIhlnmjKdcIlx1MDAxOH4n4uVaiYSD1uSvJclqm1trKzevXHUwMDFit2vu5Kly8Lx3dso2f3c9vFmdkDzU9vjy96f9jZPWyen1ceHu/FhlXHUwMDE2XGZqUJOLmVx1MDAwN4NcdTAwMDKLXHUwMDE2b+yHilFzXHUwMDEww7wlQLrjM4d/TSw0c/SK+Eg0+dTvQCozdLRqmVx1MDAwZdTMmYOIXHUwMDAzX4JK8sSkhphoalxiKjWrXHUwMDA06N9cdTAwMTlcdKqBXG5oTGn30+6v51x1MDAxYj9cdTAwMWL5Qqf6VFr62XhcdTAwMGZXmNXe+VxiW+jfO883lj6GXHUwMDE3XHUwMDE53DyyRWy0VVSoWpegtjLCl1x1MDAwMtZNXHUwMDExXHUwMDEzen6+fVVn9Vx1MDAxM30rLuC5fSf23VlusZHOXHUwMDFk2F5MKHm0XHUwMDA2XHUwMDFknb5M+rhcdTAwMDBcdTAwMGKAO9pEXHUwMDA10GFznG1cdTAwMTBcdTAwMDXVNubAkFOU1Mq3M+6JXG6lXSUtVFx1MDAxNtW6Mo+iOL//zsvlSv3kYW3/YGurWj8+ecug/v+sbPmsauZOoVx1MDAxMVx1MDAxMq+7XHUwMDA2rZ1CcS13l3u7qjT3V9hKp3ydkfawyOA2XHUwMDE1f0+kPSjELo6SJGVaaPCe1nVTXHUwMDA0byavwoVkJGCcXHUwMDA3UlKRbKOo0vBcdTAwMTAhXHQ1XHUwMDA3QuJSXHUwMDA3wjjrLDNcdTAwMWOs89Uxjlx1MDAwNm7SxqBzKlxcXHUwMDEw/DdcdTAwMWPva0OJvVwixfLNRH58nHej+Gi20MTna/WlfLFebcxKhIwwtjFcIuTXKGPHmE2rgOSc+bhcdTAwMDOnj0NcdTAwMTVFXHUwMDBlMy1xJkyIXHUwMDAxXt95IVx1MDAxMFxuVybTillpebRcdTAwMDI3Q0uqrWJUZoopamVcdTAwMWFdumBcdTAwMDJN1TdcdTAwMTVcdTAwMDWIS1x1MDAwNqHVMrok9yRlp/66JblvmPnmXHUwMDA1aJpGXHUwMDAx0olelVx1MDAwMV+jgIRcdTAwMTLOXHUwMDFjTYBzYMQkrVx1MDAwYpM5QjDpUvXV+3w764PemMVKr+gy7V8vYsXHPEvJsFx1MDAwNCFcdTAwMWRZXHUwMDAxWKM1XHUwMDE1XCKUhmq6RU/tZYBcdTAwMDZEUuknzknipitBONZcdTAwMTHW2FFcdTAwMTF0ykZcdTAwMDSjcSiMysCHclQ+XHUwMDA3bFx1MDAwMkaFOVTP9qFcckx3pDXVgdU4XHUwMDA3OeMkXCInXHUwMDExLHeKUcQ2hVx1MDAwNVlcdTAwMDF2KFx1MDAxY0hTdIFhdHYgqVZihF+tRurXWjhcImdcXLeegO+efnFGXHUwMDAy1fHjwqWi179Xx4NcdTAwMWJmp6ZXZVx1MDAwMeVgeHs7dDTg4ntBK43UipJtku5LXHUwMDEz7lx1MDAwZu+uiCu5nLsun1xc71x1MDAxZdbXxXOuY1iaPiFgpGXi91x1MDAxM/ey0Fx1MDAwMVXY51x1MDAxY0WIppjSgdNgjlx1MDAxY8I4taCi8pFGjrxcXCyqep+ygIrnaSe4XCLnTM3HXHUwMDEwpInewlx1MDAwNYhugtBopahDU/hsfHahXHUwMDBij9XN7VPWPDng+098f/dcdTAwMWGVYb1cdTAwMTjH+47sviRcdTAwMGKE1jTUNORzgDqwikp9UvxcdTAwMTmaZGZTXHJ4TrzvXHUwMDA3UIT3Pa1trEM1waglhtYkn1x1MDAwN1x1MDAwZsO0pNu3wqLCRrFcdTAwMDbRnp+cfpwh9nCqmfNXKsq+s82ge/pX5nnn53lfZ1x1MDAxYkM5XVwifGRcdTAwMWJcIvRcdTAwMDQ6V8KivMm6gS812Vx1MDAxMY6nSeaN+Mh7oVx1MDAxMOtcdTAwMTn2s1x1MDAxOeEyj1x1MDAxME3DLnN40Nm4xck5VcmqTfPAOmDoXHUwMDFhXHUwMDFiXHUwMDE2toDvKb1OI8Mxw7ilji6hYJ5P2Vx1MDAwNlx1MDAwMe0mXHUwMDAxXHUwMDE3hltcdTAwMWJcdTAwMGXz/oQvXHUwMDFkVEumqL1cdTAwMWUowfSXbEuC88bUuo1cdTAwMGIqKOPCbVxuQ7ujzMbmMmhcbudApzpD4fa+PqvVy4udg1xcpXJ6VThgir3kNlx1MDAwZtRYm/YjhVx1MDAxYjV4czBRrGW2wi1cdTAwMTZcYr1cdTAwMWaPQGBcdTAwMWVCKzkhe1BoMcHAgqGkZ1SWYO1cXJRWcjOwwVx1MDAxMUqhnVZgOLJcdTAwMTIzXHUwMDFhXCLjk9RAQ1NAncO7cFxmUo13TkIrXHUwMDE5XG5JQlx1MDAwYm86SWipL6E1Y2Y+XHUwMDFiX2lRO1lpTPRsnGYqln55z1x1MDAxOTOZ70paQ+FcdTAwMWJcdTAwMTNx5GJcdK1cdTAwMTE6Z/ZCKzmULFlooZ+kXGZlTympNVeDXHUwMDAxXHUwMDAxVlwie6Gra9BcdTAwMTWXUlpcdTAwMGZ8hVxy8FONXHUwMDA2WqJDaUPRQZ/4lVx1MDAwMbdSSFx1MDAxNHNcdTAwMDb/J0O9eb6E1vss9uG8Mr3OUlx1MDAwMlxyXHUwMDBls9p3/Fx1MDAwMFrGulRovIyyelwijypRZ20/Pu5+b17kNncum7Z+qWW5sDFexayROotGLCfrXHUwMDE1kK3O0jLQguNzZ9r2lED4x3mAbiWloirqXHUwMDFmjUpmZLbEcjyu6Fx1MDAxNUHUPHRbmtRcdTAwMTnqXHUwMDFkTOmhXHUwMDAy/+FUMGVcdTAwMWWy7dGo5bNDXX4+O3tr2ZO91Vb3cSdugNIyao9kmaPkQ+7J7Vx0XHUwMDEwXHUwMDBixlhhnETlZni6XHUwMDA0zDlcdLdkbH1cdLfFZfrc+LqNo/+AeFPGt0VcdTAwMTbfllx1MDAxNKVcdTAwMWWuXFw50YlH8lx1MDAxNlx1MDAxOVXNtVx1MDAxM7HuYim3XHUwMDExwmn2yi05XHUwMDEwL1m5XHR07YmdQClAXHUwMDE0XHUwMDBlNm7kTHOiYUri62VGRfBLXHUwMDE2yyhK7UOVZ/BcdTAwMDJR/DKkQEBysEyhUFx1MDAwNFx1MDAxMVKHX8rtfVx1MDAxNkN4nn6LjGbBKetcclx1MDAxY1x1MDAwMVx1MDAxZZ+UjjJcdTAwMDBcdTAwMTla6sx3yC6OXHUwMDBm2tu520e4LJxcdTAwMWZcdTAwMWXIjthTJ+1slNt7YYOJytJkq9yAYv/oIFx1MDAwZmHk0CFcdTAwMWTaIaMzSCOVoYhp2klcdTAwMWJLXHR6cdX7lFx1MDAwNYYhI2tcdTAwMDCnKZeeJWW6ZibdkqPnXHUwMDA2lVx1MDAxMaOCU1xmvT2QuFx1MDAxOJ00c8nLvmyvXq/dXG6de37Sxf3vO2fHt1d131xi8Vx0XCKnqV6rdyaprrGJajeJglx1MDAxYj9cdTAwMTUgLFBj+IVcZmpJXHUwMDA2V6J044nSTX9Jt1x1MDAxOVN9mj03zY1G3vBJt9g0Vk3NbqlodtbKrdeXy/5cdTAwMDGHmyOE0+yV2/H2XuVxu7Gycnt4v1x1MDAwYietw/rzw/VY6EWfPkDvWFljXHUwMDE51b3or+z3iDRcdTAwMTkg61xuhDZcdTAwMTXEXHT1YepvuXHKkVx1MDAwNyVcdTAwMDDFgvO2uk9cdTAwMDXfSfL1/8rwXVx1MDAxZlx1MDAxZr29XG5cdTAwMThcdTAwMWGfs+/QUvDYaDNlLII+nFx1MDAxYZBccoCBXHUwMDE5XHUwMDE0XHKpSo17XHUwMDAw/Ku+cZl+81xc0NxpPsRBeWDoUdzGjDVcdTAwMWJcdTAwMTBfrVx1MDAxZlx1MDAxNZ/OKuKqfp0vn6+687vC9+5YIDaCXHUwMDA1yKTSXHUwMDAwpdPJQefLKlx1MDAxNCDAXHUwMDAxZVxupbWIqFx1MDAwNVai16vccVx1MDAxNJ1GxHRC+8JwXHUwMDAyhmO8LVx1MDAxZoihV67ZMfD5VVLEbohcdTAwMGKGQlNynrVcdTAwMTVcdTAwMDZq2Fx1MDAxOa71N1x1MDAwMYh33j9eZOxcdTAwMGVcdTAwMGYxXHUwMDFiyN6dX7ePjPh+uHd+UfxxpjevV+TrmHZcdTAwMTdcdTAwMDJnXHUwMDFjPn1cdTAwMDZa2lCrhI9cclx1MDAxM1x1MDAxNXDoLVx1MDAxNPTQRKjbYD9NjJNuJs9ccphixlf8+1x1MDAwYrNcdJjdXHUwMDFmXHUwMDFms71cdTAwMTRcdTAwMTAmfCfVyiWk0DDOXGb6hqHqTVlBlkJlpoLsSqFDRWPLzdZccmrbUmOhwVx1MDAxYj/YbGC89VB8u9xhpyu1lW63/lI7lDeaj2d5mVxiLMWDoY9rhFx1MDAxOapDyI2BXHUwMDAwKZuDQcvqfM7vXHUwMDE3invvToziw/FR7OiE0krmy9Ww0Vxmjk9cdTAwMThcdTAwMGJcdTAwMDOgQLjMYYxcdTAwMGZASz5R27YvXHUwMDE40/+GYHx78FwiXHUwMDFhR7VcdTAwMDfjyvauwuz3k+8ruTFhbFx1MDAwM4f6l7KXnTZDhSiEw0+tXHUwMDA1a6k0pGXRXGLfLy/4492JcXycXHUwMDAyx5Q5J8KdMvs4NiohdMxIobPfxqLwWsUnOzz48oIjIE6u2jFcIn2dcKq11JRIJFx1MDAwNlGM1jdAxqVcdTAwMDBcdTAwMTVjwjWu+5VgXHUwMDAy0lWaoz2WLHzuXHUwMDE1qvmEmp2B4ZT+rsOu9Fx1MDAxN6Lfp7CP6K2pXHUwMDBmIFx1MDAxZG07XHUwMDFi7VxyXHUwMDFk4zw+0sBcdTAwMDLngplwSaVZ51bqvVx1MDAwM72z37KX55u1R1Hrmp397na6mnNkeiZqeZZtUnz420JcdTAwMDRcXCpHZc6pjr1Uo346XHUwMDBlQfSKYKd/scjT+S1RYkwwXFw0KMg4RfIhq4e+NrOTxjRnobRxZDWd1+IoXHUwMDA1iGmPQud0sujHxlx1MDAxOKrsvVx1MDAwNLBFQ68pV3qoXHUwMDFhj1MqoGaXWlqqJa6iOyQpq/d+Ufj7pPUp/EeKg0WrUT9cdTAwMWJvkZE4onbOcCU0m4SnR1Qh6mU8TSPIXHUwMDA2ylxi/76DxVx1MDAxMWJopsWPxyl2lizHLFx1MDAwZlx1MDAxMJ1AtdRcctNmaG+Ec1x1MDAxYlxi4YAqJKOCXHUwMDE20XJCPFDGUMdsoSgvTPq8KqNcdTAwMDMwlFxygL/JobLrT8RcdTAwMTee36exj+ftqSVcdTAwMTlcYqqK5oQ39DO+XHUwMDFmMHeaKjJMhPRcdFx1MDAxNVnxzW19f3rcvLg65M2V29Pn05X9bqpKfNqocJe336XIeCBcdTAwMWSwXlx1MDAwNWuKolx1MDAxMWFd0itTZIBcIsJcdTAwMWMjqVx1MDAwNmakRotcdTAwMDdcdTAwMTW9XCJwmodKSyOCXHUwMDA0enZGWlx1MDAxY7ilrZhozaNcdTAwMTmItLXcxevFSmfz8eZqtXJbWmvd7tzcx4SD9fqcMdTLiklrPLU4VMDRQzXSXHUwMDE4RilnaiFLXfjRM55mc4GTXGY0WE1NVYaS5bnQXHUwMDAxyjrD6V/Oo074l2j7tVomJflcdTAwMTThYJxZcEYp51x1MDAwYiiJdbCRXHUwMDFiXHUwMDE1ZfxkXHUwMDFlXHUwMDBmZlx1MDAwNY5kslx1MDAwMkNcdTAwMGKm20aopnnotkTO+kRwq1TovGNoXHUwMDAwxr14fknNkKxcdTAwMTGCOzNcdTAwMTTPz51cdTAwMGKMZbTVgmaCRcPCXHUwMDEwvIHgoFx1MDAwNXr8wjLpa41FPeWUoUNcdTAwMTlcdTAwMGWgQvuy2YA6r4q2XFz+M0C9M65y+5XLXHUwMDE3LVSG+lx1MDAwMSnV+pBcdTAwMGXxLUwk7f04PtFcdTAwMTH2pHXK1m/X2O5be7e5oX9wVlhbudRcdTAwMDdzqM2ceN0pOmePVppG4sSkcVdcdTAwMTNw7396Y1hu7tBd651IapKTjlxyxnFT9chkyOP71DRBO3L58C/eKLKsQf4nWe7dXHUwMDE0hltIrSmz2ueIcVx1MDAxNVx1MDAxZlx0KoWTVCs0c9NNWSSpeqZETPd6XHTn6VeN5GJcdM1pzW/L48pDz+AgbJRcdTAwMTlcdTAwMWQ25Mn3kI1l330p2K23S8T19tlcdTAwMWRsPa/t51489Vx1MDAxNWItu8TrXHUwMDBiXHUwMDA1XFxcIpYppnBcdTAwMTDmXG5cdTAwMTDFUlMuh2NSiuhZ91x1MDAxOKbdftnyXHUwMDA0mI+dmlx1MDAxN2vMteO9tlq+SFx1MDAxNuVia1dR9r6zdlx1MDAwNql5y8Uju3Ypznb2NqHdfGift/h2yW9cdTAwMWXTdJxYQGueeN3W2+1Vq3D9XHUwMDAy9bfd4lx1MDAxNrS39/I3MoPr3jS3bVM1Xlhnze2fnmy9nZaLnYzUh3svlZqN+kheXHUwMDA3iepDmUC4XvNVjaw0mIbClVx1MDAxMCNYXHSs6KlcdTAwMGYhXGa1TPBtXHUwMDFimC/xkcBKqbZccpjTaFx1MDAwMKz3uCdefaBcdTAwMWLJ0clnWVx1MDAwN9JxR6pVTFx1MDAxNYHzbrlcdTAwMWLd+k2ptdQsL3VcdTAwMWKll4dqq1RcXPrZaOOj+FxiXFxb6lRKS902faeBn+Rvmk+lXHUwMDBme//btcpcYmHg1yq+O467X//dZqNqflxc5K6LK/LifC93t3LytLXxenvlYY5YVWOpdDzKXHUwMDExjj6KXHTH0b72XFxcdTAwMTOhXHUwMDAyq4VykvJMmSdcdTAwMDdcdTAwMDYoXHUwMDEw14JcdTAwMDVU1pKqrHhcYkR8yZpcdTAwMDRcdTAwMDIpTi9rXHUwMDA0UFx01aiA6fmf8cdL1lJq8WStKlx1MDAxMmVNaf1NdVx1MDAwZvaeXFz+ftdsrVx1MDAxZG9ubK1cdTAwMTS+ZM1cdTAwMDLImsTrrlxcndZPXHUwMDFm4fnKvLbNxtbOwZl82cjiusel581cdTAwMGKzWr7bNd3jSuNquaMz2lx1MDAwNFx1MDAwMlx1MDAwZYpcdTAwMTmdZu87gUyT123i8VxysICaWFxupaxcdTAwMThqXHUwMDAwRC7jXGJcdTAwMTblRpJcbqP6XHUwMDE0KiaV/0uEJXBoLY1cYuNAJcOMt4Ixh9iwXHUwMDFiXFxvTJHfnnVcck1gqFxi5XRxN6utXHUwMDEyXnIp/0uDzEVUjTjMXHUwMDE5IU2GdVX8LWSjlI52XHUwMDFmmyer5SZ7qO2dPD8/rW/d72ykUEraUUlcdTAwMThmpVPCXHUwMDBlXHUwMDA15Vx1MDAwML5cdTAwMDLpNGOUtWQ8MslCYFx1MDAwNKU4UL2TUDBCaPNHUmOlXkdccvyGgKz7e/1RoulhatHEpWZKXHUwMDE4w/yqicXHSSNkLVPhXHUwMDFmzEg2fembWembUWdn4u7y7nCj3Dg7Wrv4Xi5vrN8oe7tcYvKGKaRcdTAwMDKRJr49gVx1MDAwMf13OZa8UdRcdTAwMWRIcG5cdTAwMDFcdTAwMWNVn1x1MDAxOWY/k8h+nDuSN6SMUFwiob3z9TfMXuD8OWXpdlspXHUwMDA0XHUwMDBlOuFaSuPdZeI6dpebzFx1MDAxM3VcdTAwMTjIXt5QXHUwMDFlbyjde1x1MDAwMnmTy9+Xln6cn/x7+//99u2iXHUwMDExOmJY1niHno2iaT9drlx1MDAxZj7lX7d+XHUwMDE0XHUwMDBir9c3Ty+Xd6uejqU+PDNcdTAwMTFcYrBaXHUwMDAxXHUwMDAxVlxm7Vx1MDAxYVx1MDAwM/XTk4p2XHUwMDE1XHUwMDE0R7x6iihcdTAwMThDpU+EYo5OxOaTt/knXHSYvVx1MDAxNHhWzljaN/Zcblx1MDAxNVx1MDAxN99cXInC+Vx1MDAwN1syZXZo7ahcdTAwMDHBNIBuNmqvS/lCp/pU+o9uo7b4dVCSXHUwMDA3nFxynpN12oicXHUwMDAxXHUwMDExKCWZ1ihcdTAwMTjMMJ5cclx1MDAwZlx1MDAxY4ByoNEwuOghXHUwMDEwXHUwMDBinFx1MDAxMI5pR7FcdTAwMTHChZOBw2Fn6PhwabhmjlxuM3yhO1x1MDAxNt0xJVJSJFxmSIbzJLnfOZGxJ0WcSlxiS1x1MDAxYi4mmVmfXHUwMDBmqbmcKFAl4+7DcUuVXpFF2r9cXERqZ1x1MDAxNoY/dpg7XHUwMDBimFx1MDAxMVx1MDAwZYxVqJOdkE5Ei566QFx1MDAxYSRuRjdHYZ4zXGLLXHUwMDFmu3VcdTAwMTOF5VOBVlx1MDAwYlxu/8E/WVx1MDAwN55cdTAwMTL7qUY4p0D8ZNd0XHUwMDA0lcrAaktcdTAwMWQpKD1yKFx1MDAxMF8qXHUwMDE2aGepXHUwMDBil1x1MDAxNsxANPtcbpWVRGutkGpRbYeaSYVO1CUuU2VRaNEk86zT4f8kKj2YmkrRaVVDvY9C1Vx1MDAxZlV8XHUwMDE5KsQh9cvhXHUwMDE57vOM2odYr+X4wXbldG+vKk9cdTAwMWW7cqfUgutUUSl0om9/fz48Klx1MDAxZIaiXHUwMDE1iVj16if2XHUwMDAzU+glWFx1MDAwMCA5Rclb65TkI1upxIKKXsNwmlx1MDAwN+mn6X8nKDBcdTAwMTh1OPI64FKcS4J8clDDXHUwMDEwyfeay1x1MDAwM2pcdTAwMGWNrGZttP2dXGLAXHUwMDAx11Ssi1x1MDAxMlx1MDAwZlx1MDAwNjrQL1xm5/vBM5Y7bEQgLYDmWuI6c0NcdTAwMTm30qjAKKR8sDiBji9IyvyfRPMpypFxhr+Qqir7+Dz29E5YzZFcdTAwMWKzL8ZtLW2RTLW7tSDJVyM001x1MDAxY5Kvkjf+k1WbXHUwMDAztClcdTAwMTbNgzBcdTAwMDJcdTAwMWSnofBszV1cdTAwMDCCkqk1+Vx1MDAwN9xTkDtQ1N/LXHSgWu+cWc+WlmRcdTAwMDGnrlx1MDAxMGhpLMp0m3XS/J+E6KPphVx1MDAxYlx1MDAxMinjLlxcfzt8Qlx1MDAxNy0r2t/5soKb7HtdOkVcdTAwMTZyXHUwMDExxFXcUqVXZJHOQ1x1MDAwZY0tN3pqQzChXHUwMDFjXHUwMDAzKTT3OJQqXHUwMDAwK0BcdTAwMThNe57GQDqxMZY6at/vqqvLwtr5S3G1srK7+SblyveY4TIrtMSxgrNcbv+s/lwi5YO61e65eVqr1PbVhru/4Vx1MDAxN7XrPTZcdTAwMTaXcidVIMm/XHUwMDE1eOfKXHUwMDBlecBGu4BZ4I42qKyvOKu2XHUwMDAxQ0GO/+Kjs9xDpKj/qSlcdTAwMWYlupNoN1x1MDAxMCrNn02nkj+JSk+nplLAlUtcdTAwMWWXL/GFm/iMdWVcdTAwMTXp4Vxm815GecCHZ83XlefaWbFTX4NcXHP7ent3/SZVQCAjP/D3b1QuXHUwMDBiXGLQ33PSUd16KnaZ7sfjQNT76Sh85kHyY7Nmb+OQXHUwMDE5LayhglKOM+frzZk9rz/f28JRpWBqXHUwMDE3sr25vFe9zKnVxzheR9voqOBcYpXT0qHePp9cdTAwMDPEXHSU3KCUXHUwMDEw6CA7s5BcdTAwMTVH/HhcdTAwMTnD6UXqNoHWVqNepmr5MKSYjVx1MDAxYUXzv6NcdTAwMDHVn0TraTKHiIjxQftiVmNlMKBjKnBeJ1xuVEtcdTAwMTbCWoFccrlYf9lcdTAwMDZUI0TS7Fx1MDAxYlBJflBQr1x1MDAxN4ew0bo52paPsru2bMb1eFWgqP9cdTAwMTSadYskNljmkVx1MDAwZexQxFknJUD4XHUwMDFj8XPLKkBQXHUwMDBi6SxiXHUwMDFmSdBzSqFs4OiQg3GF6lenkmh/N2/3YnpvV9FcdTAwMTGb9celS1x1MDAxZO/sWkNBPGqiMvtcdTAwMTOKtDu9efx8fnl5lj90T3Ll7uR7feMtVVijXHUwMDEwiumJXGIkW5HGXHUwMDAyS2lcdTAwMWKoXHUwMDAwqFx1MDAxMFx1MDAwM1dmQGdcdTAwMDF1XHUwMDAxNVx1MDAwZd1VnFx1MDAxNmo0PrJKXHUwMDFjoHsrLKd2UFx1MDAxNmWfhlx1MDAwMdVcdTAwMTdB1OKpNk2FXHUwMDBlXHUwMDAwXHUwMDA3KXHNhaI8ZqnaOq2zcyNcdTAwMGXvIb8pl1e2btbay24/RrVJJi1oqmOPXHUwMDEyXHUwMDE4+lx1MDAwNnHC8c1JpflcdTAwMDEzhkpcdTAwMDOUYLhENVx1MDAwM1x1MDAxMqtcdTAwMGWGnXHrRMBcZnKAXHUwMDEztOXlUWlfR1x1MDAxM7+Wx4TUfplCptFcdTAwMTFcdTAwMTk+Z+ZN8I5jcKM0utiT1YpKlmnosyg9VZzeglx1MDAxY02MkElzOJpI5qhPXHUwMDA0XHUwMDE3q/l6M9zc4aNIg1xmqGag6kXzazaYZU1b3IFQVksqXHUwMDAyJbRHq1lcdTAwMTOQjVx1MDAwMvTXuNHg6XbEZfbFXHUwMDFk/5z4+XxcdTAwMTaHXHUwMDExylxmNiVcdTAwMGKVfIyv3cBcdTAwMTly9kBV7Zmrs9yqlqJrzttXby/3XHUwMDBm7sfqxY/74rjJLMlIXHUwMDFi+P1cdTAwMGKRW6yfTe74/smelWD55fkpr7dM5XW8645UqVx1MDAxYyedpVxupkkgXHUwMDEw/6yMJVx1MDAwMVx1MDAwNFx1MDAwZmgjkNojWVx1MDAwYsM1XpxcdTAwMTnBXHUwMDFlXFyghGBcZvWSwYto5jxcdTAwMWLy+ktcdTAwMDEksEeanVx1MDAxYVxcM9YoIXxcdTAwMTJcdTAwMDCSos0sukTcsUn22kdkXHUwMDE3XHUwMDBiydVU1WGr7aWfjdN26ahUbpXalVx1MDAxM5yPxs9GqZG/qZWK/7VcYlx1MDAwMmGEeVx1MDAxZVx1MDAxNlxieENDt7NcdTAwMTS9m2yEQzLxjd7hQfQ6UFx1MDAwNkjCXHUwMDBmXHUwMDAwv1cymnoyoZvGtFG+NJ3AcCZcZrJcdTAwMDbFd4XzXGL7wJeBQf/BWWFcdTAwMTSaKpN9a6Y/R0VcdTAwMTSmVlx1MDAxMXRcdTAwMWPPvfFcZmBjJYSgevrSqElClyaNQ91p5Jvl+9uH+936weHt891hq76WboPHWpmqNuvn21lv8DhcXPqOKVTdrrdcdTAwMWZcdTAwMWH+ebBcdTAwMDFYTv2LLFx1MDAwNVx1MDAxYSDGRl2QXHUwMDA1xllcdTAwMTRcdTAwMDVcdTAwMDJcdTAwMTiA0VJcdTAwMGXmXHUwMDFmXGajqX+5yCPLbIMnzfZcdMXzU7FwJFx1MDAwNXBcdTAwMWFEeLdkZvs7yYUgllx1MDAwNnegaFwirEPaY5JbcJ5cdTAwMDFcIqlJKsOhKGSTa76Q53J+XHUwMDA0jSX3rFx1MDAwZSj2xVx1MDAwMkXIsMFtfYqqplBVlFx1MDAxONTeXCKkMmZ1Kvd3I/mYclxcXq0n0bziXG70dsaNzctEXHUwMDE3XHUwMDEyZZ7JOlx1MDAxMFx1MDAxNVx1MDAxN1x1MDAwM1x1MDAxZORO1HhlsVx1MDAwZeVGKKbZXHUwMDFmyqni7s3xNjt6rL41915cdTAwMWInXHUwMDFiy83uWNjl6KJcdTAwMDVGSEB3zHGmhlx1MDAxNVx1MDAxYihcdTAwMTeglKO2a9pcbuFJXHUwMDFlXHUwMDAyJWm7lrpeK8lcdTAwMTn3ZWJ+7fQkwLeUXHUwMDAyvmQ8XGbCxlx1MDAxYl9cdTAwMWFVaf1cdTAwMDIwUnHU3Vx1MDAxM8WSJ1x1MDAxNoBZ1Vx1MDAwNbdW/iE3b+vNc9D1jcvX7eKgPpuiklxilYCdqtJ4vlDAp3ndeXf/Wu/e0/tfl/69Wl7KPzzUXHUwMDEw6OhG+as0LEou90T3kZE3mFgrZoQ3aFx1MDAwMqrSa4R2VGhzsM9cdTAwMDBwo4LeuSpt8yjui9dcdKgnJLmCnNKuwFx1MDAxN+CuXHUwMDAyzSVcdTAwMDV3SGlcdTAwMDDFbX/GvqjmfVx1MDAxYftUU/ZTTZqwTGaFpOpv3vOi+FxuVGhAuFJqXHUwMDA2e0Wpy1xyzcZti12q9Iou0v7lXCI0mJmXNbZcdTAwMTODPoxcdTAwMTNOXHUwMDFi4ajdRy/hJOLDSOrgaD5C76xcdTAwMWI4xs7I6UouUzPkdFx1MDAxOWY0o1x1MDAxZEhAt9XncmmDk0EhaPi5006lXHUwMDFh75xcXK61w9PKs3g+uj9mqllQpe87R/eenbZSrVZ9aEdcdTAwMGIhq4CD5kKSXCIwQ+QqNFx1MDAwNJTHqpwylLTgqaOuKNxXSquERlx1MDAwNuahUL1+2ndcdTAwMWavo9lcdTAwMTTKriTl34dNb6dmU0r0XHUwMDA26/w1M3R8kLtxRsJkmYGzUnP/9F931Ladutu965bOcOZcdTAwMGJC5qurbG2js5JKJGpBZTNS8H9cdTAwMDJcdTAwMWX9o1x1MDAxOcONXCJXNkBDpFx1MDAxZPrKzmo5VG5cdTAwMGXxXHUwMDE5KCVoM1Vy6TnwkrZXnVxuZZBRSFxcvpCXVE7U31x1MDAwZYtpXHUwMDBlvJAwccKY9OaWRI/L+1x1MDAwN16StsKz76k0wVwijng6KPRcdTAwMWaajeLSc7VToWpp1Mygk+9020uFZrE0XHUwMDE352ZUp8RkYzfs8kRuyHs72fg4ybyX6ONwhsqPitJr6Wwouu9cdTAwMDP3nKFcdTAwMTVWXHUwMDEylVx1MDAxMzVKNb7O11x1MDAxNKKBkpCKXHUwMDBlOsW8yEfysNLhylx1MDAwM0uJXHI6zZHX341cdCrT+zicgaBcdTAwMDY0XHUwMDFleoCEuFx1MDAxOYcmXHUwMDFi3aPsyUHacFx1MDAwNMTv83DiViq9omu0f72IXHUwMDExz8zFSd73XHUwMDFjdHGsoWqwXHUwMDFhPZyeg1x1MDAxNnVcdTAwMTlcdTAwMTjeXHUwMDAxUNFcdTAwMDVBRzVs4CtZ9ZdO5MAhXHUwMDE3RzHmuFx1MDAxM1x1MDAxY6RFXHUwMDE313PwJXvnKlx1MDAwZSh+U6FtXHUwMDEyqVx1MDAwNjwnXHUwMDFm5+Jue4u5/Pnyw2PZvrCG2ny5r0S5NaaIOVxiXHUwMDAxNC1AkaHMhVx1MDAxYqG9h1x1MDAxMlmkdk41I5VRUoVcIo367V5UIJnmXGJNi/9cdTAwMGInXHUwMDA3x21cIkGqkFx1MDAwMpR8XHUwMDA1+Pvwa3VcXH6Nb1Crmdba29BSsNhwI2Vwem24oN/MY1xublsnXHUwMDA1qNz+2HHVu0N7c1Q7PDs7mkOYX+J19/fKlirur+cveGVja7N7/laqjXfdXGJcdTAwMGZHw1x1MDAwN51cdTAwMGWHgk/lTPmf3jjOlEA8XHUwMDFhq6icLzEgXHUwMDFmgrzjgVx1MDAwMkW1tZBcdTAwMTO48EA+8/60fzOM36VwppihXHUwMDFlI9afqG/jo1x1MDAwN0FcdDdYKiUjvYRcdTAwMWWcXG5P+eS9ST6i7ZZ64XZeXHUwMDE3as79SUbY0pj+JDG3kY3rlExzia5cdTAwMTNcYlx1MDAxZFx1MDAxOMeNQ8ts2FBcdGD0+CioXHUwMDA0yKtCs+w8UcLIXHUwMDAz/Tgp1MiehFBcdTAwMTGgyXdcdTAwMTRW4vCLKKS+LHs86u+n95yY1VxuJ9TbrjZcXOEqcjzEqK2bnmfhyoubo0Jjv/6j+vyYdydcdTAwMWJwdbebv0xnLNFcdTAwMWKarElApn6ZsIHg6JRcdIVcdTAwMTKY6khccvhly5GDqVHXi4VV73JRRPWvXHUwMDE3eWS/I2CQMpKdoYRVqidcdTAwMDUyXHUwMDFjazezeMFkYlx1MDAxZVx1MDAxZZ/S5ImitLHOqqgjKlx1MDAwM1x1MDAxNfJGlDBcdTAwMGJ5duXHzzjyXHUwMDBlNLWOk3Tkr5D75VBcdTAwMTmAYX1cdTAwMTdcclx1MDAxMkeXXHUwMDEwV59TkkxcYu03fMm7VESfpvdcdTAwMWMlZVx1MDAxYcOdN4ksjs/RVWdcXLiJXHUwMDEy/Edpu0k5d1R+6Jyl3Fxi3TSH/NBkRzJRuSE5XHUwMDA1zPXAh5ZluPosXHUwMDE1OkVcbqOax4Ih29modFx1MDAxM3hcdTAwMDEuKclHUeQg8yDY2kBJK1BMSOvCRc2/8Pw+iX0816fP8rA0X9JcdTAwMWLVI0It46NcciaB6lx1MDAxZOpJnLj/TpBWoJSb7Dws21xcXGZJ5TQkJVVqKpBcdTAwMDFuQFtcdTAwMTlcdTAwMTNozoD6p1K7XHUwMDAxZ0ZeL3bd02t4xc9DWI0tXFx6iVx1MDAwZdJIsMBcdTAwMTU6/GB4VFlRx0htXHLXVPBCpJMtXHUwMDAzOit6K5lcbpp/fDzLb/mHh+NcdTAwMGUuvc8n9O2pWnrOefmCXuRcdTAwMWb0hkx8UeqB7F//+Nf/XHUwMDAxXHUwMDAxs6MjIn0= - - - - - 404 Error423 Error403 Error403 Error404 ErrorStartPayload: {"email": "you@email.com","password": "xxxx-xxxx"}Fetch DB Recorddoes recordexists?does recordexists?is it less than max concurrentsession allowedfor client?Yesis it anactive record?is it a client or realm admin?NoNoNoNoNo record foundLockedAction forbiddenAction forbiddenNo record foundYesYesFetch client detailsFetch number of unexpired session for the user on above clientCreate a sessionBake JWT(s)only active/unlockedYesNoYesis UseRefreshTokenenabled?Noaccess_tokenrefresh_token (if applicable)Respond with 200 status codeCreate Refresh TokenYes \ No newline at end of file diff --git a/assets/images/flow-charts/2-admin-login.png b/assets/images/flow-charts/2-admin-login.png deleted file mode 100644 index e3ca636..0000000 Binary files a/assets/images/flow-charts/2-admin-login.png and /dev/null differ diff --git a/config/env/default.yml b/config/env/default.yml index 13eaccd..51b9064 100644 --- a/config/env/default.yml +++ b/config/env/default.yml @@ -7,7 +7,8 @@ database: uri: "postgres://postgres:1234@localhost:5432" name: shield secrets: - signing_key: 87cc20e216861442af1bc993ad6f274b61bb65b956444da50f446b2046c0d260 + signing_key: 87cc20e216861442af1bc993ad6f274b61bb65b956444da50f446b2046c0d260 # NOTE: This key must be changed in production by providing SIGNING_KEY env variable + api_key_signing_secret: 87cc20e216861442af1bc993ad6f274b61bb65b956444da50f446b2046c0d261 # NOTE: This key must be changed in production by providing API_KEY_SIGNING_SECRET env variable logger: level: debug admin: diff --git a/docs/latest.md b/docs/latest.md deleted file mode 100644 index 44736f0..0000000 --- a/docs/latest.md +++ /dev/null @@ -1,106 +0,0 @@ -# Shield: An Advanced IAM and CIAM Solution - -_Not ready for production use yet. Please use with caution._ - -![Introduction](https://raw.githubusercontent.com/shield-auth/shield/refs/heads/trunk/assets/images/shield-hero.png) - -Shield is a robust, multi-tenant authentication and authorization solution -developed by [Mukesh Singh](https://linkedin.com/in/ca-mksingh) for modern -age applications. It provides a comprehensive set of features to secure your -applications and manage user access effectively. - -## Key Features - -- **Multi-tenant Support:** Manage multiple organizations or projects within a - single instance. -- **User Management:** Efficiently handle user accounts and permissions. -- **Role-based Access Control (RBAC):** Define and manage user roles and permissions. -- **Session Management:** Secure handling of user sessions. -- **API Key Support:** Generate and manage API keys for secure programmatic access. - - - API Key Rotation - - Rate Limiting - - Expiration - - Blacklisting and Whitelisting - - Revocation - - - -## Getting Started - -### Prerequisites - -Before you begin, ensure you have the following installed: - -- [Docker](https://docs.docker.com/get-docker/) -- [Docker Compose](https://docs.docker.com/compose/install/) - -### Installation - -#### 1. Clone the repository - -```bash -git clone https://github.com/shield-auth/shield.git -cd shield -``` - -#### 2. Set Up Environment Variables - -```bash -cp .env.example .env -``` - -#### 3. Build and Start the Containers - -```bash -docker compose up -d --build --wait -``` - -#### 4. Retrieve Default credentials - -The default credentials are saved in `/usr/local/bin/logs/default_cred.json. -To view them: - -```bash -docker exec shield-shield-1 cat /usr/local/bin/logs/default_cred.json -``` - -_Note: If the above command doesn't work, use `docker ps` to find the correct -container ID for the shield container._ - -### Resource Initialization Flow - -The following diagram illustrates the resource initialization process: -![Resource Initialization Flow Chart](https://raw.githubusercontent.com/shield-auth/shield/refs/heads/trunk/assets/images/flow-charts/1-shield-start-transparent.svg) - -### Usage Guide - -#### Admin Login - -To log in as an admin, use the following endpoint: - -`{YOUR-SHIELD-URL}/realms/:realm_id/clients/:client_id/admin-login` - -Replace `:realm_id` with your realm ID and `:client_id` with your client ID. - -Example curl command: - -```bash -curl -X POST \ - https://shield.example.com/realms/:realm_id/clients/:client_id/admin-login \ - -H 'Content-Type: application/json' \ - -d '{ - "email": "admin@admin.com", - "password": "12345" - }' -``` - -### Admin Login Flow - -The following diagram illustrates the admin login process: -![Admin Login Flow Chart](https://raw.githubusercontent.com/shield-auth/shield/refs/heads/trunk/assets/images/flow-charts/2-admin-login-transparent.svg) diff --git a/entity/src/extensions/active_enums.rs b/entity/src/extensions/active_enums.rs index 1605418..0905241 100644 --- a/entity/src/extensions/active_enums.rs +++ b/entity/src/extensions/active_enums.rs @@ -1,10 +1,16 @@ -use crate::sea_orm_active_enums::ApiUserAccess; +use crate::sea_orm_active_enums::{ApiUserAccess, ApiUserScope}; use std::cmp::Ordering; +pub mod role_level { + pub const REALM_ADMIN: u32 = 10; + pub const CLIENT_ADMIN: u32 = 20; +} + pub mod access_level { pub const READ: u32 = 10; pub const WRITE: u32 = 20; - pub const DELETE: u32 = 30; + pub const UPDATE: u32 = 30; + pub const DELETE: u32 = 40; pub const ADMIN: u32 = 100; } @@ -13,6 +19,7 @@ impl ApiUserAccess { match self { ApiUserAccess::Read => access_level::READ, ApiUserAccess::Write => access_level::WRITE, + ApiUserAccess::Update => access_level::UPDATE, ApiUserAccess::Delete => access_level::DELETE, ApiUserAccess::Admin => access_level::ADMIN, } @@ -23,6 +30,19 @@ impl ApiUserAccess { } } +impl ApiUserScope { + fn to_level(&self) -> u32 { + match self { + ApiUserScope::Realm => role_level::REALM_ADMIN, + ApiUserScope::Client => role_level::CLIENT_ADMIN, + } + } + + pub fn has_access(&self, required: ApiUserScope) -> bool { + self.to_level() <= required.to_level() + } +} + impl PartialOrd for ApiUserAccess { fn partial_cmp(&self, other: &Self) -> Option { Some(self.to_level().cmp(&other.to_level())) @@ -34,3 +54,15 @@ impl Ord for ApiUserAccess { self.to_level().cmp(&other.to_level()) } } + +impl PartialOrd for ApiUserScope { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.to_level().cmp(&other.to_level())) + } +} + +impl Ord for ApiUserScope { + fn cmp(&self, other: &Self) -> Ordering { + self.to_level().cmp(&other.to_level()) + } +} diff --git a/entity/src/middlewares/mod.rs b/entity/src/middlewares/mod.rs index 0673fe0..b4d28ca 100644 --- a/entity/src/middlewares/mod.rs +++ b/entity/src/middlewares/mod.rs @@ -1,6 +1,7 @@ pub mod api_user; pub mod client; pub mod realm; +pub mod refresh_token; pub mod resource; pub mod resource_group; pub mod session; diff --git a/entity/src/middlewares/refresh_token.rs b/entity/src/middlewares/refresh_token.rs new file mode 100644 index 0000000..435d95a --- /dev/null +++ b/entity/src/middlewares/refresh_token.rs @@ -0,0 +1,29 @@ +use crate::{models::refresh_token, utils::check_locked_at_constraint}; +use async_trait::async_trait; +use sea_orm::{entity::prelude::*, sqlx::types::chrono::Utc, ActiveValue}; + +#[async_trait] +impl ActiveModelBehavior for refresh_token::ActiveModel { + /// Will be triggered before insert / update + async fn before_save(mut self, db: &C, _insert: bool) -> Result + where + C: ConnectionTrait, + { + if let ActiveValue::Set(ref locked_at) = self.locked_at { + check_locked_at_constraint(locked_at)? + } + + if let ActiveValue::Set(ref expires) = self.expires { + if expires < &Utc::now().fixed_offset() { + return Err(DbErr::Custom("Expires must be greater than created_at".to_owned())); + } + } + + refresh_token::Entity::delete_many() + .filter(refresh_token::Column::Expires.lt(Utc::now())) + .exec(db) + .await?; + + Ok(self) + } +} diff --git a/entity/src/models/api_user.rs b/entity/src/models/api_user.rs index 9cadd2c..c492f2b 100644 --- a/entity/src/models/api_user.rs +++ b/entity/src/models/api_user.rs @@ -1,7 +1,7 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 use super::sea_orm_active_enums::ApiUserAccess; -use super::sea_orm_active_enums::ApiUserRole; +use super::sea_orm_active_enums::ApiUserScope; use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; @@ -10,15 +10,12 @@ use serde::{Deserialize, Serialize}; pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: Uuid, - pub secret: String, pub name: String, pub description: Option, pub realm_id: Uuid, pub client_id: Uuid, - pub role: ApiUserRole, + pub role: ApiUserScope, pub access: ApiUserAccess, - pub created_by: Uuid, - pub updated_by: Uuid, pub expires: DateTimeWithTimeZone, pub locked_at: Option, pub created_at: DateTimeWithTimeZone, @@ -43,22 +40,6 @@ pub enum Relation { on_delete = "Cascade" )] Realm, - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::CreatedBy", - to = "super::user::Column::Id", - on_update = "NoAction", - on_delete = "Cascade" - )] - User2, - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::UpdatedBy", - to = "super::user::Column::Id", - on_update = "NoAction", - on_delete = "Cascade" - )] - User1, } impl Related for Entity { diff --git a/entity/src/models/refresh_token.rs b/entity/src/models/refresh_token.rs index 7f7074e..570f5fc 100644 --- a/entity/src/models/refresh_token.rs +++ b/entity/src/models/refresh_token.rs @@ -12,6 +12,7 @@ pub struct Model { pub client_id: Option, pub realm_id: Uuid, pub re_used_count: i32, + pub expires: DateTimeWithTimeZone, pub locked_at: Option, pub created_at: DateTimeWithTimeZone, pub updated_at: DateTimeWithTimeZone, @@ -70,5 +71,3 @@ impl Related for Entity { Relation::User.def() } } - -impl ActiveModelBehavior for ActiveModel {} diff --git a/entity/src/models/sea_orm_active_enums.rs b/entity/src/models/sea_orm_active_enums.rs index 5383974..0724ddb 100644 --- a/entity/src/models/sea_orm_active_enums.rs +++ b/entity/src/models/sea_orm_active_enums.rs @@ -13,15 +13,17 @@ pub enum ApiUserAccess { Delete, #[sea_orm(string_value = "read")] Read, + #[sea_orm(string_value = "update")] + Update, #[sea_orm(string_value = "write")] Write, } #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] -#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "api_user_role")] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "api_user_scope")] #[serde(rename_all = "snake_case")] -pub enum ApiUserRole { - #[sea_orm(string_value = "client_admin")] - ClientAdmin, - #[sea_orm(string_value = "realm_admin")] - RealmAdmin, +pub enum ApiUserScope { + #[sea_orm(string_value = "client")] + Client, + #[sea_orm(string_value = "realm")] + Realm, } diff --git a/entity/src/models/user.rs b/entity/src/models/user.rs index 9620490..af66820 100644 --- a/entity/src/models/user.rs +++ b/entity/src/models/user.rs @@ -10,7 +10,6 @@ pub struct Model { pub id: Uuid, pub first_name: String, pub last_name: Option, - #[sea_orm(unique)] pub email: String, pub email_verified_at: Option, pub phone: Option, diff --git a/migration/src/m20220101_000003_create_user_table.rs b/migration/src/m20220101_000003_create_user_table.rs index ce6590e..dacbe66 100644 --- a/migration/src/m20220101_000003_create_user_table.rs +++ b/migration/src/m20220101_000003_create_user_table.rs @@ -16,7 +16,7 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(User::Id).uuid().not_null().primary_key()) .col(ColumnDef::new(User::FirstName).string().not_null()) .col(ColumnDef::new(User::LastName).string()) - .col(ColumnDef::new(User::Email).unique_key().string().not_null()) + .col(ColumnDef::new(User::Email).string().not_null()) .col(ColumnDef::new(User::EmailVerifiedAt).timestamp_with_time_zone()) .col(ColumnDef::new(User::Phone).string()) .col(ColumnDef::new(User::Image).string()) @@ -44,6 +44,14 @@ impl MigrationTrait for Migration { .not_null() .default(chrono::Utc::now()), ) + .index( + Index::create() + .name("user_email_realm_id_index") + .table(User::Table) + .col(User::Email) + .col(User::RealmId) + .unique(), + ) .to_owned(), ) .await diff --git a/migration/src/m20220101_000006_create_refresh_token_table.rs b/migration/src/m20220101_000006_create_refresh_token_table.rs index f601695..cea1f19 100644 --- a/migration/src/m20220101_000006_create_refresh_token_table.rs +++ b/migration/src/m20220101_000006_create_refresh_token_table.rs @@ -20,6 +20,7 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(RefreshToken::ClientId).uuid()) .col(ColumnDef::new(RefreshToken::RealmId).uuid().not_null()) .col(ColumnDef::new(RefreshToken::ReUsedCount).integer().not_null().default(0)) + .col(ColumnDef::new(RefreshToken::Expires).timestamp_with_time_zone().not_null()) .col(ColumnDef::new(RefreshToken::LockedAt).timestamp_with_time_zone()) .col( ColumnDef::new(RefreshToken::CreatedAt) @@ -118,6 +119,7 @@ pub enum RefreshToken { RealmId, ReUsedCount, LockedAt, + Expires, CreatedAt, UpdatedAt, } diff --git a/migration/src/m20220101_000007_create_api_user_table.rs b/migration/src/m20220101_000007_create_api_user_table.rs index a34ae19..48d1216 100644 --- a/migration/src/m20220101_000007_create_api_user_table.rs +++ b/migration/src/m20220101_000007_create_api_user_table.rs @@ -1,5 +1,4 @@ use super::m20220101_000002_create_client_table::Client; -use super::m20220101_000003_create_user_table::User; use crate::m20220101_000001_create_realm_table::Realm; use sea_orm::sqlx::types::chrono; use sea_orm::{ActiveEnum, DbBackend, DeriveActiveEnum, EnumIter, Schema}; @@ -12,7 +11,7 @@ pub struct Migration; impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { let schema = Schema::new(DbBackend::Postgres); - manager.create_type(schema.create_enum_from_active_enum::()).await?; + manager.create_type(schema.create_enum_from_active_enum::()).await?; manager.create_type(schema.create_enum_from_active_enum::()).await?; manager .create_table( @@ -20,7 +19,6 @@ impl MigrationTrait for Migration { .table(ApiUser::Table) .if_not_exists() .col(ColumnDef::new(ApiUser::Id).uuid().not_null().primary_key()) - .col(ColumnDef::new(ApiUser::Secret).string().not_null()) .col(ColumnDef::new(ApiUser::Name).string().not_null()) .col(ColumnDef::new(ApiUser::Description).string()) .col(ColumnDef::new(ApiUser::RealmId).uuid().not_null()) @@ -39,24 +37,8 @@ impl MigrationTrait for Migration { .to(Client::Table, Client::Id) .on_delete(ForeignKeyAction::Cascade), ) - .col(ColumnDef::new(ApiUser::Role).custom(ApiUserRole::name()).not_null()) + .col(ColumnDef::new(ApiUser::Role).custom(ApiUserScope::name()).not_null()) .col(ColumnDef::new(ApiUser::Access).custom(ApiUserAccess::name()).not_null()) - .col(ColumnDef::new(ApiUser::CreatedBy).uuid().not_null()) - .foreign_key( - ForeignKey::create() - .name("fk_api_user_created_by") - .from(ApiUser::Table, ApiUser::CreatedBy) - .to(User::Table, User::Id) - .on_delete(ForeignKeyAction::Cascade), - ) - .col(ColumnDef::new(ApiUser::UpdatedBy).uuid().not_null()) - .foreign_key( - ForeignKey::create() - .name("fk_api_user_updated_by") - .from(ApiUser::Table, ApiUser::UpdatedBy) - .to(User::Table, User::Id) - .on_delete(ForeignKeyAction::Cascade), - ) .col(ColumnDef::new(ApiUser::Expires).timestamp_with_time_zone().not_null()) .col(ColumnDef::new(ApiUser::LockedAt).timestamp_with_time_zone()) .col( @@ -90,12 +72,12 @@ impl MigrationTrait for Migration { } #[derive(EnumIter, DeriveActiveEnum)] -#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "api_user_role")] -pub enum ApiUserRole { - #[sea_orm(string_value = "realm_admin")] - RealmAdmin, - #[sea_orm(string_value = "client_admin")] - ClientAdmin, +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "api_user_scope")] +pub enum ApiUserScope { + #[sea_orm(string_value = "realm")] + Realm, + #[sea_orm(string_value = "client")] + Client, } #[derive(EnumIter, DeriveActiveEnum, PartialEq, Eq)] @@ -105,6 +87,8 @@ pub enum ApiUserAccess { Read, #[sea_orm(string_value = "write")] Write, + #[sea_orm(string_value = "update")] + Update, #[sea_orm(string_value = "delete")] Delete, #[sea_orm(string_value = "admin")] @@ -115,7 +99,6 @@ pub enum ApiUserAccess { pub enum ApiUser { Table, Id, - Secret, Name, Description, RealmId, @@ -124,8 +107,6 @@ pub enum ApiUser { Access, Expires, LockedAt, - CreatedBy, - UpdatedBy, CreatedAt, UpdatedAt, } diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index 440adea..fe40e0f 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -1,236 +1,108 @@ -use chrono::Utc; +use axum_extra::either::Either; use entity::{ - client, refresh_token, resource, resource_group, - sea_orm_active_enums::{ApiUserAccess, ApiUserRole}, + sea_orm_active_enums::{ApiUserAccess, ApiUserScope}, session, user, }; -use sea_orm::{ - prelude::Uuid, ActiveModelTrait, ColumnTrait, DatabaseTransaction, DbErr, EntityTrait, PaginatorTrait, QueryFilter, Set, TransactionTrait, -}; +use sea_orm::{prelude::Uuid, ColumnTrait, EntityTrait, QueryFilter, TransactionTrait}; use std::sync::Arc; use crate::{ mappers::auth::{ - CreateUserRequest, IntrospectRequest, IntrospectResponse, LogoutRequest, LogoutResponse, RefreshTokenRequest, RefreshTokenResponse, + CreateUserRequest, Credentials, IntrospectRequest, IntrospectResponse, LoginResponse, LogoutRequest, LogoutResponse, RefreshTokenRequest, + RefreshTokenResponse, }, middleware::session_info_extractor::SessionInfo, packages::{ - api_token::{decode_refresh_token, ApiUser, RefreshTokenClaims}, + api_token::{verify_and_decode_jwt, ApiUser, RefreshTokenClaims}, db::AppState, errors::{AuthenticateError, Error}, - jwt_token::{create, decode, JwtUser}, + jwt_token::{decode, JwtUser}, settings::SETTINGS, }, - services::{auth::handle_refresh_token, user::insert_user}, - utils::role_checker::{has_access_to_api_cred, is_current_realm_admin, is_master_realm_admin}, + services::{ + auth::{ + create_session, create_session_and_refresh_token, get_active_refresh_token_by_id, get_active_resource_by_gu, + get_active_resource_group_by_rcu, get_active_session_by_id, get_active_sessions_by_user_and_client_id, handle_refresh_token, + }, + client::get_active_client_by_id, + user::{get_active_user_and_resource_groups, get_active_user_by_id, insert_user}, + }, + utils::role_checker::has_access_to_api_cred, }; use axum::{extract::Path, Extension, Json}; -use serde::{Deserialize, Serialize}; use tracing::debug; -#[derive(Deserialize)] -pub struct Credentials { - email: String, - password: String, -} - -#[derive(Serialize)] -pub struct LoginResponse { - access_token: String, - user: user::Model, - session_id: Uuid, - realm_id: Uuid, - client_id: Uuid, - #[serde(skip_serializing_if = "Option::is_none")] - refresh_token: Option, -} - -pub async fn login( +pub async fn admin_login( + api_user: ApiUser, Extension(state): Extension>, Extension(session_info): Extension>, Path((realm_id, client_id)): Path<(Uuid, Uuid)>, Json(payload): Json, ) -> Result, Error> { - debug!("🚀 Login request received! {:#?}", session_info); - - let user_with_resource_groups = user::Entity::find() - .filter(user::Column::Email.eq(payload.email)) - .find_also_related(resource_group::Entity) - .filter(resource_group::Column::RealmId.eq(realm_id)) - .filter(resource_group::Column::ClientId.eq(client_id)) - .one(&state.db) - .await?; - - if user_with_resource_groups.is_none() { - debug!("No matching data found"); - return Err(Error::not_found()); + debug!("🚀 Admin login request received! {:#?}", session_info); + if !api_user.has_access(ApiUserScope::Client, ApiUserAccess::Admin) { + debug!("No allowed access"); + return Err(Error::Authenticate(AuthenticateError::ActionForbidden)); } - let (user, resource_groups) = user_with_resource_groups.unwrap(); + let (user, resource_groups) = get_active_user_and_resource_groups(&state.db, Either::E1(payload.email), realm_id, client_id).await?; if !user.verify_password(&payload.password) { debug!("Wrong password"); return Err(Error::Authenticate(AuthenticateError::WrongCredentials)); } - if user.locked_at.is_some() { - debug!("User is locked"); - return Err(Error::Authenticate(AuthenticateError::Locked)); - } - if resource_groups.is_none() { - debug!("No matching resource group found"); - return Err(Error::not_found()); - } + let client = get_active_client_by_id(&state.db, client_id).await?; + let sessions = get_active_sessions_by_user_and_client_id(&state.db, user.id, client.id).await?; - let resource_groups = resource_groups.unwrap(); - if resource_groups.locked_at.is_some() { - debug!("Resource group is locked"); - return Err(Error::Authenticate(AuthenticateError::Locked)); + if sessions.len() >= client.max_concurrent_sessions as usize { + debug!("Client has reached max concurrent sessions"); + return Err(Error::Authenticate(AuthenticateError::MaxConcurrentSessions)); } - // Fetch client separately - let client = client::Entity::find_by_id(client_id).one(&state.db).await?.ok_or_else(|| { - debug!("No client found"); - Error::not_found() - })?; + let login_response = create_session_and_refresh_token(state, user, client, resource_groups, session_info).await?; + Ok(Json(login_response)) +} - if client.locked_at.is_some() { - debug!("Client is locked"); - return Err(Error::Authenticate(AuthenticateError::Locked)); +pub async fn login( + api_user: ApiUser, + Extension(state): Extension>, + Extension(session_info): Extension>, + Path((realm_id, client_id)): Path<(Uuid, Uuid)>, + Json(payload): Json, +) -> Result, Error> { + debug!("🚀 Login request received! {:#?}", session_info); + if !api_user.has_access(ApiUserScope::Client, ApiUserAccess::Write) { + debug!("No allowed access"); + return Err(Error::Authenticate(AuthenticateError::ActionForbidden)); } - let login_response = state - .db - .transaction(|txn| { - Box::pin(async move { - let result: Result = async { - let refresh_token_model = if client.use_refresh_token { - let model = refresh_token::ActiveModel { - id: Set(Uuid::now_v7()), - user_id: Set(user.id), - client_id: Set(Some(client_id)), - realm_id: Set(realm_id), - re_used_count: Set(0), - locked_at: Set(None), - ..Default::default() - }; - Some(model.insert(txn).await?) - } else { - None - }; - - let session = create_session( - &client, - &user, - resource_groups, - session_info, - refresh_token_model.as_ref().map(|x| x.id), - txn, - ) - .await?; + let (user, resource_groups) = get_active_user_and_resource_groups(&state.db, Either::E1(payload.email), realm_id, client_id).await?; - let refresh_token = if let Some(refresh_token) = refresh_token_model { - let claims = RefreshTokenClaims::from(&refresh_token, &client); - Some(claims.create_token(&SETTINGS.read().secrets.signing_key).unwrap()) - } else { - None - }; - - Ok(LoginResponse { - access_token: session.access_token, - realm_id: user.realm_id, - user, - session_id: session.session_id, - client_id: client.id, - refresh_token, - }) - } - .await; - - result.map_err(|e| DbErr::Custom(e.to_string())) - }) - }) - .await?; - - Ok(Json(login_response)) -} + if !user.verify_password(&payload.password) { + debug!("Wrong password"); + return Err(Error::Authenticate(AuthenticateError::WrongCredentials)); + } -async fn create_session( - client: &client::Model, - user: &user::Model, - resource_groups: resource_group::Model, - session_info: Arc, - refresh_token_id: Option, - db: &DatabaseTransaction, -) -> Result { - let sessions = session::Entity::find() - .filter(session::Column::ClientId.eq(client.id)) - .filter(session::Column::UserId.eq(user.id)) - .filter(session::Column::Expires.gt(chrono::Utc::now())) - .count(db) - .await?; + let client = get_active_client_by_id(&state.db, client_id).await?; + let sessions = get_active_sessions_by_user_and_client_id(&state.db, user.id, client.id).await?; - if sessions >= client.max_concurrent_sessions as u64 { + if sessions.len() >= client.max_concurrent_sessions as usize { debug!("Client has reached max concurrent sessions"); return Err(Error::Authenticate(AuthenticateError::MaxConcurrentSessions)); } - // Fetch resources - let resources = resource::Entity::find() - .filter(resource::Column::GroupId.eq(resource_groups.id)) - .filter(resource::Column::LockedAt.is_null()) - .all(db) - .await?; - - if resources.is_empty() { - debug!("No resources found"); - return Err(Error::Authenticate(AuthenticateError::Locked)); - } - - let session_model = session::ActiveModel { - id: Set(Uuid::now_v7()), - user_id: Set(user.id), - client_id: Set(client.id), - ip_address: Set(session_info.ip_address.to_string()), - user_agent: Set(Some(session_info.user_agent.to_string())), - browser: Set(Some(session_info.browser.to_string())), - browser_version: Set(Some(session_info.browser_version.to_string())), - operating_system: Set(Some(session_info.operating_system.to_string())), - device_type: Set(Some(session_info.device_type.to_string())), - country_code: Set(session_info.country_code.to_string()), - refresh_token_id: Set(refresh_token_id), - expires: Set((Utc::now() + chrono::Duration::seconds(client.session_lifetime as i64)).into()), - ..Default::default() - }; - let session = session_model.insert(db).await?; - - let access_token = create( - user.clone(), - client, - resource_groups, - resources, - &session, - &SETTINGS.read().secrets.signing_key, - ) - .unwrap(); - - Ok(LoginResponse { - access_token, - realm_id: user.realm_id, - user: user.clone(), - session_id: session.id, - client_id: client.id, - refresh_token: None, - }) + let login_response = create_session_and_refresh_token(state, user, client, resource_groups, session_info).await?; + Ok(Json(login_response)) } pub async fn register( - user: JwtUser, + api_user: ApiUser, Extension(state): Extension>, Path((realm_id, client_id)): Path<(Uuid, Uuid)>, Json(payload): Json, ) -> Result, Error> { - if is_master_realm_admin(&user) || is_current_realm_admin(&user, &realm_id.to_string()) { + if api_user.has_access(ApiUserScope::Client, ApiUserAccess::Write) { let user = insert_user(&state.db, realm_id, client_id, payload).await?; Ok(Json(user)) } else { @@ -248,43 +120,44 @@ pub async fn logout_current_session(user: JwtUser, Extension(state): Extension>, - Path((realm_id, _)): Path<(Uuid, Uuid)>, + Path((_, _)): Path<(Uuid, Uuid)>, Json(payload): Json, ) -> Result, Error> { - if is_master_realm_admin(&user) || is_current_realm_admin(&user, &realm_id.to_string()) { - match payload.access_token { - Some(access_token) => { - let sid = decode(&access_token, &SETTINGS.read().secrets.signing_key) + if !api_user.has_access(ApiUserScope::Client, ApiUserAccess::Write) { + debug!("No allowed access"); + return Err(Error::Authenticate(AuthenticateError::ActionForbidden)); + } + + match payload.access_token { + Some(access_token) => { + let claims = decode(&access_token, &SETTINGS.read().secrets.signing_key) + .map_err(|_| AuthenticateError::InvalidToken)? + .claims; + + let result = session::Entity::delete_by_id(claims.sid).exec(&state.db).await?; + Ok(Json(LogoutResponse { + ok: result.rows_affected == 1, + user_id: claims.sub, + session_id: claims.sid, + })) + } + None => match payload.refresh_token { + Some(refresh_token) => { + let claims = decode(&refresh_token, &SETTINGS.read().secrets.signing_key) .map_err(|_| AuthenticateError::InvalidToken)? - .claims - .sid; - let result = session::Entity::delete_by_id(sid).exec(&state.db).await?; + .claims; + + let result = session::Entity::delete_by_id(claims.sid).exec(&state.db).await?; Ok(Json(LogoutResponse { ok: result.rows_affected == 1, - user_id: user.sub, - session_id: user.sid, + user_id: claims.sub, + session_id: claims.sid, })) } - None => match payload.refresh_token { - Some(refresh_token) => { - let sid = decode(&refresh_token, &SETTINGS.read().secrets.signing_key) - .map_err(|_| AuthenticateError::InvalidToken)? - .claims - .sid; - let result = session::Entity::delete_by_id(sid).exec(&state.db).await?; - Ok(Json(LogoutResponse { - ok: result.rows_affected == 1, - user_id: user.sub, - session_id: user.sid, - })) - } - None => Err(Error::Authenticate(AuthenticateError::NoResource)), - }, - } - } else { - Err(Error::Authenticate(AuthenticateError::ActionForbidden)) + None => Err(Error::Authenticate(AuthenticateError::NoResource)), + }, } } @@ -306,129 +179,91 @@ pub async fn logout_my_all_sessions( } pub async fn logout_all( - user: JwtUser, + api_user: ApiUser, Extension(state): Extension>, - Path((realm_id, client_id)): Path<(Uuid, Uuid)>, + Path((_, client_id)): Path<(Uuid, Uuid)>, Json(payload): Json, ) -> Result, Error> { - if is_master_realm_admin(&user) || is_current_realm_admin(&user, &realm_id.to_string()) { - match payload.access_token { - Some(access_token) => { - let sub = decode(&access_token, &SETTINGS.read().secrets.signing_key) + if !api_user.has_access(ApiUserScope::Client, ApiUserAccess::Write) { + debug!("No allowed access"); + return Err(Error::Authenticate(AuthenticateError::ActionForbidden)); + } + + match payload.access_token { + Some(access_token) => { + let claims = decode(&access_token, &SETTINGS.read().secrets.signing_key) + .map_err(|_| AuthenticateError::InvalidToken)? + .claims; + + let result = session::Entity::delete_many() + .filter(session::Column::ClientId.eq(client_id)) + .filter(session::Column::UserId.eq(claims.sub)) + .exec(&state.db) + .await?; + Ok(Json(LogoutResponse { + ok: result.rows_affected > 0, + user_id: claims.sub, + session_id: claims.sid, + })) + } + None => match payload.refresh_token { + Some(refresh_token) => { + let claims = decode(&refresh_token, &SETTINGS.read().secrets.signing_key) .map_err(|_| AuthenticateError::InvalidToken)? - .claims - .sub; + .claims; let result = session::Entity::delete_many() .filter(session::Column::ClientId.eq(client_id)) - .filter(session::Column::UserId.eq(sub)) + .filter(session::Column::UserId.eq(claims.sub)) .exec(&state.db) .await?; Ok(Json(LogoutResponse { ok: result.rows_affected > 0, - user_id: user.sub, - session_id: user.sid, + user_id: claims.sub, + session_id: claims.sid, })) } - None => match payload.refresh_token { - Some(refresh_token) => { - let sub = decode(&refresh_token, &SETTINGS.read().secrets.signing_key) - .map_err(|_| AuthenticateError::InvalidToken)? - .claims - .sub; - let result = session::Entity::delete_many() - .filter(session::Column::ClientId.eq(client_id)) - .filter(session::Column::UserId.eq(sub)) - .exec(&state.db) - .await?; - Ok(Json(LogoutResponse { - ok: result.rows_affected > 0, - user_id: user.sub, - session_id: user.sid, - })) - } - None => Err(Error::Authenticate(AuthenticateError::NoResource)), - }, - } - } else { - Err(Error::Authenticate(AuthenticateError::ActionForbidden)) + None => Err(Error::Authenticate(AuthenticateError::NoResource)), + }, } } pub async fn introspect( - user: JwtUser, + api_user: ApiUser, Extension(state): Extension>, Path((realm_id, client_id)): Path<(Uuid, Uuid)>, Json(payload): Json, ) -> Result, Error> { - if is_master_realm_admin(&user) || is_current_realm_admin(&user, &realm_id.to_string()) { - let token_data = decode(&payload.access_token, &SETTINGS.read().secrets.signing_key).expect("Failed to decode token"); - - if token_data.claims.resource.is_none() || token_data.claims.resource.is_some() && token_data.claims.resource.unwrap().client_id != client_id - { - return Err(Error::Authenticate(AuthenticateError::NoResource)); - } + if !api_user.has_access(ApiUserScope::Client, ApiUserAccess::Read) { + debug!("No allowed access"); + return Err(Error::Authenticate(AuthenticateError::ActionForbidden)); + } - let session = session::Entity::find_by_id(token_data.claims.sid).one(&state.db).await?; - match session { - Some(session) => { - let user = user::Entity::find_by_id(session.user_id) - .filter(user::Column::LockedAt.is_null()) - .one(&state.db) - .await?; + let token_data = decode(&payload.access_token, &SETTINGS.read().secrets.signing_key)?; - match user { - Some(user) => { - let client = client::Entity::find_by_id(session.client_id) - .filter(client::Column::LockedAt.is_null()) - .one(&state.db) - .await?; - - match client { - Some(client) => { - let resource_group = resource_group::Entity::find() - .filter(resource_group::Column::RealmId.eq(realm_id)) - .filter(resource_group::Column::ClientId.eq(client.id)) - .filter(resource_group::Column::UserId.eq(user.id)) - .filter(resource_group::Column::LockedAt.is_null()) - .one(&state.db) - .await?; - - match resource_group { - Some(resource_group) => { - let resources = resource::Entity::find() - .filter(resource::Column::GroupId.eq(resource_group.id)) - .filter(resource::Column::LockedAt.is_null()) - .all(&state.db) - .await?; - Ok(Json(IntrospectResponse { - active: true, - client_id: client.id, - first_name: user.first_name.to_string(), - last_name: Some(user.last_name.unwrap_or("".to_string())), - sub: user.id, - token_type: "bearer".to_string(), - exp: token_data.claims.exp, - iat: token_data.claims.iat, - iss: SETTINGS.read().server.host.clone(), - client_name: client.name, - resource_group: resource_group.name, - resources: resources.iter().map(|r| r.name.clone()).collect::>(), - })) - } - None => Err(Error::Authenticate(AuthenticateError::NoResource))?, - } - } - None => Err(Error::Authenticate(AuthenticateError::NoResource)), - } - } - None => Err(Error::Authenticate(AuthenticateError::NoResource)), - } - } - None => Err(Error::Authenticate(AuthenticateError::NoResource)), - } - } else { - Err(Error::Authenticate(AuthenticateError::ActionForbidden)) + if token_data.claims.resource.is_none() || token_data.claims.resource.is_some() && token_data.claims.resource.unwrap().client_id != client_id { + return Err(Error::Authenticate(AuthenticateError::ActionForbidden)); } + + let session = get_active_session_by_id(&state.db, token_data.claims.sid).await?; + let user = get_active_user_by_id(&state.db, session.user_id).await?; + let client = get_active_client_by_id(&state.db, session.client_id).await?; + let resource_group = get_active_resource_group_by_rcu(&state.db, realm_id, session.client_id, session.user_id).await?; + let resources = get_active_resource_by_gu(&state.db, resource_group.id, session.user_id).await?; + + Ok(Json(IntrospectResponse { + active: true, + client_id: client.id, + first_name: user.first_name.to_string(), + last_name: Some(user.last_name.unwrap_or("".to_string())), + sub: user.id, + token_type: "bearer".to_string(), + exp: token_data.claims.exp, + iat: token_data.claims.iat, + iss: SETTINGS.read().server.host.clone(), + client_name: client.name, + resource_group: resource_group.name, + resources: resources.iter().map(|r| r.name.clone()).collect::>(), + })) } pub async fn refresh_token( @@ -438,58 +273,20 @@ pub async fn refresh_token( Path((realm_id, client_id)): Path<(Uuid, Uuid)>, Json(payload): Json, ) -> Result, Error> { - if !has_access_to_api_cred(&user, ApiUserRole::ClientAdmin, ApiUserAccess::Admin).await { + if !has_access_to_api_cred(&user, ApiUserScope::Client, ApiUserAccess::Write).await { debug!("No allowed access"); return Err(Error::Authenticate(AuthenticateError::ActionForbidden)); } - let token_data = decode_refresh_token(&payload.refresh_token, &SETTINGS.read().secrets.signing_key).expect("Failed to decode token"); + let token_data = verify_and_decode_jwt::(&payload.refresh_token, &SETTINGS.read().secrets.signing_key, None)?; if token_data.claims.rli != realm_id || token_data.claims.cli != client_id { - return Err(Error::Authenticate(AuthenticateError::InvalidToken)); - } - - let refresh_token = refresh_token::Entity::find_active_by_id(&state.db, token_data.claims.sub).await?; - if refresh_token.is_none() { - return Err(Error::not_found()); - } - let client = client::Entity::find_active_by_id(&state.db, token_data.claims.cli).await?; - if client.is_none() { - return Err(Error::Authenticate(AuthenticateError::InvalidToken)); - } - - let refresh_token = refresh_token.unwrap(); - let client = client.unwrap(); - - // Fetch user and resource groups - let user_with_resource_groups = user::Entity::find() - .filter(user::Column::Id.eq(refresh_token.user_id)) - .find_also_related(resource_group::Entity) - .filter(resource_group::Column::RealmId.eq(client.realm_id)) - .filter(resource_group::Column::ClientId.eq(client.id)) - .one(&state.db) - .await?; - - if user_with_resource_groups.is_none() { - debug!("No matching data found"); - return Err(Error::not_found()); - } - - let (user, resource_groups) = user_with_resource_groups.unwrap(); - if user.locked_at.is_some() { - debug!("User is locked"); - return Err(Error::Authenticate(AuthenticateError::Locked)); - } - - if resource_groups.is_none() { - debug!("No matching resource group found"); - return Err(Error::not_found()); + return Err(Error::Authenticate(AuthenticateError::ActionForbidden)); } - let resource_groups = resource_groups.unwrap(); - if resource_groups.locked_at.is_some() { - debug!("Resource group is locked"); - return Err(Error::Authenticate(AuthenticateError::Locked)); - } + let refresh_token = get_active_refresh_token_by_id(&state.db, token_data.claims.sub).await?; + let client = get_active_client_by_id(&state.db, token_data.claims.cli).await?; + let (user, resource_groups) = + get_active_user_and_resource_groups(&state.db, Either::E2(refresh_token.user_id), client.realm_id, client.id).await?; debug!("Before transaction calls"); Ok(state diff --git a/src/handlers/client/api_user.rs b/src/handlers/client/api_user.rs index 7e6c309..5580158 100644 --- a/src/handlers/client/api_user.rs +++ b/src/handlers/client/api_user.rs @@ -2,7 +2,10 @@ use std::sync::Arc; use axum::{extract::Path, Extension, Json}; use chrono::Utc; -use entity::api_user; +use entity::{ + api_user, + sea_orm_active_enums::{ApiUserAccess, ApiUserScope}, +}; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; use uuid::Uuid; @@ -12,22 +15,18 @@ use crate::{ DeleteResponse, }, packages::{ + api_token::ApiUser, db::AppState, errors::{AuthenticateError, Error}, - jwt_token::JwtUser, - }, - utils::{ - helpers::generate_random_string::{generate_random_string, Length}, - role_checker::{is_current_realm_admin, is_master_realm_admin}, }, }; pub async fn get_api_users( - user: JwtUser, + api_user: ApiUser, Extension(state): Extension>, Path((realm_id, client_id)): Path<(Uuid, Uuid)>, ) -> Result>, Error> { - if !is_master_realm_admin(&user) && !is_current_realm_admin(&user, &realm_id.to_string()) { + if !api_user.is_master_realm_admin() { return Err(Error::Authenticate(AuthenticateError::NoResource)); } @@ -40,20 +39,17 @@ pub async fn get_api_users( } pub async fn create_api_user( - user: JwtUser, + api_user: ApiUser, Extension(state): Extension>, Path((realm_id, client_id)): Path<(Uuid, Uuid)>, Json(payload): Json, ) -> Result, Error> { - if !is_master_realm_admin(&user) && !is_current_realm_admin(&user, &realm_id.to_string()) { + if !api_user.has_access(payload.role.clone(), ApiUserAccess::Admin) { return Err(Error::Authenticate(AuthenticateError::NoResource)); } - let api_secret = generate_random_string(Length::U64); - let api_user_model = api_user::ActiveModel { id: Set(Uuid::now_v7()), - secret: Set(api_secret), name: Set(payload.name), description: Set(payload.description), realm_id: Set(realm_id), @@ -61,8 +57,6 @@ pub async fn create_api_user( role: Set(payload.role), access: Set(payload.access), expires: Set(payload.expires.unwrap().to_datetime()), - created_by: Set(user.sub), - updated_by: Set(user.sub), ..Default::default() }; @@ -71,13 +65,13 @@ pub async fn create_api_user( } pub async fn update_api_user( - user: JwtUser, + api_user: ApiUser, Extension(state): Extension>, - Path((realm_id, _client_id, api_user_id)): Path<(Uuid, Uuid, Uuid)>, + Path((_realm_id, _client_id, api_user_id)): Path<(Uuid, Uuid, Uuid)>, Json(payload): Json, ) -> Result, Error> { - if !is_master_realm_admin(&user) && !is_current_realm_admin(&user, &realm_id.to_string()) { - return Err(Error::Authenticate(AuthenticateError::NoResource)); + if !api_user.is_master_realm_admin() { + return Err(Error::Authenticate(AuthenticateError::ActionForbidden)); } let api_user = api_user::Entity::find_by_id(api_user_id).one(&state.db).await?; @@ -123,12 +117,12 @@ pub async fn update_api_user( } pub async fn delete_api_user( - user: JwtUser, + api_user: ApiUser, Extension(state): Extension>, - Path((realm_id, _client_id, api_user_id)): Path<(Uuid, Uuid, Uuid)>, + Path((_realm_id, _client_id, api_user_id)): Path<(Uuid, Uuid, Uuid)>, ) -> Result, Error> { - if !is_master_realm_admin(&user) && !is_current_realm_admin(&user, &realm_id.to_string()) { - return Err(Error::Authenticate(AuthenticateError::NoResource)); + if !api_user.has_access(ApiUserScope::Realm, ApiUserAccess::Admin) { + return Err(Error::Authenticate(AuthenticateError::ActionForbidden)); } let delete_result = api_user::Entity::delete_by_id(api_user_id).exec(&state.db).await?; diff --git a/src/handlers/client/mod.rs b/src/handlers/client/mod.rs index a5b8393..2dc7b67 100644 --- a/src/handlers/client/mod.rs +++ b/src/handlers/client/mod.rs @@ -7,95 +7,97 @@ use crate::{ DeleteResponse, }, packages::{ + api_token::ApiUser, db::AppState, errors::{AuthenticateError, Error}, - jwt_token::JwtUser, }, services::client::{delete_client_by_id, get_all_clients, get_client_by_id, insert_client, update_client_by_id}, - utils::{ - default_resource_checker::is_default_client, - role_checker::{is_current_realm_admin, is_master_realm_admin}, - }, + utils::default_resource_checker::is_default_client, }; use axum::{extract::Path, Extension, Json}; -use entity::client; +use entity::{ + client, + sea_orm_active_enums::{ApiUserAccess, ApiUserScope}, +}; use sea_orm::prelude::Uuid; pub async fn get_clients( - user: JwtUser, + api_user: ApiUser, Extension(state): Extension>, Path(realm_id): Path, ) -> Result>, Error> { - if is_master_realm_admin(&user) || is_current_realm_admin(&user, &realm_id.to_string()) { - let clients = get_all_clients(&state.db, realm_id).await?; - if clients.is_empty() { - return Err(Error::not_found()); - } - Ok(Json(clients)) - } else { - Err(Error::Authenticate(AuthenticateError::NoResource)) + if !api_user.has_access(ApiUserScope::Realm, ApiUserAccess::Read) { + return Err(Error::Authenticate(AuthenticateError::NoResource)); } + + let clients = get_all_clients(&state.db, realm_id).await?; + Ok(Json(clients)) } pub async fn get_client( - user: JwtUser, + api_user: ApiUser, Extension(state): Extension>, Path((realm_id, client_id)): Path<(Uuid, Uuid)>, ) -> Result, Error> { - if is_master_realm_admin(&user) || is_current_realm_admin(&user, &realm_id.to_string()) { - let client = get_client_by_id(&state.db, client_id).await?; - match client { - Some(client) => Ok(Json(client)), - None => Err(Error::Authenticate(AuthenticateError::NoResource)), + if !api_user.has_access(ApiUserScope::Client, ApiUserAccess::Read) { + return Err(Error::Authenticate(AuthenticateError::NoResource)); + } + + let client = get_client_by_id(&state.db, client_id).await?; + match client { + Some(client) => { + if client.realm_id != realm_id { + return Err(Error::Authenticate(AuthenticateError::NoResource)); + } + Ok(Json(client)) } - } else { - Err(Error::Authenticate(AuthenticateError::ActionForbidden)) + None => Err(Error::Authenticate(AuthenticateError::NoResource)), } } pub async fn create_client( - user: JwtUser, + api_user: ApiUser, Extension(state): Extension>, Path(realm_id): Path, Json(payload): Json, ) -> Result, Error> { - if is_master_realm_admin(&user) || is_current_realm_admin(&user, &realm_id.to_string()) { - let client = insert_client(&state.db, payload).await?; - Ok(Json(client)) - } else { - Err(Error::Authenticate(AuthenticateError::NoResource)) + if !api_user.has_access(ApiUserScope::Realm, ApiUserAccess::Admin) { + return Err(Error::Authenticate(AuthenticateError::NoResource)); } + + let client = insert_client(&state.db, realm_id, payload).await?; + Ok(Json(client)) } pub async fn update_client( - user: JwtUser, + api_user: ApiUser, Extension(state): Extension>, Path((realm_id, client_id)): Path<(Uuid, Uuid)>, Json(payload): Json, ) -> Result, Error> { - if is_master_realm_admin(&user) || is_current_realm_admin(&user, &realm_id.to_string()) { - let client = update_client_by_id(&state.db, client_id, payload).await?; - Ok(Json(client)) - } else { - Err(Error::Authenticate(AuthenticateError::NoResource)) + if !api_user.has_access(ApiUserScope::Client, ApiUserAccess::Update) { + return Err(Error::Authenticate(AuthenticateError::NoResource)); } + + let client = update_client_by_id(&state.db, realm_id, client_id, payload).await?; + Ok(Json(client)) } pub async fn delete_client( - user: JwtUser, + api_user: ApiUser, Extension(state): Extension>, Path((realm_id, client_id)): Path<(Uuid, Uuid)>, ) -> Result, Error> { + if !api_user.is_master_realm_admin() { + return Err(Error::Authenticate(AuthenticateError::ActionForbidden)); + } + if is_default_client(client_id) { return Err(Error::cannot_perform_operation("Cannot delete the default client")); } - if is_master_realm_admin(&user) || is_current_realm_admin(&user, &realm_id.to_string()) { - let client = delete_client_by_id(&state.db, client_id).await?; - Ok(Json(DeleteResponse { - ok: client.rows_affected == 1, - })) - } else { - Err(Error::Authenticate(AuthenticateError::NoResource)) - } + let client = delete_client_by_id(&state.db, realm_id, client_id).await?; + Ok(Json(DeleteResponse { + ok: client.rows_affected == 1, + })) } diff --git a/src/handlers/realm.rs b/src/handlers/realm.rs index 0f69114..f2653ae 100644 --- a/src/handlers/realm.rs +++ b/src/handlers/realm.rs @@ -1,7 +1,10 @@ use std::sync::Arc; use axum::{extract::Path, Extension, Json}; -use entity::realm; +use entity::{ + realm, + sea_orm_active_enums::{ApiUserAccess, ApiUserScope}, +}; use sea_orm::prelude::Uuid; use crate::{ @@ -10,82 +13,81 @@ use crate::{ DeleteResponse, }, packages::{ + api_token::ApiUser, db::AppState, errors::{AuthenticateError, Error}, - jwt_token::JwtUser, }, services::realm::{delete_realm_by_id, get_all_realms, get_realm_by_id, insert_realm, update_realm_by_id}, - utils::{ - default_resource_checker::is_default_realm, - role_checker::{is_current_realm_admin, is_master_realm_admin}, - }, + utils::default_resource_checker::is_default_realm, }; -pub async fn get_realms(user: JwtUser, Extension(state): Extension>) -> Result>, Error> { - if is_master_realm_admin(&user) { - let realms = get_all_realms(&state.db).await?; - if realms.is_empty() { - return Err(Error::not_found()); - } - return Ok(Json(realms)); +pub async fn get_realms(api_user: ApiUser, Extension(state): Extension>) -> Result>, Error> { + if !api_user.is_master_realm_admin() { + return Err(Error::Authenticate(AuthenticateError::NoResource)); } - Err(Error::Authenticate(AuthenticateError::NoResource)) + let realms = get_all_realms(&state.db).await?; + Ok(Json(realms)) } -pub async fn get_realm(user: JwtUser, Extension(state): Extension>, Path(realm_id): Path) -> Result, Error> { - if is_master_realm_admin(&user) || is_current_realm_admin(&user, &realm_id.to_string()) { - let fetched_realm = get_realm_by_id(&state.db, realm_id).await?; - match fetched_realm { - Some(fetched_realm) => Ok(Json(fetched_realm)), - None => Err(Error::not_found()), - } - } else { - Err(Error::Authenticate(AuthenticateError::NoResource)) +pub async fn get_realm( + api_user: ApiUser, + Extension(state): Extension>, + Path(realm_id): Path, +) -> Result, Error> { + if !api_user.has_access(ApiUserScope::Client, ApiUserAccess::Read) { + return Err(Error::Authenticate(AuthenticateError::NoResource)); + } + + let fetched_realm = get_realm_by_id(&state.db, realm_id).await?; + match fetched_realm { + Some(fetched_realm) => Ok(Json(fetched_realm)), + None => Err(Error::not_found()), } } pub async fn create_realm( - user: JwtUser, + api_user: ApiUser, Extension(state): Extension>, Json(payload): Json, ) -> Result, Error> { - if is_master_realm_admin(&user) { - let realm = insert_realm(&state.db, payload.name).await?; - Ok(Json(realm)) - } else { - Err(Error::Authenticate(AuthenticateError::NoResource)) + if !api_user.is_master_realm_admin() { + return Err(Error::Authenticate(AuthenticateError::NoResource)); } + + let realm = insert_realm(&state.db, payload.name).await?; + Ok(Json(realm)) } pub async fn update_realm( - user: JwtUser, + api_user: ApiUser, Extension(state): Extension>, Path(realm_id): Path, Json(payload): Json, ) -> Result, Error> { - if is_master_realm_admin(&user) { - let realm = update_realm_by_id(&state.db, realm_id, payload).await?; - Ok(Json(realm)) - } else { - Err(Error::Authenticate(AuthenticateError::NoResource)) + if !api_user.has_access(ApiUserScope::Realm, ApiUserAccess::Update) { + return Err(Error::Authenticate(AuthenticateError::NoResource)); } + + let realm = update_realm_by_id(&state.db, realm_id, payload).await?; + Ok(Json(realm)) } pub async fn delete_realm( - user: JwtUser, + api_user: ApiUser, Extension(state): Extension>, Path(realm_id): Path, ) -> Result, Error> { - if is_master_realm_admin(&user) { - if is_default_realm(realm_id) { - return Err(Error::cannot_perform_operation("Cannot delete the default realm")); - } - let result = delete_realm_by_id(&state.db, realm_id).await?; - Ok(Json(DeleteResponse { - ok: result.rows_affected == 1, - })) - } else { - Err(Error::Authenticate(AuthenticateError::NoResource)) + if !api_user.is_master_realm_admin() { + return Err(Error::Authenticate(AuthenticateError::NoResource)); } + + if is_default_realm(realm_id) { + return Err(Error::cannot_perform_operation("Cannot delete the default realm")); + } + + let result = delete_realm_by_id(&state.db, realm_id).await?; + Ok(Json(DeleteResponse { + ok: result.rows_affected == 1, + })) } diff --git a/src/mappers/auth.rs b/src/mappers/auth.rs index 224e6c2..7c66b36 100644 --- a/src/mappers/auth.rs +++ b/src/mappers/auth.rs @@ -1,7 +1,25 @@ +use entity::user; use sea_orm::prelude::Uuid; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +#[derive(Deserialize)] +pub struct Credentials { + pub email: String, + pub password: String, +} + +#[derive(Serialize)] +pub struct LoginResponse { + pub access_token: String, + pub user: user::Model, + pub session_id: Uuid, + pub realm_id: Uuid, + pub client_id: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, +} + #[derive(Deserialize)] pub struct ResourceSubset { pub group_name: String, diff --git a/src/mappers/client/api_user.rs b/src/mappers/client/api_user.rs index c9badbc..08056be 100644 --- a/src/mappers/client/api_user.rs +++ b/src/mappers/client/api_user.rs @@ -1,11 +1,17 @@ use chrono::{DateTime, Duration, FixedOffset, Utc}; use entity::{ api_user, - sea_orm_active_enums::{ApiUserAccess, ApiUserRole}, + sea_orm_active_enums::{ApiUserAccess, ApiUserScope}, }; +use jsonwebtoken::{EncodingKey, Header}; +use once_cell::sync::Lazy; use sea_orm::prelude::DateTimeWithTimeZone; use serde::{Deserialize, Serialize}; +use crate::packages::{api_token::ApiTokenClaims, settings::SETTINGS}; + +static HEADER: Lazy
= Lazy::new(Header::default); + #[derive(Deserialize)] pub enum TokenExpires { #[serde(rename = "never")] @@ -44,7 +50,7 @@ impl TokenExpires { pub struct CreateApiUserRequest { pub name: String, pub description: Option, - pub role: ApiUserRole, + pub role: ApiUserScope, pub access: ApiUserAccess, pub expires: Option, } @@ -58,7 +64,7 @@ pub struct CreateApiUserResponse { pub api_key: String, pub realm_id: String, pub client_id: String, - pub role: ApiUserRole, + pub role: ApiUserScope, pub access: ApiUserAccess, pub created_at: DateTimeWithTimeZone, pub expires_at: DateTimeWithTimeZone, @@ -67,10 +73,10 @@ pub struct CreateApiUserResponse { impl From for CreateApiUserResponse { fn from(api_user: api_user::Model) -> Self { Self { + api_key: Self::create_token(api_user.clone(), &SETTINGS.read().secrets.api_key_signing_secret).unwrap(), id: api_user.id.to_string(), name: api_user.name, description: api_user.description, - api_key: format!("{}.{}", api_user.id, api_user.secret), realm_id: api_user.realm_id.to_string(), client_id: api_user.client_id.to_string(), role: api_user.role, @@ -81,11 +87,23 @@ impl From for CreateApiUserResponse { } } +impl CreateApiUserResponse { + pub fn create_token(api_user: api_user::Model, secret: &str) -> Result { + create_api_key(api_user, secret) + } +} + +pub fn create_api_key(api_user: api_user::Model, secret: &str) -> Result { + let encoding_key = EncodingKey::from_secret(secret.as_ref()); + let claims = ApiTokenClaims::new(api_user); + jsonwebtoken::encode(&HEADER, &claims, &encoding_key) +} + #[derive(Deserialize)] pub struct UpdateApiUserRequest { pub name: Option, pub description: Option, - pub role: Option, + pub role: Option, pub access: Option, pub expires: Option, pub lock: Option, @@ -100,7 +118,7 @@ pub struct UpdateApiUserResponse { pub api_key: String, pub realm_id: String, pub client_id: String, - pub role: ApiUserRole, + pub role: ApiUserScope, pub access: ApiUserAccess, pub locked_at: Option, pub created_at: DateTimeWithTimeZone, @@ -111,10 +129,10 @@ pub struct UpdateApiUserResponse { impl From for UpdateApiUserResponse { fn from(api_user: api_user::Model) -> Self { Self { + api_key: create_api_key(api_user.clone(), &SETTINGS.read().secrets.api_key_signing_secret).unwrap(), id: api_user.id.to_string(), name: api_user.name, description: api_user.description, - api_key: format!("{}.{}", api_user.id, api_user.secret), realm_id: api_user.realm_id.to_string(), client_id: api_user.client_id.to_string(), role: api_user.role, diff --git a/src/mappers/client/mod.rs b/src/mappers/client/mod.rs index 132d431..31b4d07 100644 --- a/src/mappers/client/mod.rs +++ b/src/mappers/client/mod.rs @@ -1,11 +1,9 @@ pub mod api_user; -use sea_orm::prelude::Uuid; use serde::Deserialize; #[derive(Deserialize)] pub struct CreateClientRequest { pub name: String, - pub realm_id: Uuid, } #[derive(Deserialize)] diff --git a/src/packages/admin.rs b/src/packages/admin.rs index 35e262c..bfd43cc 100644 --- a/src/packages/admin.rs +++ b/src/packages/admin.rs @@ -1,4 +1,9 @@ -use entity::{client, realm, resource, resource_group, user}; +use chrono::{Duration, Utc}; +use entity::{ + api_user, client, realm, resource, resource_group, + sea_orm_active_enums::{ApiUserAccess, ApiUserScope}, + user, +}; use futures::future; use sea_orm::{ prelude::Uuid, ActiveModelTrait, ColumnTrait, DatabaseConnection, DatabaseTransaction, EntityTrait, QueryFilter, Set, TransactionError, @@ -17,7 +22,7 @@ use crate::{ utils::{hash::generate_password_hash, helpers::default_cred::DefaultCred}, }; -use super::{db::AppState, errors::Error}; +use super::{api_token::ApiUser, db::AppState, errors::Error}; pub async fn setup(state: &AppState) -> Result> { info!("Checking ADMIN availability!"); @@ -45,12 +50,14 @@ async fn initialize_db(conn: &DatabaseConnection) -> Result<(), TransactionError let realm = create_master_realm(txn).await?; let client = create_default_client(txn, realm.id).await?; let user = create_admin_user(txn, realm.id).await?; + let api_user = create_api_user(txn, realm.id, client.id).await?; let resource_assignment_result = assign_resource_to_admin(txn, realm.id, client.id, user.id).await?; let default_cred = DefaultCred { realm_id: realm.id, client_id: client.id, master_admin_user_id: user.id, + master_api_key: ApiUser::create_token(api_user, &SETTINGS.read().secrets.api_key_signing_secret).expect("Failed to create api key"), resource_group_id: resource_assignment_result.resource_group_id, resource_ids: resource_assignment_result.resource_ids, }; @@ -69,7 +76,7 @@ async fn create_master_realm(conn: &DatabaseTransaction) -> Result Re ..Default::default() }; let inserted_client = client_model.insert(conn).await?; - info!("✅ 2/5: Default client created"); + info!("✅ 2/6: Default client created"); Ok(inserted_client) } @@ -100,11 +107,29 @@ async fn create_admin_user(conn: &DatabaseTransaction, realm_id: Uuid) -> Result ..Default::default() }; let inserted_user = user_model.insert(conn).await?; - info!("✅ 3/5: Admin user created"); + info!("✅ 3/6: Admin user created"); Ok(inserted_user) } +async fn create_api_user(conn: &DatabaseTransaction, realm_id: Uuid, client_id: Uuid) -> Result { + let api_user_model = api_user::ActiveModel { + id: Set(Uuid::now_v7()), + name: Set("master_realm_default_api_user".to_owned()), + description: Set(Some("This api user has been created at the time of system initialization.".to_owned())), + realm_id: Set(realm_id), + client_id: Set(client_id), + role: Set(ApiUserScope::Realm), + access: Set(ApiUserAccess::Admin), + expires: Set((Utc::now() + Duration::days(30)).into()), + ..Default::default() + }; + let inserted_api_user = api_user_model.insert(conn).await?; + info!("✅ 4/6: Default api user created"); + + Ok(inserted_api_user) +} + struct ResourceAssignmentResult { resource_group_id: Uuid, resource_ids: Vec, @@ -128,7 +153,7 @@ async fn assign_resource_to_admin( ..Default::default() }; let inserted_resource_group = resource_group_model.insert(conn).await?; - info!("✅ 4/5: Default resource group created"); + info!("✅ 5/6: Default resource group created"); let resource_model = resource::ActiveModel { id: Set(Uuid::now_v7()), @@ -148,7 +173,7 @@ async fn assign_resource_to_admin( ..Default::default() }; let (inserted_resource, inserted_resource_2) = future::try_join(resource_model.insert(conn), new_resource_2.insert(conn)).await?; - info!("✅ 5/5: Default resource created"); + info!("✅ 6/6: Default resource created"); Ok(ResourceAssignmentResult { resource_group_id: inserted_resource_group.id, resource_ids: vec![inserted_resource.id, inserted_resource_2.id], @@ -157,7 +182,7 @@ async fn assign_resource_to_admin( fn write_default_cred(default_cred: DefaultCred) -> Result<(), Error> { info!("🗝️ Please note these credentials!"); - println!("{:#?}", default_cred); + info!("{:#?}", default_cred); let file_path = "./logs/default_cred.json"; let path = Path::new(file_path); diff --git a/src/packages/api_token.rs b/src/packages/api_token.rs index 8b1eb7c..504a5ed 100644 --- a/src/packages/api_token.rs +++ b/src/packages/api_token.rs @@ -2,15 +2,17 @@ use jsonwebtoken::{errors::Error as JwtError, DecodingKey, EncodingKey, Header, use once_cell::sync::Lazy; use sea_orm::{ prelude::{DateTimeWithTimeZone, Uuid}, - DatabaseConnection, + DatabaseConnection, EntityTrait, }; -use serde::{Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use entity::{ api_user, client, refresh_token, - sea_orm_active_enums::{ApiUserAccess, ApiUserRole}, + sea_orm_active_enums::{ApiUserAccess, ApiUserScope}, }; +use crate::mappers::client::api_user::create_api_key; + use super::{ errors::{AuthenticateError, Error}, settings::SETTINGS, @@ -19,6 +21,29 @@ use super::{ static VALIDATION: Lazy = Lazy::new(Validation::default); static HEADER: Lazy
= Lazy::new(Header::default); +#[derive(Deserialize, Serialize)] +pub struct ApiTokenClaims { + pub exp: usize, // Expiration time (as UTC timestamp). validate_exp defaults to true in validation + pub iat: usize, // Issued at (as UTC timestamp) + pub sub: Uuid, // Subject + pub iss: String, // Issuer + pub role: ApiUserScope, + pub access: ApiUserAccess, +} + +impl ApiTokenClaims { + pub fn new(api_user: api_user::Model) -> Self { + Self { + exp: chrono::Local::now().timestamp() as usize + api_user.expires.timestamp() as usize, + iat: chrono::Local::now().timestamp() as usize, + sub: api_user.id, + iss: SETTINGS.read().server.host.clone(), + role: api_user.role, + access: api_user.access, + } + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct ApiUser { pub id: Uuid, @@ -26,7 +51,7 @@ pub struct ApiUser { pub client_id: Uuid, pub name: String, pub description: Option, - pub role: ApiUserRole, + pub role: ApiUserScope, pub access: ApiUserAccess, pub expires: DateTimeWithTimeZone, } @@ -45,17 +70,14 @@ impl ApiUser { } } + pub fn create_token(api_user: api_user::Model, secret: &str) -> Result { + create_api_key(api_user, secret) + } + pub async fn validate_cred(db: &DatabaseConnection, api_key: &str) -> Result { - let parts = api_key.split('.').collect::>(); - if parts.len() != 2 { - return Err(Error::Authenticate(AuthenticateError::InvalidApiCredentials)); - } - let id = parts[0] - .parse::() - .map_err(|_| Error::Authenticate(AuthenticateError::InvalidApiCredentials))?; - let secret = parts[1]; + let token_data = verify_and_decode_jwt::(api_key, &SETTINGS.read().secrets.api_key_signing_secret, None)?; - let api_user = api_user::Entity::find_active_by_id(db, id).await?; + let api_user = api_user::Entity::find_by_id(token_data.claims.sub).one(db).await?; if api_user.is_none() { return Err(Error::Authenticate(AuthenticateError::InvalidApiCredentials)); } @@ -71,11 +93,18 @@ impl ApiUser { } } - if api_user.secret != secret { - return Err(Error::Authenticate(AuthenticateError::InvalidApiCredentials)); + Ok(Self::from(api_user)) + } + + pub fn has_access(&self, role: ApiUserScope, access: ApiUserAccess) -> bool { + if self.role.has_access(role) && self.access.has_access(access) { + return true; } + false + } - Ok(Self::from(api_user)) + pub fn is_master_realm_admin(&self) -> bool { + self.realm_id == SETTINGS.read().default_cred.realm_id } } @@ -109,8 +138,11 @@ impl RefreshTokenClaims { } } -pub fn decode_refresh_token(token: &str, secret: &str) -> Result, JwtError> { +pub fn verify_and_decode_jwt(token: &str, secret: &str, validation: Option<&Validation>) -> Result, JwtError> +where + T: DeserializeOwned, +{ let decoding_key = DecodingKey::from_secret(secret.as_ref()); - - jsonwebtoken::decode::(token, &decoding_key, &VALIDATION) + let validation = validation.unwrap_or(&VALIDATION); + jsonwebtoken::decode::(token, &decoding_key, validation) } diff --git a/src/packages/db.rs b/src/packages/db.rs index 2a59db0..23d1be3 100644 --- a/src/packages/db.rs +++ b/src/packages/db.rs @@ -2,6 +2,7 @@ use std::time::Duration; use migration::{Migrator, MigratorTrait}; use sea_orm::{ConnectOptions, ConnectionTrait, Database, DatabaseConnection, DbBackend, DbErr, Statement}; +use tracing::info; use super::settings::SETTINGS; @@ -18,7 +19,7 @@ pub async fn get_db_connection_pool() -> Result { let mut opts = ConnectOptions::new(&connection_string); opts.max_connections(20).connect_timeout(Duration::from_secs(5)); - println!("🚀 Connecting to the database..., {}", connection_string); + info!("🚀 Connecting to the database..., {}", connection_string); let db = Database::connect(uri).await?; let db = match db.get_database_backend() { DbBackend::MySql => { @@ -40,7 +41,7 @@ pub async fn get_db_connection_pool() -> Result { match exists { None => { - println!("🪹 Database does not exist, creating it"); + info!("🪹 Database does not exist, creating it"); db.execute(Statement::from_string( db.get_database_backend(), format!("CREATE DATABASE \"{}\";", db_name), @@ -48,7 +49,7 @@ pub async fn get_db_connection_pool() -> Result { .await?; } _ => { - println!("🛢️ Database already exists"); + info!("🛢️ Database already exists"); } } diff --git a/src/packages/errors.rs b/src/packages/errors.rs index 0faf244..686118c 100644 --- a/src/packages/errors.rs +++ b/src/packages/errors.rs @@ -2,6 +2,7 @@ use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; use bcrypt::BcryptError; +use jsonwebtoken; use sea_orm::{DbErr, TransactionError}; use serde_json::json; use std::io; @@ -24,7 +25,7 @@ pub enum Error { DbTransaction(Box, StatusCode), #[error("{0}")] - NotFound(#[from] NotFound), + NotFound(#[from] NotFoundError), #[error("{0}")] RunSyncTask(#[from] JoinError), @@ -37,6 +38,9 @@ pub enum Error { #[error("{0}")] SerdeJson(#[from] serde_json::Error), + + #[error("JWT error: {0}")] + Jwt(#[from] jsonwebtoken::errors::Error), } impl Error { @@ -46,6 +50,7 @@ impl Error { Error::BadRequest(err) => err.get_codes(), Error::NotFound(_) => (StatusCode::NOT_FOUND, 40003), Error::Authenticate(err) => err.get_codes(), + Error::Jwt(_) => (StatusCode::UNAUTHORIZED, 40012), // New JWT error code // 5XX Errors Error::RunSyncTask(_) => (StatusCode::INTERNAL_SERVER_ERROR, 5005), @@ -58,7 +63,7 @@ impl Error { } pub fn not_found() -> Self { - Error::NotFound(NotFound::Generic) + Error::NotFound(NotFoundError::Generic) } pub fn cannot_perform_operation(message: &str) -> Self { @@ -109,8 +114,8 @@ impl AuthenticateError { AuthenticateError::Locked => (StatusCode::LOCKED, 40006), AuthenticateError::NoResource => (StatusCode::FORBIDDEN, 40008), AuthenticateError::ActionForbidden => (StatusCode::FORBIDDEN, 40009), - AuthenticateError::MaxConcurrentSessions => (StatusCode::LOCKED, 40010), - AuthenticateError::InvalidApiCredentials => (StatusCode::FORBIDDEN, 40011), + AuthenticateError::MaxConcurrentSessions => (StatusCode::FORBIDDEN, 40010), + AuthenticateError::InvalidApiCredentials => (StatusCode::UNAUTHORIZED, 40011), } } } @@ -120,11 +125,9 @@ impl From> for Error { match err { TransactionError::Connection(db_err) => Error::DbTransaction(Box::new(db_err), StatusCode::INTERNAL_SERVER_ERROR), TransactionError::Transaction(db_err) => { - // Here we can check if db_err has a specific status code let status_code = match db_err { DbErr::RecordNotFound(_) => StatusCode::NOT_FOUND, DbErr::Custom(ref e) if e.contains("duplicate key value") => StatusCode::CONFLICT, - // Add more specific cases as needed _ => StatusCode::INTERNAL_SERVER_ERROR, }; Error::DbTransaction(Box::new(db_err), status_code) @@ -146,9 +149,19 @@ impl BadRequestError { } #[derive(thiserror::Error, Debug)] -pub enum NotFound { +pub enum NotFoundError { #[error("Not found")] Generic, + #[error("Session not found")] + SessionNotFound, + #[error("User not found")] + UserNotFound, + #[error("Client not found")] + ClientNotFound, + #[error("Resource Group not found")] + ResourceGroupNotFound, + #[error("Resource not found")] + ResourceNotFound, } #[derive(thiserror::Error, Debug)] diff --git a/src/packages/settings.rs b/src/packages/settings.rs index 2707b8b..0c0703b 100644 --- a/src/packages/settings.rs +++ b/src/packages/settings.rs @@ -37,6 +37,7 @@ pub struct Admin { #[derive(Debug, Clone, Deserialize)] pub struct Secrets { pub signing_key: String, + pub api_key_signing_secret: String, } #[derive(Debug, Clone, Deserialize)] @@ -66,6 +67,9 @@ impl Settings { if let Ok(signing_key) = env::var("SIGNING_KEY") { builder = builder.set_override("secrets.signing_key", signing_key)?; } + if let Ok(api_key_signing_secret) = env::var("API_KEY_SIGNING_SECRET") { + builder = builder.set_override("secrets.api_key_signing_secret", api_key_signing_secret)?; + } if let Ok(port) = env::var("PORT") { builder = builder.set_override("server.port", port)?; } @@ -94,6 +98,7 @@ impl Settings { builder = builder.set_override("default_cred.realm_id", default_cred.realm_id.to_string())?; builder = builder.set_override("default_cred.client_id", default_cred.client_id.to_string())?; builder = builder.set_override("default_cred.master_admin_user_id", default_cred.master_admin_user_id.to_string())?; + builder = builder.set_override("default_cred.master_api_key", default_cred.master_api_key.to_string())?; builder = builder.set_override("default_cred.resource_group_id", default_cred.resource_group_id.to_string())?; let resource_ids_value: Vec = default_cred.resource_ids.iter().map(|uuid| Value::new(None, uuid.to_string())).collect(); @@ -103,6 +108,7 @@ impl Settings { builder = builder.set_override("default_cred.client_id", "00000000-0000-0000-0000-000000000000")?; builder = builder.set_override("default_cred.master_admin_user_id", "00000000-0000-0000-0000-000000000000")?; builder = builder.set_override("default_cred.resource_group_id", "00000000-0000-0000-0000-000000000000")?; + builder = builder.set_override("default_cred.master_api_key", "00000000-0000-0000-0000-000000000000")?; builder = builder.set_override("default_cred.resource_ids", vec!["00000000-0000-0000-0000-000000000000"])?; } diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 49b87c1..d047387 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,19 +1,18 @@ -use axum::{ - middleware, - routing::{get, post}, - Router, -}; +use axum::{middleware, routing::post, Router}; use crate::{ - handlers::auth::{introspect, login, logout, logout_all, logout_current_session, logout_my_all_sessions, refresh_token, register}, + handlers::auth::{admin_login, introspect, login, logout, logout_all, logout_current_session, logout_my_all_sessions, refresh_token, register}, middleware::session_info_extractor::session_info_middleware, }; pub fn create_routes() -> Router { Router::new() + .route("/admin-login", post(admin_login)) .route("/login", post(login)) - .route("/logout", get(logout_current_session).post(logout)) - .route("/logout-all", get(logout_my_all_sessions).post(logout_all)) + .route("/logout", post(logout)) + .route("/logout-current-session", post(logout_current_session)) + .route("/logout-my-all-sessions", post(logout_my_all_sessions)) + .route("/logout-all", post(logout_all)) .route("/register", post(register)) .route("/refresh-token", post(refresh_token)) .route("/introspect", post(introspect)) diff --git a/src/services/auth.rs b/src/services/auth.rs index 0ff1350..4b7f531 100644 --- a/src/services/auth.rs +++ b/src/services/auth.rs @@ -1,6 +1,22 @@ -use crate::packages::{api_token::RefreshTokenClaims, errors::Error}; -use entity::{client, refresh_token}; -use sea_orm::{prelude::Uuid, ActiveModelTrait, DatabaseTransaction, EntityTrait, Set}; +use std::sync::Arc; + +use crate::{ + mappers::auth::LoginResponse, + middleware::session_info_extractor::SessionInfo, + packages::{ + api_token::RefreshTokenClaims, + db::AppState, + errors::{AuthenticateError, Error, NotFoundError}, + jwt_token::create, + settings::SETTINGS, + }, +}; +use chrono::{self, Duration, Utc}; +use entity::{client, refresh_token, resource, resource_group, session, user}; +use sea_orm::{ + prelude::Uuid, ActiveModelTrait, ColumnTrait, DatabaseConnection, DatabaseTransaction, DbErr, EntityTrait, QueryFilter, Set, TransactionTrait, +}; +use tracing::debug; pub async fn handle_refresh_token( txn: &DatabaseTransaction, @@ -16,6 +32,7 @@ pub async fn handle_refresh_token( realm_id: Set(client.realm_id), re_used_count: Set(0), locked_at: Set(None), + expires: Set((Utc::now() + Duration::seconds(client.refresh_token_lifetime as i64)).into()), ..Default::default() }; model.insert(txn).await? @@ -27,6 +44,7 @@ pub async fn handle_refresh_token( realm_id: Set(refresh_token.realm_id), re_used_count: Set(refresh_token.re_used_count + 1), locked_at: Set(None), + expires: Set((Utc::now() + Duration::seconds(client.refresh_token_lifetime as i64)).into()), ..Default::default() }; model.update(txn).await? @@ -34,3 +52,202 @@ pub async fn handle_refresh_token( Ok(RefreshTokenClaims::from(&refresh_token_model, client)) } + +pub async fn get_active_session_by_id(db: &DatabaseConnection, id: Uuid) -> Result { + let session = session::Entity::find_by_id(id).one(db).await?; + if session.is_none() { + debug!("No session found"); + return Err(Error::NotFound(NotFoundError::SessionNotFound)); + } + + let session = session.unwrap(); + Ok(session) +} + +pub async fn get_active_sessions_by_user_and_client_id( + db: &DatabaseConnection, + user_id: Uuid, + client_id: Uuid, +) -> Result, Error> { + let sessions = session::Entity::find() + .filter(session::Column::UserId.eq(user_id)) + .filter(session::Column::ClientId.eq(client_id)) + .filter(session::Column::Expires.gt(chrono::Local::now())) + .all(db) + .await?; + Ok(sessions) +} + +pub async fn create_session_and_refresh_token( + state: Arc, + user: user::Model, + client: client::Model, + resource_groups: resource_group::Model, + session_info: Arc, +) -> Result { + Ok(state + .db + .transaction(|txn| { + Box::pin(async move { + let result: Result = async { + let refresh_token_model = if client.use_refresh_token { + let model = refresh_token::ActiveModel { + id: Set(Uuid::now_v7()), + user_id: Set(user.id), + client_id: Set(Some(client.id)), + realm_id: Set(client.realm_id), + re_used_count: Set(0), + expires: Set((Utc::now() + Duration::seconds(client.refresh_token_lifetime as i64)).into()), + locked_at: Set(None), + ..Default::default() + }; + Some(model.insert(txn).await?) + } else { + None + }; + + let session = create_session( + &client, + &user, + resource_groups, + session_info, + refresh_token_model.as_ref().map(|x| x.id), + txn, + ) + .await?; + + let refresh_token = if let Some(refresh_token) = refresh_token_model { + let claims = RefreshTokenClaims::from(&refresh_token, &client); + Some(claims.create_token(&SETTINGS.read().secrets.signing_key).unwrap()) + } else { + None + }; + + Ok(LoginResponse { + access_token: session.access_token, + realm_id: user.realm_id, + user, + session_id: session.session_id, + client_id: client.id, + refresh_token, + }) + } + .await; + + result.map_err(|e| DbErr::Custom(e.to_string())) + }) + }) + .await?) +} + +pub async fn create_session( + client: &client::Model, + user: &user::Model, + resource_groups: resource_group::Model, + session_info: Arc, + refresh_token_id: Option, + db: &DatabaseTransaction, +) -> Result { + // Fetch resources + let resources = resource::Entity::find() + .filter(resource::Column::GroupId.eq(resource_groups.id)) + .filter(resource::Column::LockedAt.is_null()) + .all(db) + .await?; + + // TODO: if resource_groups_id is Some and resources are empty then return error else continue + if resources.is_empty() { + debug!("No resources found"); + return Err(Error::Authenticate(AuthenticateError::Locked)); + } + + let session_model = session::ActiveModel { + id: Set(Uuid::now_v7()), + user_id: Set(user.id), + client_id: Set(client.id), + ip_address: Set(session_info.ip_address.to_string()), + user_agent: Set(Some(session_info.user_agent.to_string())), + browser: Set(Some(session_info.browser.to_string())), + browser_version: Set(Some(session_info.browser_version.to_string())), + operating_system: Set(Some(session_info.operating_system.to_string())), + device_type: Set(Some(session_info.device_type.to_string())), + country_code: Set(session_info.country_code.to_string()), + refresh_token_id: Set(refresh_token_id), + expires: Set((chrono::Utc::now() + chrono::Duration::seconds(client.session_lifetime as i64)).into()), + ..Default::default() + }; + let session = session_model.insert(db).await?; + + let access_token = create( + user.clone(), + client, + resource_groups, + resources, + &session, + &SETTINGS.read().secrets.signing_key, + ) + .unwrap(); + + Ok(LoginResponse { + access_token, + realm_id: user.realm_id, + user: user.clone(), + session_id: session.id, + client_id: client.id, + refresh_token: None, + }) +} + +pub async fn get_active_resource_group_by_rcu( + db: &DatabaseConnection, + realm_id: Uuid, + client_id: Uuid, + user_id: Uuid, +) -> Result { + let resource_group = resource_group::Entity::find() + .filter(resource_group::Column::RealmId.eq(realm_id)) + .filter(resource_group::Column::ClientId.eq(client_id)) + .filter(resource_group::Column::UserId.eq(user_id)) + .one(db) + .await?; + if resource_group.is_none() { + debug!("No resource group found"); + return Err(Error::NotFound(NotFoundError::ResourceGroupNotFound)); + } + + let resource_group = resource_group.unwrap(); + if resource_group.locked_at.is_some() { + debug!("Resource group is locked"); + return Err(Error::Authenticate(AuthenticateError::Locked)); + } + Ok(resource_group) +} + +pub async fn get_active_resource_by_gu(db: &DatabaseConnection, group_id: Uuid, _user_id: Uuid) -> Result, Error> { + let resource = resource::Entity::find() + .filter(resource::Column::GroupId.eq(group_id)) + .filter(resource::Column::LockedAt.is_null()) + .all(db) + .await?; + if resource.is_empty() { + debug!("No resource found"); + return Err(Error::NotFound(NotFoundError::ResourceNotFound)); + } + + Ok(resource) +} + +pub async fn get_active_refresh_token_by_id(db: &DatabaseConnection, id: Uuid) -> Result { + let refresh_token = refresh_token::Entity::find_by_id(id).one(db).await?; + if refresh_token.is_none() { + debug!("No refresh token found"); + return Err(Error::not_found()); + } + + let refresh_token = refresh_token.unwrap(); + if refresh_token.locked_at.is_some() { + debug!("Refresh token is locked"); + return Err(Error::Authenticate(AuthenticateError::Locked)); + } + Ok(refresh_token) +} diff --git a/src/services/client.rs b/src/services/client.rs index fc82c41..8a25694 100644 --- a/src/services/client.rs +++ b/src/services/client.rs @@ -1,37 +1,51 @@ use chrono::Utc; +use entity::client; use sea_orm::{prelude::Uuid, ActiveModelTrait, ColumnTrait, DatabaseConnection, DeleteResult, EntityTrait, QueryFilter, Set}; +use tracing::debug; use crate::{ mappers::client::{CreateClientRequest, UpdateClientRequest}, - packages::errors::{AuthenticateError, Error}, + packages::errors::{AuthenticateError, Error, NotFoundError}, utils::default_resource_checker::is_default_client, }; -use entity::client; pub async fn get_all_clients(db: &DatabaseConnection, realm_id: Uuid) -> Result, Error> { Ok(client::Entity::find().filter(client::Column::RealmId.eq(realm_id)).all(db).await?) } +pub async fn get_client_by_id_and_realm_id(db: &DatabaseConnection, realm_id: Uuid, client_id: Uuid) -> Result, Error> { + Ok(client::Entity::find() + .filter(client::Column::RealmId.eq(realm_id)) + .filter(client::Column::Id.eq(client_id)) + .one(db) + .await?) +} + pub async fn get_client_by_id(db: &DatabaseConnection, client_id: Uuid) -> Result, Error> { Ok(client::Entity::find_by_id(client_id).one(db).await?) } -pub async fn insert_client(db: &DatabaseConnection, payload: CreateClientRequest) -> Result { +pub async fn insert_client(db: &DatabaseConnection, realm_id: Uuid, payload: CreateClientRequest) -> Result { let client = client::ActiveModel { id: Set(Uuid::now_v7()), name: Set(payload.name.to_owned()), - realm_id: Set(payload.realm_id), + realm_id: Set(realm_id), ..Default::default() }; Ok(client.insert(db).await?) } -pub async fn update_client_by_id(db: &DatabaseConnection, client_id: Uuid, payload: UpdateClientRequest) -> Result { +pub async fn update_client_by_id( + db: &DatabaseConnection, + realm_id: Uuid, + client_id: Uuid, + payload: UpdateClientRequest, +) -> Result { if is_default_client(client_id) && payload.lock == Some(true) { return Err(Error::cannot_perform_operation("Cannot lock the default client")); } - let client = get_client_by_id(db, client_id).await?; + let client = get_client_by_id_and_realm_id(db, realm_id, client_id).await?; match client { Some(client) => { let locked_at = match payload.lock { @@ -73,6 +87,25 @@ pub async fn update_client_by_id(db: &DatabaseConnection, client_id: Uuid, paylo } } -pub async fn delete_client_by_id(db: &DatabaseConnection, id: Uuid) -> Result { - Ok(client::Entity::delete_by_id(id).exec(db).await?) +pub async fn delete_client_by_id(db: &DatabaseConnection, realm_id: Uuid, id: Uuid) -> Result { + Ok(client::Entity::delete_many() + .filter(client::Column::RealmId.eq(realm_id)) + .filter(client::Column::Id.eq(id)) + .exec(db) + .await?) +} + +pub async fn get_active_client_by_id(db: &DatabaseConnection, client_id: Uuid) -> Result { + let client = client::Entity::find_by_id(client_id).one(db).await?; + if client.is_none() { + debug!("No client found"); + return Err(Error::NotFound(NotFoundError::ClientNotFound)); + } + + let client = client.unwrap(); + if client.locked_at.is_some() { + debug!("Client is locked"); + return Err(Error::Authenticate(AuthenticateError::Locked)); + } + Ok(client) } diff --git a/src/services/user.rs b/src/services/user.rs index 5bea313..0c0e9a9 100644 --- a/src/services/user.rs +++ b/src/services/user.rs @@ -1,7 +1,13 @@ -use crate::{mappers::auth::CreateUserRequest, packages::errors::Error, utils::hash::generate_password_hash}; +use crate::{ + mappers::auth::CreateUserRequest, + packages::errors::{AuthenticateError, Error, NotFoundError}, + utils::hash::generate_password_hash, +}; +use axum_extra::either::Either; use entity::{resource, resource_group, user}; use futures::future::join_all; -use sea_orm::{prelude::Uuid, ActiveModelTrait, DatabaseConnection, Set}; +use sea_orm::{prelude::Uuid, ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set}; +use tracing::debug; pub async fn insert_user(db: &DatabaseConnection, realm_id: Uuid, client_id: Uuid, payload: CreateUserRequest) -> Result { let password_hash = generate_password_hash(payload.password).await?; @@ -49,3 +55,63 @@ pub async fn insert_user(db: &DatabaseConnection, realm_id: Uuid, client_id: Uui Ok(user) } + +pub async fn get_active_user_by_id(db: &DatabaseConnection, id: Uuid) -> Result { + let user = user::Entity::find_by_id(id).one(db).await?; + if user.is_none() { + debug!("No user found"); + return Err(Error::NotFound(NotFoundError::UserNotFound)); + } + + let user = user.unwrap(); + if user.locked_at.is_some() { + debug!("User is locked"); + return Err(Error::Authenticate(AuthenticateError::Locked)); + } + Ok(user) +} + +pub async fn get_active_user_and_resource_groups( + db: &DatabaseConnection, + user_identifier: Either, + realm_id: Uuid, + client_id: Uuid, +) -> Result<(user::Model, resource_group::Model), Error> { + let mut query = user::Entity::find(); + query = match user_identifier { + Either::E1(email) => query.filter(user::Column::Email.eq(email)), + Either::E2(user_id) => query.filter(user::Column::Id.eq(user_id)), + }; + + let user_with_resource_groups = query + .find_also_related(resource_group::Entity) + .filter(resource_group::Column::RealmId.eq(realm_id)) + .filter(resource_group::Column::ClientId.eq(client_id)) + .one(db) + .await?; + + if user_with_resource_groups.is_none() { + debug!("No matching data found"); + return Err(Error::not_found()); + } + + let (user, resource_groups) = user_with_resource_groups.unwrap(); + + if user.locked_at.is_some() { + debug!("User is locked"); + return Err(Error::Authenticate(AuthenticateError::Locked)); + } + + if resource_groups.is_none() { + debug!("No matching resource group found"); + return Err(Error::not_found()); + } + + let resource_groups = resource_groups.unwrap(); + if resource_groups.locked_at.is_some() { + debug!("Resource group is locked"); + return Err(Error::Authenticate(AuthenticateError::Locked)); + } + + Ok((user, resource_groups)) +} diff --git a/src/utils/authenticate_api_request.rs b/src/utils/authenticate_api_request.rs index 0cf891f..ebb3acc 100644 --- a/src/utils/authenticate_api_request.rs +++ b/src/utils/authenticate_api_request.rs @@ -18,7 +18,6 @@ where let state = parts.extensions.get::>().expect("AppState not found"); if let Some(api_key) = parts.headers.get("Api-Key").and_then(|v| v.to_str().ok()) { - println!("API_KEY::: {:?}", &api_key); return ApiUser::validate_cred(&state.db, api_key).await; } Err(Error::Authenticate(AuthenticateError::InvalidApiCredentials)) diff --git a/src/utils/helpers/default_cred.rs b/src/utils/helpers/default_cred.rs index 1209f10..767d357 100644 --- a/src/utils/helpers/default_cred.rs +++ b/src/utils/helpers/default_cred.rs @@ -11,6 +11,7 @@ pub struct DefaultCred { pub realm_id: Uuid, pub client_id: Uuid, pub master_admin_user_id: Uuid, + pub master_api_key: String, pub resource_group_id: Uuid, pub resource_ids: Vec, } diff --git a/src/utils/helpers/generate_random_string.rs b/src/utils/helpers/generate_random_string.rs deleted file mode 100644 index 40da76a..0000000 --- a/src/utils/helpers/generate_random_string.rs +++ /dev/null @@ -1,18 +0,0 @@ -use base64::{engine::general_purpose::STANDARD, Engine}; -use rand::RngCore; - -pub enum Length { - // U32, - U64, -} - -pub fn generate_random_string(length: Length) -> String { - let length = match length { - // Length::U32 => 32, - Length::U64 => 64, - }; - - let mut bytes = vec![0u8; length]; - rand::thread_rng().fill_bytes(&mut bytes); - STANDARD.encode(&bytes) -} diff --git a/src/utils/helpers/mod.rs b/src/utils/helpers/mod.rs index e897b01..ec9691a 100644 --- a/src/utils/helpers/mod.rs +++ b/src/utils/helpers/mod.rs @@ -1,2 +1 @@ pub mod default_cred; -pub mod generate_random_string; diff --git a/src/utils/role_checker.rs b/src/utils/role_checker.rs index 6f2ca33..628ecce 100644 --- a/src/utils/role_checker.rs +++ b/src/utils/role_checker.rs @@ -1,4 +1,4 @@ -use entity::sea_orm_active_enums::{ApiUserAccess, ApiUserRole}; +use entity::sea_orm_active_enums::{ApiUserAccess, ApiUserScope}; use sea_orm::prelude::Uuid; use crate::packages::{api_token::ApiUser, jwt_token::JwtUser}; @@ -30,8 +30,8 @@ pub fn is_current_realm_admin(user: &JwtUser, realm_id: &str) -> bool { }) } -pub async fn has_access_to_api_cred(api_user: &ApiUser, role: ApiUserRole, access: ApiUserAccess) -> bool { - if api_user.role != role { +pub async fn has_access_to_api_cred(api_user: &ApiUser, role: ApiUserScope, access: ApiUserAccess) -> bool { + if !api_user.role.has_access(role) { return false; }