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 @@
-
\ 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 @@
-
\ 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