Test change pass
This commit is contained in:
parent
ba4e6a377f
commit
09d164576f
217
Cargo.lock
generated
217
Cargo.lock
generated
@ -4,9 +4,9 @@ version = 3
|
||||
|
||||
[[package]]
|
||||
name = "actix"
|
||||
version = "0.13.1"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cba56612922b907719d4a01cf11c8d5b458e7d3dba946d0435f20f58d6795ed2"
|
||||
checksum = "dc9ef49f64074352f73ef9ec8c060a5f5799c96715c986a17f10933c3da2955c"
|
||||
dependencies = [
|
||||
"actix-macros",
|
||||
"actix-rt",
|
||||
@ -29,11 +29,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "actix-codec"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8"
|
||||
checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bitflags 2.4.2",
|
||||
"bytes 1.5.0",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@ -46,9 +46,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "actix-http"
|
||||
version = "3.5.1"
|
||||
version = "3.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "129d4c88e98860e1758c5de288d1632b07970a16d59bdf7b8d66053d582bb71f"
|
||||
checksum = "d223b13fd481fc0d1f83bb12659ae774d9e3601814c68a0bc539731698cca743"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-rt",
|
||||
@ -180,9 +180,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "actix-tls"
|
||||
version = "3.2.0"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "929e47cc23865cdb856e59673cfba2d28f00b3bbd060dfc80e33a00a3cea8317"
|
||||
checksum = "d4cce60a2f2b477bc72e5cde0af1812a6e82d8fd85b5570a5dcf2a5bf2c5be5f"
|
||||
dependencies = [
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
@ -209,9 +209,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "actix-web"
|
||||
version = "4.4.1"
|
||||
version = "4.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e43428f3bf11dee6d166b00ec2df4e3aa8cc1606aaa0b7433c146852e2f4e03b"
|
||||
checksum = "43a6556ddebb638c2358714d853257ed226ece6023ef9364f23f0c70737ea984"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-http",
|
||||
@ -548,6 +548,12 @@ version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "basen"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dbe4bb73fd931c4d1aaf53b35d1286c8a948ad00ec92c8e3c856f15fd027f43"
|
||||
|
||||
[[package]]
|
||||
name = "bigdecimal"
|
||||
version = "0.3.1"
|
||||
@ -677,9 +683,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bytecheck"
|
||||
version = "0.6.11"
|
||||
version = "0.6.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627"
|
||||
checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
|
||||
dependencies = [
|
||||
"bytecheck_derive",
|
||||
"ptr_meta",
|
||||
@ -688,9 +694,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bytecheck_derive"
|
||||
version = "0.6.11"
|
||||
version = "0.6.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61"
|
||||
checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -936,9 +942,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "curl-sys"
|
||||
version = "0.4.70+curl-8.5.0"
|
||||
version = "0.4.71+curl-8.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c0333d8849afe78a4c8102a429a446bfdd055832af071945520e835ae2d841e"
|
||||
checksum = "c7b12a7ab780395666cb576203dc3ed6e01513754939a600b85196ccf5356bc5"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@ -1661,6 +1667,12 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.28"
|
||||
@ -1714,9 +1726,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.59"
|
||||
version = "0.1.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539"
|
||||
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
@ -1759,9 +1771,9 @@ checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.1.0"
|
||||
version = "2.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
|
||||
checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.3",
|
||||
@ -1870,9 +1882,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.0"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0"
|
||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
@ -1891,6 +1903,8 @@ dependencies = [
|
||||
"actix-jwt-session",
|
||||
"actix-web",
|
||||
"async-trait",
|
||||
"base64 0.21.7",
|
||||
"basen",
|
||||
"bincode",
|
||||
"chrono",
|
||||
"derive_more",
|
||||
@ -1899,7 +1913,9 @@ dependencies = [
|
||||
"figment",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"hmac",
|
||||
"http-api-isahc-client",
|
||||
"humantime",
|
||||
"jet-contract",
|
||||
"oauth2",
|
||||
"oauth2-amazon",
|
||||
@ -1909,6 +1925,7 @@ dependencies = [
|
||||
"oauth2-gitlab",
|
||||
"oauth2-google",
|
||||
"oauth2-signin",
|
||||
"password-hash",
|
||||
"rand",
|
||||
"reqwest",
|
||||
"rumqttc",
|
||||
@ -1919,11 +1936,13 @@ dependencies = [
|
||||
"serde-aux",
|
||||
"serde-email",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sqlx",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tracing-test",
|
||||
"uuid",
|
||||
"validators",
|
||||
]
|
||||
@ -2035,9 +2054,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.152"
|
||||
version = "0.2.153"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@ -2160,13 +2179,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "maybe-async"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f1b8c13cb1f814b634a96b2c725449fe7ed464a7b8781de8688be5ffbd3f305"
|
||||
checksum = "afc95a651c82daf7004c824405aa1019723644950d488571bd718e3ed84646ed"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2224,9 +2243,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
|
||||
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
|
||||
dependencies = [
|
||||
"adler",
|
||||
]
|
||||
@ -2320,6 +2339,12 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.45"
|
||||
@ -3081,18 +3106,18 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
|
||||
[[package]]
|
||||
name = "rend"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd"
|
||||
checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
|
||||
dependencies = [
|
||||
"bytecheck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.23"
|
||||
version = "0.11.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41"
|
||||
checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"bytes 1.5.0",
|
||||
@ -3119,6 +3144,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
@ -3131,7 +3157,7 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots 0.25.3",
|
||||
"webpki-roots 0.25.4",
|
||||
"winreg",
|
||||
]
|
||||
|
||||
@ -3166,9 +3192,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rkyv"
|
||||
version = "0.7.43"
|
||||
version = "0.7.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527a97cdfef66f65998b5f3b637c26f5a5ec09cc52a3f9932313ac645f4190f5"
|
||||
checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0"
|
||||
dependencies = [
|
||||
"bitvec",
|
||||
"bytecheck",
|
||||
@ -3184,9 +3210,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rkyv_derive"
|
||||
version = "0.7.43"
|
||||
version = "0.7.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5c462a1328c8e67e4d6dbad1eb0355dd43e8ab432c6e227a43657f16ade5033"
|
||||
checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3278,9 +3304,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "1.33.1"
|
||||
version = "1.34.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06676aec5ccb8fc1da723cc8c0f9a46549f21ebb8753d3915c6c41db1e7f1dc4"
|
||||
checksum = "755392e1a2f77afd95580d3f0d0e94ac83eeeb7167552c9b5bca549e61a94d83"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"borsh",
|
||||
@ -3309,9 +3335,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.30"
|
||||
version = "0.38.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca"
|
||||
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"errno",
|
||||
@ -3572,9 +3598,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry"
|
||||
version = "0.32.1"
|
||||
version = "0.32.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab18211f62fb890f27c9bb04861f76e4be35e4c2fcbfc2d98afa37aadebb16f1"
|
||||
checksum = "766448f12e44d68e675d5789a261515c46ac6ccd240abdd451a9c46c84a49523"
|
||||
dependencies = [
|
||||
"isahc 0.9.14",
|
||||
"reqwest",
|
||||
@ -3587,14 +3613,14 @@ dependencies = [
|
||||
"sentry-tracing",
|
||||
"tokio",
|
||||
"ureq",
|
||||
"webpki-roots 0.25.3",
|
||||
"webpki-roots 0.25.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-backtrace"
|
||||
version = "0.32.1"
|
||||
version = "0.32.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf018ff7d5ce5b23165a9cbfee60b270a55ae219bc9eebef2a3b6039356dd7e5"
|
||||
checksum = "32701cad8b3c78101e1cd33039303154791b0ff22e7802ed8cc23212ef478b45"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"once_cell",
|
||||
@ -3604,9 +3630,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-contexts"
|
||||
version = "0.32.1"
|
||||
version = "0.32.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d934df6f9a17b8c15b829860d9d6d39e78126b5b970b365ccbd817bc0fe82c9"
|
||||
checksum = "17ddd2a91a13805bd8dab4ebf47323426f758c35f7bf24eacc1aded9668f3824"
|
||||
dependencies = [
|
||||
"hostname",
|
||||
"libc",
|
||||
@ -3618,9 +3644,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-core"
|
||||
version = "0.32.1"
|
||||
version = "0.32.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e362d3fb1c5de5124bf1681086eaca7adf6a8c4283a7e1545359c729f9128ff"
|
||||
checksum = "b1189f68d7e7e102ef7171adf75f83a59607fafd1a5eecc9dc06c026ff3bdec4"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"rand",
|
||||
@ -3631,9 +3657,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-log"
|
||||
version = "0.32.1"
|
||||
version = "0.32.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7574422f662fe062a2ef7d027659ad8745e05fb815770887aeb08e2fb92cf6"
|
||||
checksum = "d2d7cd58e7b31a1a533163abf86c182824ea8f8867853a6b402cde053595a54b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"sentry-core",
|
||||
@ -3641,9 +3667,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-panic"
|
||||
version = "0.32.1"
|
||||
version = "0.32.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0224e7a8e2bd8a32d96804acb8243d6d6e073fed55618afbdabae8249a964d8"
|
||||
checksum = "d1c18d0b5fba195a4950f2f4c31023725c76f00aabb5840b7950479ece21b5ca"
|
||||
dependencies = [
|
||||
"sentry-backtrace",
|
||||
"sentry-core",
|
||||
@ -3651,9 +3677,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-tracing"
|
||||
version = "0.32.1"
|
||||
version = "0.32.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "087bed8c616d176a9c6b662a8155e5f23b40dc9e1fa96d0bd5fb56e8636a9275"
|
||||
checksum = "3012699a9957d7f97047fd75d116e22d120668327db6e7c59824582e16e791b2"
|
||||
dependencies = [
|
||||
"sentry-backtrace",
|
||||
"sentry-core",
|
||||
@ -3663,9 +3689,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-types"
|
||||
version = "0.32.1"
|
||||
version = "0.32.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb4f0e37945b7a8ce7faebc310af92442e2d7c5aa7ef5b42fe6daa98ee133f65"
|
||||
checksum = "c7173fd594569091f68a7c37a886e202f4d0c1db1e1fa1d18a051ba695b2e2ec"
|
||||
dependencies = [
|
||||
"debugid",
|
||||
"hex",
|
||||
@ -3752,9 +3778,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.112"
|
||||
version = "1.0.113"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed"
|
||||
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@ -3974,7 +4000,7 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c"
|
||||
dependencies = [
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"nom",
|
||||
"unicode_categories",
|
||||
]
|
||||
@ -4290,6 +4316,12 @@ dependencies = [
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.5.1"
|
||||
@ -4362,12 +4394,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.31"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e"
|
||||
checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
@ -4382,10 +4415,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.16"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f"
|
||||
checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
@ -4406,9 +4440,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.35.1"
|
||||
version = "1.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
|
||||
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes 1.5.0",
|
||||
@ -4529,9 +4563,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.8"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
|
||||
checksum = "c6a4b9e8023eb94392d3dca65d717c53abc5dad49c07cb65bb8fcd87115fa325"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
@ -4550,9 +4584,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.21.0"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
|
||||
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@ -4653,6 +4687,29 @@ dependencies = [
|
||||
"tracing-serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-test"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a2c0ff408fe918a94c428a3f2ad04e4afd5c95bbc08fcf868eff750c15728a4"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"tracing-core",
|
||||
"tracing-subscriber",
|
||||
"tracing-test-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-test-macro"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "258bc1c4f8e2e73a977812ab339d503e6feeb92700f6d07a6de4d321522d5c08"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
@ -4749,7 +4806,7 @@ dependencies = [
|
||||
"rustls 0.21.10",
|
||||
"rustls-webpki",
|
||||
"url",
|
||||
"webpki-roots 0.25.3",
|
||||
"webpki-roots 0.25.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4927,9 +4984,9 @@ checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7"
|
||||
checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
@ -4969,9 +5026,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.25.3"
|
||||
version = "0.25.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10"
|
||||
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
@ -5144,9 +5201,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.35"
|
||||
version = "0.5.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1931d78a9c73861da0134f453bb1f790ce49b2e30eba8410b4b79bac72b46a2d"
|
||||
checksum = "a7cad8365489051ae9f054164e459304af2e7e9bb407c958076c8bf4aef52da5"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -1,9 +1,10 @@
|
||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
||||
|
||||
use super::sea_orm_active_enums::Roles;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::sea_orm_active_enums::Roles;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "instance_admins")]
|
||||
pub struct Model {
|
||||
|
@ -1,9 +1,10 @@
|
||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
||||
|
||||
use super::sea_orm_active_enums::ProjectMemberRoles;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::sea_orm_active_enums::ProjectMemberRoles;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "project_member_invites")]
|
||||
pub struct Model {
|
||||
|
@ -1,9 +1,10 @@
|
||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
||||
|
||||
use super::sea_orm_active_enums::ProjectMemberRoles;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::sea_orm_active_enums::ProjectMemberRoles;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "project_members")]
|
||||
pub struct Model {
|
||||
|
@ -1,9 +1,10 @@
|
||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
||||
|
||||
use super::sea_orm_active_enums::Roles;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::sea_orm_active_enums::Roles;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "workspace_member_invites")]
|
||||
pub struct Model {
|
||||
|
@ -1,9 +1,10 @@
|
||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
||||
|
||||
use super::sea_orm_active_enums::Roles;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::sea_orm_active_enums::Roles;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "workspace_members")]
|
||||
pub struct Model {
|
||||
|
@ -43,3 +43,13 @@ dotenv = "0.15.0"
|
||||
chrono = { version = "0.4.32", default-features = false, features = ["clock", "serde"] }
|
||||
validators = { version = "0.25.3", default-features = false, features = ["email", "derive", "all-validators"] }
|
||||
sentry = { version = "0.32.1", default-features = false, features = ["tokio", "rustls", "tracing", "isahc", "sentry-backtrace", "sentry-log", "sentry-contexts", "backtrace", "panic"] }
|
||||
basen = "0.1.0"
|
||||
base64 = "0.21.7"
|
||||
hmac = { version = "0.12.1", features = ["std"] }
|
||||
sha2 = "0.10.8"
|
||||
humantime = "2.1.0"
|
||||
password-hash = "0.5.0"
|
||||
tracing-test = { version = "0.2.4", features = ["no-env-filter"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-test = "0.2.4"
|
||||
|
@ -20,6 +20,8 @@
|
||||
* `HAS_OPENAI_CONFIGURED` - added for compatibility but not supported
|
||||
* `FILE_SIZE_LIMIT` - maximum file size
|
||||
* `IS_SELF_MANAGED` - always `true` but added for compatibility with `plane`
|
||||
* `PASSWORD_RESET_SECRET` - mandatory secret to encrypt password reset token
|
||||
* `PASSWORD_RESET_TIMEOUT` - mandatory duration for password reset token, example: "12h 5min 2ns"
|
||||
|
||||
### Optional Environment
|
||||
|
||||
|
BIN
crates/jet-api/config/jwt-decoding.bin
Normal file
BIN
crates/jet-api/config/jwt-decoding.bin
Normal file
Binary file not shown.
BIN
crates/jet-api/config/jwt-encoding.bin
Normal file
BIN
crates/jet-api/config/jwt-encoding.bin
Normal file
Binary file not shown.
@ -1,4 +1,5 @@
|
||||
use figment::{providers::Env, Figment};
|
||||
use figment::providers::Env;
|
||||
use figment::Figment;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_aux::field_attributes::deserialize_bool_from_anything;
|
||||
use serde_aux::serde_introspection::serde_introspect;
|
||||
|
@ -1,7 +1,5 @@
|
||||
use figment::{
|
||||
value::{Dict, Map},
|
||||
Error, Profile, Provider,
|
||||
};
|
||||
use figment::value::{Dict, Map};
|
||||
use figment::{Error, Profile, Provider};
|
||||
use jet_contract::redis;
|
||||
use jet_contract::redis::Commands;
|
||||
use tracing::debug;
|
||||
|
@ -1,8 +1,10 @@
|
||||
use actix_web::{guard::Guard, web::Data, FromRequest};
|
||||
use actix_web::web::Data;
|
||||
use actix_web::FromRequest;
|
||||
use derive_more::*;
|
||||
use futures_util::{future::LocalBoxFuture, FutureExt};
|
||||
use futures_util::future::LocalBoxFuture;
|
||||
use futures_util::FutureExt;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use tracing::warn;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
#[display(fmt = "{{\"error\":\"Instance is not configured\"}}")]
|
||||
@ -53,21 +55,26 @@ impl FromRequest for RequireInstanceConfigured {
|
||||
req: &actix_web::HttpRequest,
|
||||
_payload: &mut actix_web::dev::Payload,
|
||||
) -> Self::Future {
|
||||
debug!("Looking for instance");
|
||||
let db = req.app_data::<Data<DatabaseConnection>>().cloned();
|
||||
debug!("Looking for instance: db found");
|
||||
async move {
|
||||
let Some(db) = db else {
|
||||
warn!("Failed to fetch database connection fot required instance configured");
|
||||
return Err(NoInstance);
|
||||
};
|
||||
debug!("Looking for instance: db exists");
|
||||
use sea_orm::EntityTrait;
|
||||
let Ok(Some(instance)) = entities::prelude::Instances::find().one(&**db).await else {
|
||||
warn!("No instance found");
|
||||
return Err(NoInstance);
|
||||
};
|
||||
debug!("Looking for instance: instance found");
|
||||
if !instance.is_setup_done {
|
||||
warn!("Instance is not configured");
|
||||
return Err(NoInstance);
|
||||
}
|
||||
debug!("Looking for instance: instance is configured");
|
||||
return Ok(Self(instance));
|
||||
}
|
||||
.boxed_local()
|
||||
|
@ -1,11 +1,8 @@
|
||||
use crate::models::{Error, JsonError};
|
||||
use crate::session::AppClaims;
|
||||
use actix_jwt_session::{
|
||||
Duration, Hashing, JwtTtl, Pair, RefreshTtl, SessionStorage, JWT_HEADER_NAME,
|
||||
REFRESH_HEADER_NAME,
|
||||
};
|
||||
use actix_web::web::scope;
|
||||
use actix_web::web::{Data, ServiceConfig};
|
||||
use actix_web::web::{scope, Data, ServiceConfig};
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use entities::prelude::Users;
|
||||
use entities::users::Model as User;
|
||||
@ -18,13 +15,19 @@ use validators::prelude::*;
|
||||
use validators::Validator;
|
||||
use validators_prelude::Host;
|
||||
|
||||
use crate::models::{Error, JsonError};
|
||||
use crate::session::AppClaims;
|
||||
|
||||
mod change_password;
|
||||
mod email_check;
|
||||
mod magic_generate;
|
||||
mod magic_sign_in;
|
||||
mod reset_password;
|
||||
mod sign_in;
|
||||
mod sign_out;
|
||||
mod sign_up;
|
||||
mod social_auth;
|
||||
mod token_refresh;
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct AuthResponseBody {
|
||||
@ -57,13 +60,17 @@ pub fn configure(http_client: reqwest::Client, config: &mut ServiceConfig) {
|
||||
.service(sign_up::sign_up)
|
||||
.service(sign_out::sign_out)
|
||||
.service(magic_sign_in::magic_sign_in)
|
||||
.service(token_refresh::refresh_token)
|
||||
.service(change_password::change_password)
|
||||
.configure(|c| {
|
||||
social_auth::configure(http_client, c);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, derive_more::Display)]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, derive_more::Display,
|
||||
)]
|
||||
pub enum PublishError {
|
||||
#[display(fmt = "Failed to inform system modules about created user account")]
|
||||
UserCreated,
|
||||
@ -73,7 +80,7 @@ pub enum PublishError {
|
||||
MagicLinkEmail,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, derive_more::Display)]
|
||||
#[derive(Debug, Clone, PartialEq, derive_more::Display, serde::Serialize, serde::Deserialize)]
|
||||
pub enum OAuthError {
|
||||
#[display(fmt = "Failed to load workspace invides for {user_id} on {provider} callback")]
|
||||
FetchWorkspaceInvites { user_id: Uuid, provider: String },
|
||||
@ -83,7 +90,15 @@ pub enum OAuthError {
|
||||
ConnectSocialMedia { user_id: Uuid, provider: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, derive_more::Display, derive_more::From)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
derive_more::Display,
|
||||
derive_more::From,
|
||||
serde::Serialize,
|
||||
serde::Deserialize,
|
||||
)]
|
||||
pub enum AuthError {
|
||||
#[display(fmt = "New account creation is disabled. Please contact your site administrator")]
|
||||
RegisterOff,
|
||||
@ -232,33 +247,199 @@ pub fn random_password() -> String {
|
||||
|
||||
pub mod password {
|
||||
const HAS_NUM: u8 = 1;
|
||||
const HAS_UPPER: u8 = 2;
|
||||
const HAS_LOWER: u8 = 4;
|
||||
const HAS_SPECIAL: u8 = 8;
|
||||
const HAS_UPPER: u8 = 1 << 1;
|
||||
const HAS_LOWER: u8 = 1 << 2;
|
||||
const HAS_SPECIAL: u8 = 1 << 3;
|
||||
const TOO_SHORT: u8 = 1;
|
||||
const TOO_LONG: u8 = 2;
|
||||
|
||||
#[derive(Debug, PartialEq, serde::Serialize)]
|
||||
#[cfg_attr(test, derive(serde::Deserialize))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PassValidity {
|
||||
Valid,
|
||||
Errors {
|
||||
has_lower: bool,
|
||||
has_upper: bool,
|
||||
has_num: bool,
|
||||
has_special: bool,
|
||||
too_short: bool,
|
||||
too_long: bool,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn validate(pass: &str) -> PassValidity {
|
||||
let chars = check_characters(pass);
|
||||
let len = check_len(pass);
|
||||
tracing::debug!("chars validation: {chars}");
|
||||
if chars == HAS_LOWER | HAS_UPPER | HAS_NUM | HAS_SPECIAL && len == 0 {
|
||||
PassValidity::Valid
|
||||
} else {
|
||||
PassValidity::Errors {
|
||||
has_lower: chars & HAS_LOWER != 0,
|
||||
has_upper: chars & HAS_UPPER != 0,
|
||||
has_num: chars & HAS_NUM != 0,
|
||||
has_special: chars & HAS_SPECIAL != 0,
|
||||
too_short: len == TOO_SHORT,
|
||||
too_long: len == TOO_LONG,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid(pass: &str) -> bool {
|
||||
pass.len() >= 8
|
||||
|| pass.chars().fold(0, |memo, c| match c {
|
||||
(8..60).contains(&pass.len())
|
||||
|| check_characters(pass) & HAS_NUM & HAS_SPECIAL & HAS_UPPER & HAS_LOWER != 0
|
||||
}
|
||||
|
||||
fn check_len(pass: &str) -> u8 {
|
||||
match pass.len() {
|
||||
n if n < 8 => TOO_SHORT,
|
||||
n if n > 60 => TOO_LONG,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn check_characters(pass: &str) -> u8 {
|
||||
pass.chars().fold(0, |memo, c| {
|
||||
tracing::trace!(
|
||||
"char is {c} num {} upper {} lower {}",
|
||||
c.is_numeric(),
|
||||
c.is_uppercase(),
|
||||
c.is_lowercase()
|
||||
);
|
||||
match c {
|
||||
_ if c.is_numeric() => memo | HAS_NUM,
|
||||
_ if c.is_uppercase() => memo | HAS_UPPER,
|
||||
_ if c.is_lowercase() => memo | HAS_UPPER,
|
||||
_ if c.is_lowercase() => memo | HAS_LOWER,
|
||||
_ => memo | HAS_SPECIAL,
|
||||
}) & HAS_NUM
|
||||
& HAS_SPECIAL
|
||||
& HAS_UPPER
|
||||
& HAS_LOWER
|
||||
!= 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn missing_lower() {
|
||||
let res = validate("1A^");
|
||||
let expected = PassValidity::Errors {
|
||||
has_lower: false,
|
||||
has_upper: true,
|
||||
has_num: true,
|
||||
has_special: true,
|
||||
too_short: true,
|
||||
too_long: false,
|
||||
};
|
||||
assert_eq!(res, expected);
|
||||
|
||||
let res = validate("A^1");
|
||||
let expected = PassValidity::Errors {
|
||||
has_lower: false,
|
||||
has_upper: true,
|
||||
has_num: true,
|
||||
has_special: true,
|
||||
too_short: true,
|
||||
too_long: false,
|
||||
};
|
||||
assert_eq!(res, expected);
|
||||
|
||||
let res = validate("^1A");
|
||||
let expected = PassValidity::Errors {
|
||||
has_lower: false,
|
||||
has_upper: true,
|
||||
has_num: true,
|
||||
has_special: true,
|
||||
too_short: true,
|
||||
too_long: false,
|
||||
};
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_upper() {
|
||||
let res = validate("1a^");
|
||||
let expected = PassValidity::Errors {
|
||||
has_lower: true,
|
||||
has_upper: false,
|
||||
has_num: true,
|
||||
has_special: true,
|
||||
too_short: true,
|
||||
too_long: false,
|
||||
};
|
||||
assert_eq!(res, expected);
|
||||
|
||||
let res = validate("a^1");
|
||||
let expected = PassValidity::Errors {
|
||||
has_lower: true,
|
||||
has_upper: false,
|
||||
has_num: true,
|
||||
has_special: true,
|
||||
too_short: true,
|
||||
too_long: false,
|
||||
};
|
||||
assert_eq!(res, expected);
|
||||
|
||||
let res = validate("^1a");
|
||||
let expected = PassValidity::Errors {
|
||||
has_lower: true,
|
||||
has_upper: false,
|
||||
has_num: true,
|
||||
has_special: true,
|
||||
too_short: true,
|
||||
too_long: false,
|
||||
};
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_num() {
|
||||
let res = validate("Aa^");
|
||||
let expected = PassValidity::Errors {
|
||||
has_lower: true,
|
||||
has_upper: true,
|
||||
has_num: false,
|
||||
has_special: true,
|
||||
too_short: true,
|
||||
too_long: false,
|
||||
};
|
||||
assert_eq!(res, expected);
|
||||
|
||||
let res = validate("a^A");
|
||||
let expected = PassValidity::Errors {
|
||||
has_lower: true,
|
||||
has_upper: true,
|
||||
has_num: false,
|
||||
has_special: true,
|
||||
too_short: true,
|
||||
too_long: false,
|
||||
};
|
||||
assert_eq!(res, expected);
|
||||
|
||||
let res = validate("A^a");
|
||||
let expected = PassValidity::Errors {
|
||||
has_lower: true,
|
||||
has_upper: true,
|
||||
has_num: false,
|
||||
has_special: true,
|
||||
too_short: true,
|
||||
too_long: false,
|
||||
};
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod magic_link {
|
||||
use crate::models::Error;
|
||||
use crate::{http::AuthError, redis_c, RedisClient};
|
||||
use actix_web::web::Data;
|
||||
use jet_contract::*;
|
||||
use rand::prelude::*;
|
||||
use redis::AsyncCommands;
|
||||
|
||||
use crate::http::AuthError;
|
||||
use crate::models::Error;
|
||||
use crate::{redis_c, RedisClient};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum AttemptValidity {
|
||||
Allowed,
|
||||
@ -366,10 +547,11 @@ pub mod magic_link {
|
||||
|
||||
#[cfg(test)]
|
||||
mod create_magic_link_tests {
|
||||
use super::*;
|
||||
use actix_web::web::Data;
|
||||
use jet_contract::deadpool_redis;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn full() {
|
||||
let email = "foo@bar.com".to_string();
|
||||
|
421
crates/jet-api/src/http/api/authentication/change_password.rs
Normal file
421
crates/jet-api/src/http/api/authentication/change_password.rs
Normal file
@ -0,0 +1,421 @@
|
||||
use actix_jwt_session::Authenticated;
|
||||
use actix_web::web::{Data, Json};
|
||||
use actix_web::{post, HttpResponse};
|
||||
use sea_orm::{DatabaseConnection, DatabaseTransaction, EntityTrait, Set};
|
||||
use serde_json::json;
|
||||
use tracing::warn;
|
||||
|
||||
use super::password::*;
|
||||
use crate::extractors::RequireInstanceConfigured;
|
||||
use crate::models::{Error, JsonError, JsonErrorDetails};
|
||||
use crate::session::AppClaims;
|
||||
use crate::utils::SetPassword;
|
||||
use crate::{db_commit, db_rollback, db_t, utils};
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[cfg_attr(test, derive(serde::Serialize))]
|
||||
struct Input {
|
||||
new_password: String,
|
||||
old_password: String,
|
||||
confirm_password: String,
|
||||
}
|
||||
|
||||
#[post("/users/me/change-password")]
|
||||
pub async fn change_password(
|
||||
_: RequireInstanceConfigured,
|
||||
input: Json<Input>,
|
||||
auth: Authenticated<AppClaims>,
|
||||
db: Data<DatabaseConnection>,
|
||||
) -> Result<HttpResponse, JsonErrorDetails<PassValidity>> {
|
||||
let mut t = db_t!(db)?;
|
||||
|
||||
match try_change_password(input, auth, &mut t).await {
|
||||
Ok(r) => {
|
||||
db_commit!(t)?;
|
||||
Ok(r)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to change password: {e}");
|
||||
db_rollback!(t).ok();
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_change_password(
|
||||
input: Json<Input>,
|
||||
auth: Authenticated<AppClaims>,
|
||||
t: &mut DatabaseTransaction,
|
||||
) -> Result<HttpResponse, JsonErrorDetails<PassValidity>> {
|
||||
let user = entities::prelude::Users::find_by_id(auth.account_id())
|
||||
.one(&mut *t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to find user while change password: {e}");
|
||||
Error::DatabaseError
|
||||
})?
|
||||
.ok_or(Error::UserRequired)?;
|
||||
|
||||
let input = input.into_inner();
|
||||
if input.new_password != input.confirm_password {
|
||||
return Err(JsonError::new(
|
||||
"Confirm password should be same as the new password.",
|
||||
))?;
|
||||
}
|
||||
match validate(&input.new_password) {
|
||||
PassValidity::Valid => {}
|
||||
e => return Err(JsonError::new("New password is not correct").with_details(e))?,
|
||||
};
|
||||
|
||||
if utils::verify_password(&user.password, &input.new_password).is_ok() {
|
||||
return Err(JsonError::new(
|
||||
"New password cannot be same as old password.",
|
||||
))?;
|
||||
}
|
||||
if utils::verify_password(&user.password, &input.old_password).is_err() {
|
||||
return Err(JsonError::new("Old password is not correct"))?;
|
||||
}
|
||||
|
||||
let mut model: entities::users::ActiveModel = user.clone().into();
|
||||
model.set_password(&input.new_password).map_err(|e| {
|
||||
tracing::error!("Failed to encrypt new password: {e}");
|
||||
Error::EncryptPass
|
||||
})?;
|
||||
model.is_password_autoset = Set(false);
|
||||
entities::prelude::Users::update(model)
|
||||
.exec(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to update user: {e}");
|
||||
Error::DatabaseError
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(json!({ "message": "Password updated successfully" })))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_jwt_session::{
|
||||
Hashing, JwtTtl, RefreshTtl, SessionMiddlewareFactory, JWT_COOKIE_NAME, JWT_HEADER_NAME,
|
||||
REFRESH_COOKIE_NAME, REFRESH_HEADER_NAME,
|
||||
};
|
||||
use actix_web::body::to_bytes;
|
||||
use actix_web::http::header::ContentType;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{test, App};
|
||||
use jet_contract::deadpool_redis;
|
||||
use reqwest::{Method, StatusCode};
|
||||
use sea_orm::Database;
|
||||
use tracing_test::traced_test;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::*;
|
||||
use crate::session;
|
||||
|
||||
macro_rules! create_app {
|
||||
($app: ident, $session_storage: ident, $db: ident) => {
|
||||
std::env::set_var("DATABASE_URL", "postgres://postgres@0.0.0.0:5432/jet_test");
|
||||
let redis = deadpool_redis::Config::from_url("redis://0.0.0.0:6379")
|
||||
.create_pool(Some(deadpool_redis::Runtime::Tokio1))
|
||||
.expect("Can't connect to redis");
|
||||
let $db: sea_orm::prelude::DatabaseConnection =
|
||||
Database::connect("postgres://postgres@0.0.0.0:5432/jet_test")
|
||||
.await
|
||||
.expect("Failed to connect to database");
|
||||
let ($session_storage, factory) =
|
||||
SessionMiddlewareFactory::<session::AppClaims>::build_ed_dsa()
|
||||
.with_redis_pool(redis.clone())
|
||||
// Check if header "Authorization" exists and contains Bearer with encoded JWT
|
||||
.with_jwt_header(JWT_HEADER_NAME)
|
||||
// Check if cookie JWT exists and contains encoded JWT
|
||||
.with_jwt_cookie(JWT_COOKIE_NAME)
|
||||
.with_refresh_header(REFRESH_HEADER_NAME)
|
||||
// Check if cookie JWT exists and contains encoded JWT
|
||||
.with_refresh_cookie(REFRESH_COOKIE_NAME)
|
||||
.with_jwt_json(&["access_token"])
|
||||
.finish();
|
||||
let $db = Data::new($db.clone());
|
||||
ensure_instance($db.clone()).await;
|
||||
let $app = test::init_service(
|
||||
App::new()
|
||||
.app_data(Data::new($session_storage.clone()))
|
||||
.app_data($db.clone())
|
||||
.app_data(Data::new(redis))
|
||||
.wrap(actix_web::middleware::NormalizePath::trim())
|
||||
.wrap(actix_web::middleware::Logger::default())
|
||||
.wrap(factory)
|
||||
.service(change_password),
|
||||
)
|
||||
.await;
|
||||
};
|
||||
}
|
||||
|
||||
async fn ensure_instance(db: Data<DatabaseConnection>) {
|
||||
use entities::instances::*;
|
||||
use entities::prelude::Instances;
|
||||
use sea_orm::*;
|
||||
|
||||
if Instances::find().count(&**db).await.unwrap() > 0 {
|
||||
return;
|
||||
}
|
||||
ActiveModel {
|
||||
instance_name: Set("Plan Free".into()),
|
||||
is_setup_done: Set(true),
|
||||
..Default::default()
|
||||
}
|
||||
.save(&**db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
db: Data<DatabaseConnection>,
|
||||
user_name: &str,
|
||||
pass: &str,
|
||||
) -> entities::users::Model {
|
||||
use entities::prelude::Users;
|
||||
use entities::users::*;
|
||||
use sea_orm::*;
|
||||
|
||||
if let Ok(Some(user)) = Users::find()
|
||||
.filter(Column::Email.eq(format!("{user_name}@example.com")))
|
||||
.one(&**db)
|
||||
.await
|
||||
{
|
||||
return user;
|
||||
}
|
||||
|
||||
let pass = Hashing::encrypt(pass).unwrap();
|
||||
|
||||
Users::insert(ActiveModel {
|
||||
password: Set(pass),
|
||||
email: Set(Some(format!("{user_name}@example.com"))),
|
||||
display_name: Set(user_name.to_string()),
|
||||
username: Set(Uuid::new_v4().to_string()),
|
||||
first_name: Set("".to_string()),
|
||||
last_name: Set("".to_string()),
|
||||
last_location: Set("".to_string()),
|
||||
created_location: Set("".to_string()),
|
||||
is_password_autoset: Set(false),
|
||||
token: Set(Uuid::new_v4().to_string()),
|
||||
billing_address_country: Set("".to_string()),
|
||||
user_timezone: Set("UTC".to_string()),
|
||||
last_login_ip: Set("0.0.0.0".to_string()),
|
||||
last_login_medium: Set("None".to_string()),
|
||||
last_logout_ip: Set("0.0.0.0".to_string()),
|
||||
last_login_uagent: Set("test".to_string()),
|
||||
is_active: Set(true),
|
||||
avatar: Set("".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.exec_with_returning(&**db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[traced_test]
|
||||
#[actix_web::test]
|
||||
async fn missing_json() {
|
||||
create_app!(app, session, db);
|
||||
let req = test::TestRequest::default()
|
||||
.insert_header(ContentType::json())
|
||||
.to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert!(resp.status().is_client_error());
|
||||
}
|
||||
|
||||
#[traced_test]
|
||||
#[actix_web::test]
|
||||
async fn missing_user() {
|
||||
create_app!(app, session, db);
|
||||
let pair = session
|
||||
.store(
|
||||
AppClaims {
|
||||
expiration_time: (chrono::Utc::now() + chrono::Duration::days(100))
|
||||
.timestamp_millis(),
|
||||
issued_at: 0,
|
||||
subject: "999999999".into(),
|
||||
audience: session::Audience::Web,
|
||||
jwt_id: Uuid::new_v4(),
|
||||
account_id: Uuid::new_v4(),
|
||||
not_before: 0,
|
||||
},
|
||||
JwtTtl::new(actix_jwt_session::Duration::days(9999)),
|
||||
RefreshTtl::new(actix_jwt_session::Duration::days(9999)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let req = test::TestRequest::default()
|
||||
// .insert_header(ContentType::json())
|
||||
.insert_header((JWT_HEADER_NAME, pair.jwt.encode().unwrap()))
|
||||
.set_json(Input {
|
||||
new_password: "asd".into(),
|
||||
old_password: "asdasd".into(),
|
||||
confirm_password: "asd".into(),
|
||||
})
|
||||
.uri("/users/me/change-password")
|
||||
.method(Method::POST)
|
||||
.to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
let body = resp.into_body();
|
||||
let bytes = String::from_utf8(to_bytes(body).await.unwrap()[..].to_vec()).unwrap();
|
||||
let expected = serde_json::to_string(&json!({ "error": "User not found" })).unwrap();
|
||||
assert_eq!(bytes, expected);
|
||||
}
|
||||
|
||||
#[traced_test]
|
||||
#[actix_web::test]
|
||||
async fn old_password_is_incorrect() {
|
||||
create_app!(app, session, db);
|
||||
let user = create_user(db.clone(), "old_pass_incorrect", "qwertyQWERTY12345!@#$%").await;
|
||||
|
||||
let pair = session
|
||||
.store(
|
||||
AppClaims {
|
||||
expiration_time: (chrono::Utc::now() + chrono::Duration::days(100))
|
||||
.timestamp_millis(),
|
||||
issued_at: 0,
|
||||
subject: "999999999".into(),
|
||||
audience: session::Audience::Web,
|
||||
jwt_id: Uuid::new_v4(),
|
||||
account_id: user.id,
|
||||
not_before: 0,
|
||||
},
|
||||
JwtTtl::new(actix_jwt_session::Duration::days(9999)),
|
||||
RefreshTtl::new(actix_jwt_session::Duration::days(9999)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let req = test::TestRequest::default()
|
||||
// .insert_header(ContentType::json())
|
||||
.insert_header((JWT_HEADER_NAME, pair.jwt.encode().unwrap()))
|
||||
.set_json(Input {
|
||||
new_password: "asd".into(),
|
||||
old_password: "asdasd".into(),
|
||||
confirm_password: "asd".into(),
|
||||
})
|
||||
.uri("/users/me/change-password")
|
||||
.method(Method::POST)
|
||||
.to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
let body = resp.into_body();
|
||||
let bytes: JsonErrorDetails<PassValidity> =
|
||||
serde_json::from_slice(&to_bytes(body).await.unwrap()[..]).unwrap();
|
||||
let expected = serde_json::from_value(json!({
|
||||
"error": "New password is not correct",
|
||||
"errors": {
|
||||
"has_lower": true,
|
||||
"has_upper": false,
|
||||
"has_num": false,
|
||||
"has_special": false,
|
||||
"too_short": true,
|
||||
"too_long": false
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(bytes, expected);
|
||||
}
|
||||
|
||||
#[traced_test]
|
||||
#[actix_web::test]
|
||||
async fn use_old() {
|
||||
create_app!(app, session, db);
|
||||
|
||||
const OLD_PASS: &str = "aA1!ahidhasuiduah872364";
|
||||
const NEW_PASS: &str = "aA1!ahidhasuiduah872364";
|
||||
|
||||
let user = create_user(db.clone(), "use_old_change_pass", OLD_PASS).await;
|
||||
|
||||
let pair = session
|
||||
.store(
|
||||
AppClaims {
|
||||
expiration_time: (chrono::Utc::now() + chrono::Duration::days(100))
|
||||
.timestamp_millis(),
|
||||
issued_at: 0,
|
||||
subject: "999999999".into(),
|
||||
audience: session::Audience::Web,
|
||||
jwt_id: Uuid::new_v4(),
|
||||
account_id: user.id,
|
||||
not_before: 0,
|
||||
},
|
||||
JwtTtl::new(actix_jwt_session::Duration::days(9999)),
|
||||
RefreshTtl::new(actix_jwt_session::Duration::days(9999)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let req = test::TestRequest::default()
|
||||
// .insert_header(ContentType::json())
|
||||
.insert_header((JWT_HEADER_NAME, pair.jwt.encode().unwrap()))
|
||||
.set_json(Input {
|
||||
old_password: OLD_PASS.into(),
|
||||
new_password: NEW_PASS.into(),
|
||||
confirm_password: NEW_PASS.into(),
|
||||
})
|
||||
.uri("/users/me/change-password")
|
||||
.method(Method::POST)
|
||||
.to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
let body = resp.into_body();
|
||||
let json: serde_json::Value =
|
||||
serde_json::from_slice(&to_bytes(body).await.unwrap()[..]).unwrap();
|
||||
assert_eq!(json, serde_json::json!({ "error": "New password cannot be same as old password." }));
|
||||
}
|
||||
|
||||
#[traced_test]
|
||||
#[actix_web::test]
|
||||
async fn valid() {
|
||||
create_app!(app, session, db);
|
||||
|
||||
const OLD_PASS: &str = "qwertyQWERTY12345!@#$%";
|
||||
const NEW_PASS: &str = "aA1!ahidhasuiduah872364";
|
||||
|
||||
let user = create_user(db.clone(), "valid_change_pass", OLD_PASS).await;
|
||||
|
||||
let pair = session
|
||||
.store(
|
||||
AppClaims {
|
||||
expiration_time: (chrono::Utc::now() + chrono::Duration::days(100))
|
||||
.timestamp_millis(),
|
||||
issued_at: 0,
|
||||
subject: "999999999".into(),
|
||||
audience: session::Audience::Web,
|
||||
jwt_id: Uuid::new_v4(),
|
||||
account_id: user.id,
|
||||
not_before: 0,
|
||||
},
|
||||
JwtTtl::new(actix_jwt_session::Duration::days(9999)),
|
||||
RefreshTtl::new(actix_jwt_session::Duration::days(9999)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let req = test::TestRequest::default()
|
||||
// .insert_header(ContentType::json())
|
||||
.insert_header((JWT_HEADER_NAME, pair.jwt.encode().unwrap()))
|
||||
.set_json(Input {
|
||||
old_password: OLD_PASS.into(),
|
||||
new_password: NEW_PASS.into(),
|
||||
confirm_password: NEW_PASS.into(),
|
||||
})
|
||||
.uri("/users/me/change-password")
|
||||
.method(Method::POST)
|
||||
.to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let body = resp.into_body();
|
||||
let json: serde_json::Value =
|
||||
serde_json::from_slice(&to_bytes(body).await.unwrap()[..]).unwrap();
|
||||
assert_eq!(json, serde_json::json!({ "message": "Password updated successfully" }));
|
||||
}
|
||||
}
|
@ -1,21 +1,21 @@
|
||||
use super::{AuthError, PublishError};
|
||||
use actix_web::http::header::USER_AGENT;
|
||||
use actix_web::web::{Data, Json};
|
||||
use actix_web::{post, HttpRequest, HttpResponse};
|
||||
use entities::prelude::{Users, WorkspaceMemberInvites};
|
||||
use entities::prelude::WorkspaceMemberInvites;
|
||||
use entities::users::Model as User;
|
||||
use jet_contract::event_bus::SignInMedium;
|
||||
use sea_orm::{prelude::*, DatabaseTransaction};
|
||||
use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter};
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{DatabaseConnection, DatabaseTransaction, EntityTrait, QueryFilter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_email::Email;
|
||||
|
||||
use super::{AuthError, PublishError};
|
||||
use crate::config::ApplicationConfig;
|
||||
use crate::extractors::RequireInstanceConfigured;
|
||||
use crate::http::magic_link::create_magic_link;
|
||||
use crate::models::*;
|
||||
use crate::utils::user_by_email;
|
||||
use crate::{db_commit, db_rollback, db_t, models::*};
|
||||
use crate::{EventBusClient, RedisClient};
|
||||
use crate::{db_commit, db_rollback, db_t, EventBusClient, RedisClient};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct EmailCheckPayload {
|
||||
@ -138,7 +138,8 @@ async fn handle_existing_user(
|
||||
}))
|
||||
}
|
||||
|
||||
/// If there's no user for given email address we will register new one and send magic link sign-in
|
||||
/// If there's no user for given email address we will register new one and send
|
||||
/// magic link sign-in
|
||||
async fn register(
|
||||
req: HttpRequest,
|
||||
payload: Json<EmailCheckPayload>,
|
||||
|
@ -1,20 +1,17 @@
|
||||
use actix_web::{
|
||||
post,
|
||||
web::{Data, Json},
|
||||
HttpRequest, HttpResponse,
|
||||
};
|
||||
use actix_web::web::{Data, Json};
|
||||
use actix_web::{post, HttpRequest, HttpResponse};
|
||||
use jet_contract::event_bus::{EmailMsg, Topic};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use sea_orm::{prelude::*, DatabaseTransaction};
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{DatabaseConnection, DatabaseTransaction};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use tracing::error;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use super::{create_user, random_password};
|
||||
use crate::{
|
||||
db_commit, db_rollback, db_t, extractors::RequireInstanceConfigured, models::JsonError,
|
||||
models::*, utils::extract_req_current_site, EventBusClient, RedisClient,
|
||||
};
|
||||
use crate::extractors::RequireInstanceConfigured;
|
||||
use crate::models::{JsonError, *};
|
||||
use crate::utils::extract_req_current_site;
|
||||
use crate::{db_commit, db_rollback, db_t, EventBusClient, RedisClient};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Input {
|
||||
@ -68,7 +65,7 @@ async fn try_create_magic_link(
|
||||
let (key, token, _validity) = super::magic_link::create_magic_link(&email, redis).await?;
|
||||
let current_site = extract_req_current_site(&req)?;
|
||||
|
||||
event_bus
|
||||
if let Err(e) = event_bus
|
||||
.publish(
|
||||
Topic::Email,
|
||||
jet_contract::event_bus::Msg::Email(EmailMsg::MagicLink {
|
||||
@ -80,7 +77,10 @@ async fn try_create_magic_link(
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
{
|
||||
warn!("Failed to send event to MQTT: {e}")
|
||||
};
|
||||
|
||||
Ok(HttpResponse::Ok().json(json!({ "key": key })))
|
||||
}
|
||||
|
@ -1,33 +1,19 @@
|
||||
use actix_jwt_session::{Duration, JwtTtl, RefreshTtl, SessionStorage};
|
||||
use actix_web::{
|
||||
post,
|
||||
web::{Data, Json},
|
||||
HttpRequest, HttpResponse,
|
||||
};
|
||||
|
||||
use jet_contract::{
|
||||
event_bus::{Msg, SignInMedium, Topic, UserMsg},
|
||||
redis::AsyncCommands,
|
||||
};
|
||||
use actix_jwt_session::SessionStorage;
|
||||
use actix_web::web::{Data, Json};
|
||||
use actix_web::{post, HttpRequest, HttpResponse};
|
||||
use jet_contract::event_bus::{Msg, SignInMedium, Topic, UserMsg};
|
||||
use jet_contract::redis::AsyncCommands;
|
||||
use reqwest::StatusCode;
|
||||
use rumqttc::QoS;
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::*;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
db_commit, db_rollback, db_t,
|
||||
extractors::RequireInstanceConfigured,
|
||||
models::{Error, JsonError},
|
||||
redis_c,
|
||||
session::AppClaims,
|
||||
utils::{extract_req_ip, extract_req_uagent, user_by_email},
|
||||
};
|
||||
use crate::{EventBusClient, RedisClient};
|
||||
|
||||
use super::{AuthResponseBody, auth_http_response};
|
||||
use super::auth_http_response;
|
||||
use crate::extractors::RequireInstanceConfigured;
|
||||
use crate::models::{Error, JsonError};
|
||||
use crate::utils::{extract_req_ip, extract_req_uagent, user_by_email};
|
||||
use crate::{db_commit, db_rollback, db_t, redis_c, EventBusClient, RedisClient};
|
||||
|
||||
#[post("/magic-sign-in")]
|
||||
async fn magic_sign_in(
|
||||
|
112
crates/jet-api/src/http/api/authentication/reset_password.rs
Normal file
112
crates/jet-api/src/http/api/authentication/reset_password.rs
Normal file
@ -0,0 +1,112 @@
|
||||
use actix_jwt_session::SessionStorage;
|
||||
use actix_web::web::{Data, Json, Path};
|
||||
use actix_web::{post, HttpRequest, HttpResponse};
|
||||
use entities::prelude::Users;
|
||||
use entities::users::ActiveModel as UserModel;
|
||||
use reqwest::StatusCode;
|
||||
use sea_orm::{DatabaseConnection, DatabaseTransaction, EntityTrait, Set};
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::auth_http_response;
|
||||
use super::password::PassValidity;
|
||||
use crate::extractors::RequireInstanceConfigured;
|
||||
use crate::models::{
|
||||
Error, JsonError, JsonErrorDetails, PasswordResetSecret, PasswordResetTimeout,
|
||||
};
|
||||
use crate::utils::SetPassword;
|
||||
use crate::{db_commit, db_rollback, db_t};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Input {
|
||||
new_password: String,
|
||||
}
|
||||
|
||||
#[post("/reset-password/{uidb}/{token}/")]
|
||||
pub async fn reset_password(
|
||||
_: RequireInstanceConfigured,
|
||||
req: HttpRequest,
|
||||
path: Path<(String, String)>,
|
||||
payload: Json<Input>,
|
||||
db: Data<DatabaseConnection>,
|
||||
session: Data<SessionStorage>,
|
||||
pass_reset_secret: Data<PasswordResetSecret>,
|
||||
pass_reset_timeout: Data<PasswordResetTimeout>,
|
||||
) -> Result<HttpResponse, JsonErrorDetails<super::password::PassValidity>> {
|
||||
let mut t = db_t!(db)?;
|
||||
match try_reset_password(
|
||||
req,
|
||||
path,
|
||||
payload,
|
||||
&mut t,
|
||||
session,
|
||||
pass_reset_secret,
|
||||
pass_reset_timeout,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => {
|
||||
db_commit!(t)?;
|
||||
Ok(r)
|
||||
}
|
||||
Err(e) => {
|
||||
db_rollback!(t).ok();
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_reset_password(
|
||||
_req: HttpRequest,
|
||||
path: Path<(String, String)>,
|
||||
payload: Json<Input>,
|
||||
t: &mut DatabaseTransaction,
|
||||
session: Data<SessionStorage>,
|
||||
pass_reset_secret: Data<PasswordResetSecret>,
|
||||
pass_reset_timeout: Data<PasswordResetTimeout>,
|
||||
) -> Result<HttpResponse, JsonErrorDetails<super::password::PassValidity>> {
|
||||
let (uidb, token) = path.into_inner();
|
||||
let Ok(user_id) = crate::utils::uidb::decode(&uidb) else {
|
||||
return Err(JsonError::new("Token is invalid").with_status(StatusCode::UNAUTHORIZED))?;
|
||||
};
|
||||
let user = Users::find_by_id(user_id)
|
||||
.one(&mut *t)
|
||||
.await
|
||||
.map_err(|_e| Error::DatabaseError)?
|
||||
.ok_or(Error::UserRequired)?;
|
||||
|
||||
if !crate::utils::pass_reset_token::check(
|
||||
&user,
|
||||
&token,
|
||||
pass_reset_secret.as_str(),
|
||||
***pass_reset_timeout,
|
||||
) {
|
||||
return Err(JsonError::new("Token is invalid"))?;
|
||||
}
|
||||
|
||||
let payload = payload.into_inner();
|
||||
|
||||
match super::password::validate(&payload.new_password) {
|
||||
PassValidity::Valid => {}
|
||||
v => {
|
||||
return Err(JsonError::new("Password is innvalid")
|
||||
.with_status(StatusCode::BAD_REQUEST)
|
||||
.with_details(v))
|
||||
}
|
||||
}
|
||||
|
||||
let mut model: UserModel = user.clone().into();
|
||||
model.set_password(&payload.new_password).map_err(|e| {
|
||||
tracing::error!("Failed to encrypt password: {e}");
|
||||
Error::DatabaseError
|
||||
})?;
|
||||
model.is_password_autoset = Set(false);
|
||||
|
||||
Users::update(model).exec(&mut *t).await.map_err(|e| {
|
||||
tracing::error!("Failed to update user: {e}");
|
||||
Error::DatabaseError
|
||||
})?;
|
||||
|
||||
auth_http_response(user, session, StatusCode::OK)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
use actix_jwt_session::SessionStorage;
|
||||
use actix_web::web::{Data, Json};
|
||||
use actix_web::ResponseError;
|
||||
use actix_web::{post, HttpRequest, HttpResponse};
|
||||
use entities::prelude::Users;
|
||||
use entities::users::ActiveModel as UserModel;
|
||||
@ -11,14 +10,13 @@ use rumqttc::QoS;
|
||||
use sea_orm::*;
|
||||
use validators::prelude::*;
|
||||
|
||||
use super::EmailAllowComment;
|
||||
use crate::config::ApplicationConfig;
|
||||
use crate::extractors::RequireInstanceConfigured;
|
||||
use crate::http::api::authentication::{auth_http_response, create_user, has_workspace_invites};
|
||||
use crate::models::{Error, JsonError};
|
||||
use crate::utils::user_by_email;
|
||||
|
||||
use super::EmailAllowComment;
|
||||
|
||||
#[post("/sign-in")]
|
||||
pub async fn sign_in(
|
||||
_: RequireInstanceConfigured,
|
||||
|
@ -1,6 +1,6 @@
|
||||
use actix_jwt_session::{Authenticated, RefreshToken, SessionStorage};
|
||||
use actix_web::HttpRequest;
|
||||
use actix_web::{post, web::Data, HttpResponse};
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{post, HttpRequest, HttpResponse};
|
||||
use entities::prelude::Users;
|
||||
use entities::users::{ActiveModel as UserModel, Column};
|
||||
use sea_orm::prelude::*;
|
||||
|
@ -1,25 +1,22 @@
|
||||
use actix_jwt_session::SessionStorage;
|
||||
use actix_web::web::{Data, Json};
|
||||
use actix_web::ResponseError;
|
||||
use actix_web::{post, HttpRequest, HttpResponse};
|
||||
use actix_web::{post, HttpRequest, HttpResponse, ResponseError};
|
||||
use entities::prelude::Users;
|
||||
use entities::users::ActiveModel as UserModel;
|
||||
use jet_contract::event_bus::{Msg, SignInMedium, Topic, UserMsg};
|
||||
use jet_contract::UserId;
|
||||
use reqwest::StatusCode;
|
||||
use rumqttc::QoS;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use sea_orm::*;
|
||||
use sea_orm::{DatabaseConnection, *};
|
||||
use validators::prelude::*;
|
||||
|
||||
use super::EmailAllowComment;
|
||||
use crate::config::ApplicationConfig;
|
||||
use crate::extractors::RequireInstanceConfigured;
|
||||
use crate::http::api::authentication::{auth_http_response, create_user, has_workspace_invites};
|
||||
use crate::models::{Error, JsonError};
|
||||
use crate::utils::{extract_req_ip, extract_req_uagent};
|
||||
|
||||
use super::EmailAllowComment;
|
||||
|
||||
#[post("/sign-up")]
|
||||
pub async fn sign_up(
|
||||
_: RequireInstanceConfigured,
|
||||
|
@ -1,19 +1,11 @@
|
||||
use std::env::var as env_var;
|
||||
|
||||
use super::{auth_http_response, AuthError};
|
||||
|
||||
use actix_web::{
|
||||
get,
|
||||
web::{self, Data, ServiceConfig},
|
||||
HttpRequest, HttpResponse,
|
||||
};
|
||||
use entities::prelude::Users;
|
||||
use entities::users::{ActiveModel as UserModel, Column as UserColumn};
|
||||
use actix_web::web::{self, Data, ServiceConfig};
|
||||
use actix_web::{get, HttpRequest, HttpResponse};
|
||||
use entities::users::ActiveModel as UserModel;
|
||||
use http_api_isahc_client::IsahcClient;
|
||||
use jet_contract::{
|
||||
event_bus::{Msg, SignInMedium, Topic, UserMsg},
|
||||
UserId,
|
||||
};
|
||||
use jet_contract::event_bus::{Msg, SignInMedium, Topic, UserMsg};
|
||||
use jet_contract::UserId;
|
||||
use oauth2_amazon::{
|
||||
AmazonExtensionsBuilder, AmazonProviderWithWebServices, AmazonScope, AmazonTokenUrlRegion,
|
||||
};
|
||||
@ -27,20 +19,18 @@ use oauth2_google::{
|
||||
GoogleProviderForWebServerAppsAccessType, GoogleScope,
|
||||
};
|
||||
use oauth2_signin::web_app::{SigninFlow, SigninFlowHandleCallbackByQueryConfiguration};
|
||||
use reqwest::{header::LOCATION, StatusCode};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue::NotSet, ColumnTrait, DatabaseConnection, DatabaseTransaction,
|
||||
EntityTrait, QueryFilter, Set, TransactionTrait,
|
||||
};
|
||||
use reqwest::header::LOCATION;
|
||||
use reqwest::StatusCode;
|
||||
use sea_orm::ActiveValue::NotSet;
|
||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, DatabaseTransaction, Set};
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
use crate::{
|
||||
db_commit, db_rollback, db_t,
|
||||
extractors::RequireInstanceConfigured,
|
||||
http::OAuthError,
|
||||
models::{Error, JsonError},
|
||||
utils::user_by_email,
|
||||
};
|
||||
use super::{auth_http_response, AuthError};
|
||||
use crate::extractors::RequireInstanceConfigured;
|
||||
use crate::http::OAuthError;
|
||||
use crate::models::{Error, JsonError};
|
||||
use crate::utils::user_by_email;
|
||||
use crate::{db_commit, db_rollback, db_t};
|
||||
|
||||
macro_rules! oauth_envs {
|
||||
($env: expr, 2) => {{
|
||||
|
34
crates/jet-api/src/http/api/authentication/token_refresh.rs
Normal file
34
crates/jet-api/src/http/api/authentication/token_refresh.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use actix_jwt_session::{Authenticated, RefreshToken, SessionStorage};
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{post, HttpResponse};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::extractors::RequireInstanceConfigured;
|
||||
use crate::models::JsonError;
|
||||
use crate::session::AppClaims;
|
||||
|
||||
#[post("/token/refresh")]
|
||||
pub async fn refresh_token(
|
||||
_: RequireInstanceConfigured,
|
||||
token: Authenticated<RefreshToken>,
|
||||
session: Data<SessionStorage>,
|
||||
) -> Result<HttpResponse, JsonError> {
|
||||
let pair = session
|
||||
.refresh::<AppClaims>(token.access_jti())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!("Failed to refresh token: {e}");
|
||||
JsonError::new("Unable to refresh token")
|
||||
})?;
|
||||
let access_token = pair.jwt.encode().map_err(|e| {
|
||||
tracing::error!("Unable to encode jwt: {e}");
|
||||
JsonError::new("Invalid access token")
|
||||
})?;
|
||||
let refresh_token = pair.refresh.encode().map_err(|e| {
|
||||
tracing::error!("Unable to encode refresh token: {e}");
|
||||
JsonError::new("Invalid access token")
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.json(json!({ "access_token": access_token, "refresh_token": refresh_token })))
|
||||
}
|
@ -1,13 +1,11 @@
|
||||
use actix_web::{
|
||||
get,
|
||||
web::{Data, ServiceConfig},
|
||||
HttpResponse,
|
||||
};
|
||||
use actix_web::web::{Data, ServiceConfig};
|
||||
use actix_web::{get, HttpResponse};
|
||||
|
||||
use crate::config::ApplicationConfig;
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
// config.service(actix_web::web::resource("").guard(actix_web::guard::fn_guard(f)))
|
||||
// config.service(actix_web::web::resource("").
|
||||
// guard(actix_web::guard::fn_guard(f)))
|
||||
config.service(configs);
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,20 @@
|
||||
use crate::extractors::RequireInstanceConfigured;
|
||||
use crate::session::AppClaims;
|
||||
use actix_jwt_session::Authenticated;
|
||||
use actix_web::get;
|
||||
use actix_web::web::{scope, Data, ServiceConfig};
|
||||
use actix_web::HttpResponse;
|
||||
use actix_jwt_session::{Authenticated, Hashing};
|
||||
use actix_web::web::{scope, Data, Json, ServiceConfig};
|
||||
use actix_web::{get, post, HttpResponse};
|
||||
use entities::prelude::Users;
|
||||
use entities::users::ActiveModel as UserModel;
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::EntityTrait;
|
||||
use sea_orm::*;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::extractors::RequireInstanceConfigured;
|
||||
use crate::models::JsonError;
|
||||
use crate::session::AppClaims;
|
||||
use crate::{db_commit, db_rollback, db_t};
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config.service(scope("/users").service(retrieve));
|
||||
config.service(scope("/users").service(retrieve).service(change_password));
|
||||
}
|
||||
|
||||
#[get("/me")]
|
||||
@ -23,3 +28,53 @@ async fn retrieve(
|
||||
Err(_error) => HttpResponse::BadRequest().finish(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ChangePassInput {
|
||||
new_password: String,
|
||||
}
|
||||
|
||||
#[post("/me/change-password")]
|
||||
async fn change_password(
|
||||
_: RequireInstanceConfigured,
|
||||
payload: Json<ChangePassInput>,
|
||||
session: Authenticated<AppClaims>,
|
||||
db: Data<DatabaseConnection>,
|
||||
) -> Result<HttpResponse, JsonError> {
|
||||
let mut t = db_t!(db)?;
|
||||
match try_change_password(payload, session, &mut t).await {
|
||||
Ok(r) => {
|
||||
db_commit!(t)?;
|
||||
Ok(r)
|
||||
}
|
||||
Err(e) => {
|
||||
db_rollback!(t).ok();
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
async fn try_change_password(
|
||||
payload: Json<ChangePassInput>,
|
||||
session: Authenticated<AppClaims>,
|
||||
t: &mut DatabaseTransaction,
|
||||
) -> Result<HttpResponse, JsonError> {
|
||||
let mut user: UserModel = match Users::find_by_id(session.account_id()).one(&mut *t).await {
|
||||
Ok(Some(user)) => user,
|
||||
Ok(None) => return Err(JsonError::new("User not found")),
|
||||
Err(_error) => {
|
||||
return Err(JsonError::new(
|
||||
"Unable to change password. Please contact administrator",
|
||||
))
|
||||
}
|
||||
}
|
||||
.into();
|
||||
|
||||
user.is_password_autoset = Set(false);
|
||||
let hash = Hashing::encrypt(payload.new_password.as_str()).map_err(|e| {
|
||||
tracing::error!("Hashing password failed: {e}");
|
||||
JsonError::new("Internal error while saving password")
|
||||
})?;
|
||||
user.password = Set(hash);
|
||||
|
||||
Ok(HttpResponse::Ok().json(json!({ "message": "Password updated successfully" })))
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
use actix_web::get;
|
||||
use actix_web::web::{Path, ServiceConfig};
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::{get, HttpResponse};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
use actix_web::get;
|
||||
use actix_web::web::{Path, ServiceConfig};
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::{get, HttpResponse};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
|
@ -1,10 +1,11 @@
|
||||
use std::env;
|
||||
|
||||
use actix_jwt_session::*;
|
||||
use actix_web::middleware::NormalizePath;
|
||||
use actix_web::{web::Data, App, HttpServer};
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{App, HttpServer};
|
||||
pub use jet_contract::event_bus::Client as EventBusClient;
|
||||
pub use jet_contract::{deadpool_redis, redis, RedisClient};
|
||||
use models::PasswordResetSecret;
|
||||
pub use sea_orm::{Database, DatabaseConnection};
|
||||
|
||||
pub mod config;
|
||||
@ -81,6 +82,12 @@ async fn main() {
|
||||
let http_client = reqwest::Client::new();
|
||||
let application_config = Data::new(application_config);
|
||||
let event_bus = Data::new(jet_contract::event_bus::Client::new(eb_client));
|
||||
let pass_reset_secret = Data::new(PasswordResetSecret::new(
|
||||
env::var("PASSWORD_RESET_SECRET").expect("PASSWORD_RESET_SECRET must be provided"),
|
||||
));
|
||||
let pass_reset_timeout = Data::new(PasswordResetSecret::new(
|
||||
env::var("PASSWORD_RESET_TIMEOUT").expect("PASSWORD_RESET_TIMEOUT must be provided"),
|
||||
));
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
@ -91,9 +98,11 @@ async fn main() {
|
||||
.app_data(Data::new(db.clone()))
|
||||
.app_data(application_config.clone())
|
||||
.app_data(event_bus.clone())
|
||||
.wrap(NormalizePath::trim())
|
||||
.wrap(factory.clone())
|
||||
.app_data(pass_reset_secret.clone())
|
||||
.app_data(pass_reset_timeout.clone())
|
||||
.wrap(actix_web::middleware::NormalizePath::trim())
|
||||
.wrap(actix_web::middleware::Logger::default())
|
||||
.wrap(factory.clone())
|
||||
.configure(|config| http::configure(http_client.clone(), config))
|
||||
})
|
||||
.bind(addr)
|
||||
|
@ -1,26 +1,67 @@
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
use actix_web::HttpResponse;
|
||||
use derive_more::Display;
|
||||
use derive_more::{Constructor, Deref};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::http::AuthError;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Display)]
|
||||
#[display(fmt = "{{\"error\":\"{error}\"}}")]
|
||||
#[derive(Debug, Deref, Constructor)]
|
||||
pub struct PasswordResetSecret(String);
|
||||
|
||||
#[derive(Debug, Deref)]
|
||||
pub struct PasswordResetTimeout(chrono::Duration);
|
||||
|
||||
impl PasswordResetTimeout {
|
||||
pub fn new(s: &str) -> Self {
|
||||
use std::time::Duration;
|
||||
let d: Duration = s
|
||||
.parse::<humantime::Duration>()
|
||||
.unwrap_or_else(|e| panic!("Invalid duration for password reset timeout: {e}"))
|
||||
.into();
|
||||
Self(chrono::Duration::from_std(d).expect("Invalid duration for password reset timeout"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct JsonError {
|
||||
pub error: String,
|
||||
#[serde(skip)]
|
||||
pub status: StatusCode,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for JsonError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&serde_json::to_string(self).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonError {
|
||||
pub fn new(error: impl std::fmt::Display) -> Self {
|
||||
Self {
|
||||
error: format!("{error}"),
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_status(mut self, status: StatusCode) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_details<D: Serialize + std::fmt::Debug>(self, d: D) -> JsonErrorDetails<D> {
|
||||
JsonErrorDetails {
|
||||
status: self.status,
|
||||
error: self.error,
|
||||
details: Some(d),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl actix_web::ResponseError for JsonError {
|
||||
fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> {
|
||||
HttpResponse::BadRequest().json(self)
|
||||
HttpResponse::build(self.status).json(self)
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,7 +71,69 @@ impl From<Error> for JsonError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, derive_more::Display)]
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct JsonErrorDetails<Details>
|
||||
where
|
||||
Details: Serialize + Debug,
|
||||
{
|
||||
pub error: String,
|
||||
#[serde(skip)]
|
||||
pub status: StatusCode,
|
||||
#[serde(flatten)]
|
||||
pub details: Option<Details>,
|
||||
}
|
||||
|
||||
impl<D: Serialize + Debug> Display for JsonErrorDetails<D> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&serde_json::to_string(self).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Serialize + Debug> JsonErrorDetails<D> {
|
||||
pub fn new(error: impl std::fmt::Display) -> Self {
|
||||
Self {
|
||||
error: format!("{error}"),
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
details: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_status(mut self, status: StatusCode) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_details(mut self, d: D) -> Self {
|
||||
self.details = Some(d);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Serialize + Debug> actix_web::ResponseError for JsonErrorDetails<D> {
|
||||
fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> {
|
||||
HttpResponse::build(self.status).json(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Serialize + Debug> From<Error> for JsonErrorDetails<D> {
|
||||
fn from(e: Error) -> Self {
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Serialize + Debug> From<JsonError> for JsonErrorDetails<D> {
|
||||
fn from(value: JsonError) -> Self {
|
||||
let JsonError { error, status } = value;
|
||||
Self {
|
||||
error,
|
||||
status,
|
||||
details: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, derive_more::Display)]
|
||||
#[cfg_attr(test, derive(serde::Deserialize))]
|
||||
pub enum Error {
|
||||
#[display(fmt = "Database connection error")]
|
||||
DatabaseError,
|
||||
@ -52,6 +155,10 @@ pub enum Error {
|
||||
ContactSupport,
|
||||
#[display(fmt = "User not found")]
|
||||
UserRequired,
|
||||
#[display(
|
||||
fmt = "Password couldn't be encrypted. Please try again later or contact the support team."
|
||||
)]
|
||||
EncryptPass,
|
||||
}
|
||||
|
||||
impl From<AuthError> for Error {
|
||||
@ -66,6 +173,7 @@ impl actix_web::ResponseError for Error {
|
||||
Self::DatabaseError | Self::NoHost => {
|
||||
HttpResponse::InternalServerError().json(JsonError::new(self))
|
||||
}
|
||||
Self::UserRequired => HttpResponse::Unauthorized().json(JsonError::new(self)),
|
||||
_ => HttpResponse::BadRequest().json(JsonError::new(self)),
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,26 @@
|
||||
use actix_web::web::Data;
|
||||
use actix_jwt_session::Hashing;
|
||||
use actix_web::HttpRequest;
|
||||
use chrono::Utc;
|
||||
use entities::prelude::{
|
||||
ProjectMemberInvites, ProjectMembers, WorkspaceMemberInvites, WorkspaceMembers,
|
||||
};
|
||||
use entities::project_member_invites::{
|
||||
Column as ProjectMemberInviteColumn, Model as ProjectMemberInvite,
|
||||
};
|
||||
use entities::project_members::ActiveModel as ProjectMemberModel;
|
||||
use entities::sea_orm_active_enums::{ProjectMemberRoles, Roles};
|
||||
use entities::workspace_member_invites::{
|
||||
Column as WorkspaceMemberInviteColumn, Model as WorkspaceMemberInvite,
|
||||
};
|
||||
use entities::workspace_members::ActiveModel as WorkspaceMemberModel;
|
||||
use entities::{
|
||||
prelude::{ProjectMemberInvites, ProjectMembers, WorkspaceMemberInvites, WorkspaceMembers},
|
||||
sea_orm_active_enums::Roles,
|
||||
};
|
||||
use entities::{
|
||||
project_members::ActiveModel as ProjectMemberModel, sea_orm_active_enums::ProjectMemberRoles,
|
||||
};
|
||||
use reqwest::header::USER_AGENT;
|
||||
use sea_orm::sea_query::OnConflict;
|
||||
use sea_orm::*;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::http::OAuthError;
|
||||
use crate::{http::AuthError, models::Error};
|
||||
use crate::http::{AuthError, OAuthError};
|
||||
use crate::models::Error;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! db_t {
|
||||
@ -38,13 +36,23 @@ macro_rules! db_t {
|
||||
#[macro_export]
|
||||
macro_rules! db_commit {
|
||||
($db: expr) => {{
|
||||
$db.commit().await.map_err(|e| {
|
||||
let res = if cfg!(test) {
|
||||
$db.rollback().await
|
||||
} else {
|
||||
$db.commit().await
|
||||
};
|
||||
res.map_err(|e| {
|
||||
tracing::error!("Failed to commit db tracation: {e}");
|
||||
crate::models::Error::DatabaseError
|
||||
})
|
||||
}};
|
||||
($db: expr, $msg: expr) => {{
|
||||
$db.commit().await.map_err(|e| {
|
||||
let res = if cfg!(test) {
|
||||
$db.rollback().await
|
||||
} else {
|
||||
$db.commit().await
|
||||
};
|
||||
res.map_err(|e| {
|
||||
tracing::error!(std::concat!($msg, ": {}"), e);
|
||||
crate::models::Error::DatabaseError
|
||||
})
|
||||
@ -75,6 +83,25 @@ macro_rules! redis_c {
|
||||
};
|
||||
}
|
||||
|
||||
pub trait SetPassword {
|
||||
fn set_password(&mut self, password: &str) -> Result<(), password_hash::Error>;
|
||||
}
|
||||
|
||||
impl SetPassword for entities::users::ActiveModel {
|
||||
fn set_password(&mut self, password: &str) -> Result<(), password_hash::Error> {
|
||||
self.password = Set(encrypt_password(password)?);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt_password(pass: &str) -> Result<String, password_hash::Error> {
|
||||
Hashing::encrypt(pass)
|
||||
}
|
||||
|
||||
pub fn verify_password(password_hash: &str, password: &str) -> Result<(), password_hash::Error> {
|
||||
Hashing::verify(password_hash, password)
|
||||
}
|
||||
|
||||
pub async fn user_by_email(
|
||||
email: &str,
|
||||
db: &mut DatabaseTransaction,
|
||||
@ -265,3 +292,75 @@ pub async fn invites_to_membership(
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub mod uidb {
|
||||
use base64::prelude::*;
|
||||
use reqwest::StatusCode;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::JsonError;
|
||||
|
||||
pub fn decode(uidb: &str) -> Result<Uuid, JsonError> {
|
||||
let Ok(bytes) = BASE64_URL_SAFE.decode(uidb) else {
|
||||
return Err(JsonError::new("Token is invalid").with_status(StatusCode::UNAUTHORIZED));
|
||||
};
|
||||
Uuid::from_slice(&bytes)
|
||||
.map_err(|_| JsonError::new("Token is invalid").with_status(StatusCode::UNAUTHORIZED))
|
||||
}
|
||||
|
||||
pub fn encode(uuid: Uuid) -> String {
|
||||
BASE64_URL_SAFE.encode(uuid.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
pub mod pass_reset_token {
|
||||
use chrono::{Duration, NaiveDateTime, Utc};
|
||||
use entities::users::Model as User;
|
||||
use hmac::*;
|
||||
use sha2::Sha256;
|
||||
use tracing::warn;
|
||||
|
||||
pub fn check(user: &User, token: &str, secret: &str, password_reset_timeout: Duration) -> bool {
|
||||
let Some((ts_b36, _)) = token.split_once('-') else {
|
||||
return false;
|
||||
};
|
||||
let Some(ts): Option<u64> = basen::BASE36.decode_var_len(&ts_b36) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let valid_token = make_token_with_timestamp(ts as i64, user, secret);
|
||||
if valid_token != token {
|
||||
return false;
|
||||
}
|
||||
let Some(timestamp) = NaiveDateTime::from_timestamp_millis(ts as i64) else {
|
||||
warn!("Invalid timestamp in token. Not a milliseconds");
|
||||
return false;
|
||||
};
|
||||
chrono::Utc::now().naive_utc() - timestamp < password_reset_timeout
|
||||
}
|
||||
|
||||
pub fn make_token(user: &User, secret: &str) -> String {
|
||||
make_token_with_timestamp(Utc::now().naive_utc().timestamp_millis(), user, secret)
|
||||
}
|
||||
|
||||
fn make_token_with_timestamp(timestamp: i64, user: &User, secret: &str) -> String {
|
||||
let ts_b36 = basen::BASE36.encode_var_len(&(timestamp as u64));
|
||||
let hash_value = user_to_hash_value(timestamp, user);
|
||||
let mut mac =
|
||||
Hmac::<Sha256>::new_from_slice(secret.as_bytes()).expect("Invalid hmac secret");
|
||||
mac.update(hash_value.as_bytes());
|
||||
let result = mac.finalize();
|
||||
let s = String::from_utf8(result.into_bytes()[..].to_vec()).unwrap();
|
||||
format!("{ts_b36}-{s}")
|
||||
}
|
||||
|
||||
fn user_to_hash_value(timestamp: i64, user: &User) -> String {
|
||||
format!(
|
||||
"{pk}{pass}{login_timestamp}{timestamp}{email}",
|
||||
pk = user.id,
|
||||
pass = user.password,
|
||||
login_timestamp = user.last_login_time.unwrap_or_default(),
|
||||
email = user.email.as_deref().unwrap_or_default()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,11 @@
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
use derive_more::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub use deadpool_redis;
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
pub use deadpool_redis::Pool as RedisClient;
|
||||
pub use redis;
|
||||
pub use rumqttc;
|
||||
pub use serde;
|
||||
|
||||
use derive_more::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
pub use {deadpool_redis, redis, rumqttc, serde};
|
||||
|
||||
pub type Id = Uuid;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
//! This library allows to create HTTP based micro-server which will serve as plugin for JET server
|
||||
//! This library allows to create HTTP based micro-server which will serve as
|
||||
//! plugin for JET server
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
|
7
rustfmt.toml
Normal file
7
rustfmt.toml
Normal file
@ -0,0 +1,7 @@
|
||||
imports_granularity = "Module"
|
||||
group_imports = "StdExternalCrate"
|
||||
reorder_modules = true
|
||||
reorder_imports = true
|
||||
use_field_init_shorthand = true
|
||||
wrap_comments = true
|
||||
edition = "2021"
|
Loading…
Reference in New Issue
Block a user