Test change pass

This commit is contained in:
eraden 2024-02-08 10:14:00 +01:00
parent ba4e6a377f
commit 09d164576f
34 changed files with 1314 additions and 242 deletions

217
Cargo.lock generated
View File

@ -4,9 +4,9 @@ version = 3
[[package]] [[package]]
name = "actix" name = "actix"
version = "0.13.1" version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cba56612922b907719d4a01cf11c8d5b458e7d3dba946d0435f20f58d6795ed2" checksum = "dc9ef49f64074352f73ef9ec8c060a5f5799c96715c986a17f10933c3da2955c"
dependencies = [ dependencies = [
"actix-macros", "actix-macros",
"actix-rt", "actix-rt",
@ -29,11 +29,11 @@ dependencies = [
[[package]] [[package]]
name = "actix-codec" name = "actix-codec"
version = "0.5.1" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 2.4.2",
"bytes 1.5.0", "bytes 1.5.0",
"futures-core", "futures-core",
"futures-sink", "futures-sink",
@ -46,9 +46,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-http" name = "actix-http"
version = "3.5.1" version = "3.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "129d4c88e98860e1758c5de288d1632b07970a16d59bdf7b8d66053d582bb71f" checksum = "d223b13fd481fc0d1f83bb12659ae774d9e3601814c68a0bc539731698cca743"
dependencies = [ dependencies = [
"actix-codec", "actix-codec",
"actix-rt", "actix-rt",
@ -180,9 +180,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-tls" name = "actix-tls"
version = "3.2.0" version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "929e47cc23865cdb856e59673cfba2d28f00b3bbd060dfc80e33a00a3cea8317" checksum = "d4cce60a2f2b477bc72e5cde0af1812a6e82d8fd85b5570a5dcf2a5bf2c5be5f"
dependencies = [ dependencies = [
"actix-rt", "actix-rt",
"actix-service", "actix-service",
@ -209,9 +209,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-web" name = "actix-web"
version = "4.4.1" version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e43428f3bf11dee6d166b00ec2df4e3aa8cc1606aaa0b7433c146852e2f4e03b" checksum = "43a6556ddebb638c2358714d853257ed226ece6023ef9364f23f0c70737ea984"
dependencies = [ dependencies = [
"actix-codec", "actix-codec",
"actix-http", "actix-http",
@ -548,6 +548,12 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "basen"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dbe4bb73fd931c4d1aaf53b35d1286c8a948ad00ec92c8e3c856f15fd027f43"
[[package]] [[package]]
name = "bigdecimal" name = "bigdecimal"
version = "0.3.1" version = "0.3.1"
@ -677,9 +683,9 @@ dependencies = [
[[package]] [[package]]
name = "bytecheck" name = "bytecheck"
version = "0.6.11" version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
dependencies = [ dependencies = [
"bytecheck_derive", "bytecheck_derive",
"ptr_meta", "ptr_meta",
@ -688,9 +694,9 @@ dependencies = [
[[package]] [[package]]
name = "bytecheck_derive" name = "bytecheck_derive"
version = "0.6.11" version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -936,9 +942,9 @@ dependencies = [
[[package]] [[package]]
name = "curl-sys" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c0333d8849afe78a4c8102a429a446bfdd055832af071945520e835ae2d841e" checksum = "c7b12a7ab780395666cb576203dc3ed6e01513754939a600b85196ccf5356bc5"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@ -1661,6 +1667,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.28" version = "0.14.28"
@ -1714,9 +1726,9 @@ dependencies = [
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.59" version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"core-foundation-sys", "core-foundation-sys",
@ -1759,9 +1771,9 @@ checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.1.0" version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.3", "hashbrown 0.14.3",
@ -1870,9 +1882,9 @@ dependencies = [
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.12.0" version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [ dependencies = [
"either", "either",
] ]
@ -1891,6 +1903,8 @@ dependencies = [
"actix-jwt-session", "actix-jwt-session",
"actix-web", "actix-web",
"async-trait", "async-trait",
"base64 0.21.7",
"basen",
"bincode", "bincode",
"chrono", "chrono",
"derive_more", "derive_more",
@ -1899,7 +1913,9 @@ dependencies = [
"figment", "figment",
"futures", "futures",
"futures-util", "futures-util",
"hmac",
"http-api-isahc-client", "http-api-isahc-client",
"humantime",
"jet-contract", "jet-contract",
"oauth2", "oauth2",
"oauth2-amazon", "oauth2-amazon",
@ -1909,6 +1925,7 @@ dependencies = [
"oauth2-gitlab", "oauth2-gitlab",
"oauth2-google", "oauth2-google",
"oauth2-signin", "oauth2-signin",
"password-hash",
"rand", "rand",
"reqwest", "reqwest",
"rumqttc", "rumqttc",
@ -1919,11 +1936,13 @@ dependencies = [
"serde-aux", "serde-aux",
"serde-email", "serde-email",
"serde_json", "serde_json",
"sha2",
"sqlx", "sqlx",
"thiserror", "thiserror",
"tokio", "tokio",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"tracing-test",
"uuid", "uuid",
"validators", "validators",
] ]
@ -2035,9 +2054,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.152" version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]] [[package]]
name = "libm" name = "libm"
@ -2160,13 +2179,13 @@ dependencies = [
[[package]] [[package]]
name = "maybe-async" name = "maybe-async"
version = "0.2.7" version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f1b8c13cb1f814b634a96b2c725449fe7ed464a7b8781de8688be5ffbd3f305" checksum = "afc95a651c82daf7004c824405aa1019723644950d488571bd718e3ed84646ed"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.109", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2224,9 +2243,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.7.1" version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
dependencies = [ dependencies = [
"adler", "adler",
] ]
@ -2320,6 +2339,12 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.45" version = "0.1.45"
@ -3081,18 +3106,18 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]] [[package]]
name = "rend" name = "rend"
version = "0.4.1" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd" checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
dependencies = [ dependencies = [
"bytecheck", "bytecheck",
] ]
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.11.23" version = "0.11.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251"
dependencies = [ dependencies = [
"base64 0.21.7", "base64 0.21.7",
"bytes 1.5.0", "bytes 1.5.0",
@ -3119,6 +3144,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper",
"system-configuration", "system-configuration",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
@ -3131,7 +3157,7 @@ dependencies = [
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams", "wasm-streams",
"web-sys", "web-sys",
"webpki-roots 0.25.3", "webpki-roots 0.25.4",
"winreg", "winreg",
] ]
@ -3166,9 +3192,9 @@ dependencies = [
[[package]] [[package]]
name = "rkyv" name = "rkyv"
version = "0.7.43" version = "0.7.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527a97cdfef66f65998b5f3b637c26f5a5ec09cc52a3f9932313ac645f4190f5" checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0"
dependencies = [ dependencies = [
"bitvec", "bitvec",
"bytecheck", "bytecheck",
@ -3184,9 +3210,9 @@ dependencies = [
[[package]] [[package]]
name = "rkyv_derive" name = "rkyv_derive"
version = "0.7.43" version = "0.7.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5c462a1328c8e67e4d6dbad1eb0355dd43e8ab432c6e227a43657f16ade5033" checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3278,9 +3304,9 @@ dependencies = [
[[package]] [[package]]
name = "rust_decimal" name = "rust_decimal"
version = "1.33.1" version = "1.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06676aec5ccb8fc1da723cc8c0f9a46549f21ebb8753d3915c6c41db1e7f1dc4" checksum = "755392e1a2f77afd95580d3f0d0e94ac83eeeb7167552c9b5bca549e61a94d83"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"borsh", "borsh",
@ -3309,9 +3335,9 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.30" version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [ dependencies = [
"bitflags 2.4.2", "bitflags 2.4.2",
"errno", "errno",
@ -3572,9 +3598,9 @@ dependencies = [
[[package]] [[package]]
name = "sentry" name = "sentry"
version = "0.32.1" version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab18211f62fb890f27c9bb04861f76e4be35e4c2fcbfc2d98afa37aadebb16f1" checksum = "766448f12e44d68e675d5789a261515c46ac6ccd240abdd451a9c46c84a49523"
dependencies = [ dependencies = [
"isahc 0.9.14", "isahc 0.9.14",
"reqwest", "reqwest",
@ -3587,14 +3613,14 @@ dependencies = [
"sentry-tracing", "sentry-tracing",
"tokio", "tokio",
"ureq", "ureq",
"webpki-roots 0.25.3", "webpki-roots 0.25.4",
] ]
[[package]] [[package]]
name = "sentry-backtrace" name = "sentry-backtrace"
version = "0.32.1" version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf018ff7d5ce5b23165a9cbfee60b270a55ae219bc9eebef2a3b6039356dd7e5" checksum = "32701cad8b3c78101e1cd33039303154791b0ff22e7802ed8cc23212ef478b45"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"once_cell", "once_cell",
@ -3604,9 +3630,9 @@ dependencies = [
[[package]] [[package]]
name = "sentry-contexts" name = "sentry-contexts"
version = "0.32.1" version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d934df6f9a17b8c15b829860d9d6d39e78126b5b970b365ccbd817bc0fe82c9" checksum = "17ddd2a91a13805bd8dab4ebf47323426f758c35f7bf24eacc1aded9668f3824"
dependencies = [ dependencies = [
"hostname", "hostname",
"libc", "libc",
@ -3618,9 +3644,9 @@ dependencies = [
[[package]] [[package]]
name = "sentry-core" name = "sentry-core"
version = "0.32.1" version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e362d3fb1c5de5124bf1681086eaca7adf6a8c4283a7e1545359c729f9128ff" checksum = "b1189f68d7e7e102ef7171adf75f83a59607fafd1a5eecc9dc06c026ff3bdec4"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"rand", "rand",
@ -3631,9 +3657,9 @@ dependencies = [
[[package]] [[package]]
name = "sentry-log" name = "sentry-log"
version = "0.32.1" version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7574422f662fe062a2ef7d027659ad8745e05fb815770887aeb08e2fb92cf6" checksum = "d2d7cd58e7b31a1a533163abf86c182824ea8f8867853a6b402cde053595a54b"
dependencies = [ dependencies = [
"log", "log",
"sentry-core", "sentry-core",
@ -3641,9 +3667,9 @@ dependencies = [
[[package]] [[package]]
name = "sentry-panic" name = "sentry-panic"
version = "0.32.1" version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0224e7a8e2bd8a32d96804acb8243d6d6e073fed55618afbdabae8249a964d8" checksum = "d1c18d0b5fba195a4950f2f4c31023725c76f00aabb5840b7950479ece21b5ca"
dependencies = [ dependencies = [
"sentry-backtrace", "sentry-backtrace",
"sentry-core", "sentry-core",
@ -3651,9 +3677,9 @@ dependencies = [
[[package]] [[package]]
name = "sentry-tracing" name = "sentry-tracing"
version = "0.32.1" version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "087bed8c616d176a9c6b662a8155e5f23b40dc9e1fa96d0bd5fb56e8636a9275" checksum = "3012699a9957d7f97047fd75d116e22d120668327db6e7c59824582e16e791b2"
dependencies = [ dependencies = [
"sentry-backtrace", "sentry-backtrace",
"sentry-core", "sentry-core",
@ -3663,9 +3689,9 @@ dependencies = [
[[package]] [[package]]
name = "sentry-types" name = "sentry-types"
version = "0.32.1" version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb4f0e37945b7a8ce7faebc310af92442e2d7c5aa7ef5b42fe6daa98ee133f65" checksum = "c7173fd594569091f68a7c37a886e202f4d0c1db1e1fa1d18a051ba695b2e2ec"
dependencies = [ dependencies = [
"debugid", "debugid",
"hex", "hex",
@ -3752,9 +3778,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.112" version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed" checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -3974,7 +4000,7 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c"
dependencies = [ dependencies = [
"itertools 0.12.0", "itertools 0.12.1",
"nom", "nom",
"unicode_categories", "unicode_categories",
] ]
@ -4290,6 +4316,12 @@ dependencies = [
"syn 2.0.48", "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]] [[package]]
name = "system-configuration" name = "system-configuration"
version = "0.5.1" version = "0.5.1"
@ -4362,12 +4394,13 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.31" version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa", "itoa",
"num-conv",
"powerfmt", "powerfmt",
"serde", "serde",
"time-core", "time-core",
@ -4382,10 +4415,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.16" version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
dependencies = [ dependencies = [
"num-conv",
"time-core", "time-core",
] ]
@ -4406,9 +4440,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.35.1" version = "1.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes 1.5.0", "bytes 1.5.0",
@ -4529,9 +4563,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.8" version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" checksum = "c6a4b9e8023eb94392d3dca65d717c53abc5dad49c07cb65bb8fcd87115fa325"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
@ -4550,9 +4584,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.21.0" version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@ -4653,6 +4687,29 @@ dependencies = [
"tracing-serde", "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]] [[package]]
name = "try-lock" name = "try-lock"
version = "0.2.5" version = "0.2.5"
@ -4749,7 +4806,7 @@ dependencies = [
"rustls 0.21.10", "rustls 0.21.10",
"rustls-webpki", "rustls-webpki",
"url", "url",
"webpki-roots 0.25.3", "webpki-roots 0.25.4",
] ]
[[package]] [[package]]
@ -4927,9 +4984,9 @@ checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b"
[[package]] [[package]]
name = "wasm-streams" name = "wasm-streams"
version = "0.3.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"js-sys", "js-sys",
@ -4969,9 +5026,9 @@ dependencies = [
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "0.25.3" version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]] [[package]]
name = "whoami" name = "whoami"
@ -5144,9 +5201,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.5.35" version = "0.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1931d78a9c73861da0134f453bb1f790ce49b2e30eba8410b4b79bac72b46a2d" checksum = "a7cad8365489051ae9f054164e459304af2e7e9bb407c958076c8bf4aef52da5"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

View File

@ -1,9 +1,10 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
use super::sea_orm_active_enums::Roles;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::sea_orm_active_enums::Roles;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "instance_admins")] #[sea_orm(table_name = "instance_admins")]
pub struct Model { pub struct Model {

View File

@ -1,9 +1,10 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
use super::sea_orm_active_enums::ProjectMemberRoles;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::sea_orm_active_enums::ProjectMemberRoles;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "project_member_invites")] #[sea_orm(table_name = "project_member_invites")]
pub struct Model { pub struct Model {

View File

@ -1,9 +1,10 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
use super::sea_orm_active_enums::ProjectMemberRoles;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::sea_orm_active_enums::ProjectMemberRoles;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "project_members")] #[sea_orm(table_name = "project_members")]
pub struct Model { pub struct Model {

View File

@ -1,9 +1,10 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
use super::sea_orm_active_enums::Roles;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::sea_orm_active_enums::Roles;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "workspace_member_invites")] #[sea_orm(table_name = "workspace_member_invites")]
pub struct Model { pub struct Model {

View File

@ -1,9 +1,10 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
use super::sea_orm_active_enums::Roles;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::sea_orm_active_enums::Roles;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "workspace_members")] #[sea_orm(table_name = "workspace_members")]
pub struct Model { pub struct Model {

View File

@ -43,3 +43,13 @@ dotenv = "0.15.0"
chrono = { version = "0.4.32", default-features = false, features = ["clock", "serde"] } chrono = { version = "0.4.32", default-features = false, features = ["clock", "serde"] }
validators = { version = "0.25.3", default-features = false, features = ["email", "derive", "all-validators"] } 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"] } 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"

View File

@ -20,6 +20,8 @@
* `HAS_OPENAI_CONFIGURED` - added for compatibility but not supported * `HAS_OPENAI_CONFIGURED` - added for compatibility but not supported
* `FILE_SIZE_LIMIT` - maximum file size * `FILE_SIZE_LIMIT` - maximum file size
* `IS_SELF_MANAGED` - always `true` but added for compatibility with `plane` * `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 ### Optional Environment

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +1,5 @@
use figment::{providers::Env, Figment}; use figment::providers::Env;
use figment::Figment;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_aux::field_attributes::deserialize_bool_from_anything; use serde_aux::field_attributes::deserialize_bool_from_anything;
use serde_aux::serde_introspection::serde_introspect; use serde_aux::serde_introspection::serde_introspect;

View File

@ -1,7 +1,5 @@
use figment::{ use figment::value::{Dict, Map};
value::{Dict, Map}, use figment::{Error, Profile, Provider};
Error, Profile, Provider,
};
use jet_contract::redis; use jet_contract::redis;
use jet_contract::redis::Commands; use jet_contract::redis::Commands;
use tracing::debug; use tracing::debug;

View File

@ -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 derive_more::*;
use futures_util::{future::LocalBoxFuture, FutureExt}; use futures_util::future::LocalBoxFuture;
use futures_util::FutureExt;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use tracing::warn; use tracing::{debug, warn};
#[derive(Debug, Display)] #[derive(Debug, Display)]
#[display(fmt = "{{\"error\":\"Instance is not configured\"}}")] #[display(fmt = "{{\"error\":\"Instance is not configured\"}}")]
@ -53,21 +55,26 @@ impl FromRequest for RequireInstanceConfigured {
req: &actix_web::HttpRequest, req: &actix_web::HttpRequest,
_payload: &mut actix_web::dev::Payload, _payload: &mut actix_web::dev::Payload,
) -> Self::Future { ) -> Self::Future {
debug!("Looking for instance");
let db = req.app_data::<Data<DatabaseConnection>>().cloned(); let db = req.app_data::<Data<DatabaseConnection>>().cloned();
debug!("Looking for instance: db found");
async move { async move {
let Some(db) = db else { let Some(db) = db else {
warn!("Failed to fetch database connection fot required instance configured"); warn!("Failed to fetch database connection fot required instance configured");
return Err(NoInstance); return Err(NoInstance);
}; };
debug!("Looking for instance: db exists");
use sea_orm::EntityTrait; use sea_orm::EntityTrait;
let Ok(Some(instance)) = entities::prelude::Instances::find().one(&**db).await else { let Ok(Some(instance)) = entities::prelude::Instances::find().one(&**db).await else {
warn!("No instance found"); warn!("No instance found");
return Err(NoInstance); return Err(NoInstance);
}; };
debug!("Looking for instance: instance found");
if !instance.is_setup_done { if !instance.is_setup_done {
warn!("Instance is not configured"); warn!("Instance is not configured");
return Err(NoInstance); return Err(NoInstance);
} }
debug!("Looking for instance: instance is configured");
return Ok(Self(instance)); return Ok(Self(instance));
} }
.boxed_local() .boxed_local()

View File

@ -1,11 +1,8 @@
use crate::models::{Error, JsonError};
use crate::session::AppClaims;
use actix_jwt_session::{ use actix_jwt_session::{
Duration, Hashing, JwtTtl, Pair, RefreshTtl, SessionStorage, JWT_HEADER_NAME, Duration, Hashing, JwtTtl, Pair, RefreshTtl, SessionStorage, JWT_HEADER_NAME,
REFRESH_HEADER_NAME, REFRESH_HEADER_NAME,
}; };
use actix_web::web::scope; use actix_web::web::{scope, Data, ServiceConfig};
use actix_web::web::{Data, ServiceConfig};
use actix_web::{HttpRequest, HttpResponse}; use actix_web::{HttpRequest, HttpResponse};
use entities::prelude::Users; use entities::prelude::Users;
use entities::users::Model as User; use entities::users::Model as User;
@ -18,13 +15,19 @@ use validators::prelude::*;
use validators::Validator; use validators::Validator;
use validators_prelude::Host; use validators_prelude::Host;
use crate::models::{Error, JsonError};
use crate::session::AppClaims;
mod change_password;
mod email_check; mod email_check;
mod magic_generate; mod magic_generate;
mod magic_sign_in; mod magic_sign_in;
mod reset_password;
mod sign_in; mod sign_in;
mod sign_out; mod sign_out;
mod sign_up; mod sign_up;
mod social_auth; mod social_auth;
mod token_refresh;
#[derive(Debug, serde::Serialize)] #[derive(Debug, serde::Serialize)]
pub struct AuthResponseBody { pub struct AuthResponseBody {
@ -57,13 +60,17 @@ pub fn configure(http_client: reqwest::Client, config: &mut ServiceConfig) {
.service(sign_up::sign_up) .service(sign_up::sign_up)
.service(sign_out::sign_out) .service(sign_out::sign_out)
.service(magic_sign_in::magic_sign_in) .service(magic_sign_in::magic_sign_in)
.service(token_refresh::refresh_token)
.service(change_password::change_password)
.configure(|c| { .configure(|c| {
social_auth::configure(http_client, 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 { pub enum PublishError {
#[display(fmt = "Failed to inform system modules about created user account")] #[display(fmt = "Failed to inform system modules about created user account")]
UserCreated, UserCreated,
@ -73,7 +80,7 @@ pub enum PublishError {
MagicLinkEmail, MagicLinkEmail,
} }
#[derive(Debug, Clone, derive_more::Display)] #[derive(Debug, Clone, PartialEq, derive_more::Display, serde::Serialize, serde::Deserialize)]
pub enum OAuthError { pub enum OAuthError {
#[display(fmt = "Failed to load workspace invides for {user_id} on {provider} callback")] #[display(fmt = "Failed to load workspace invides for {user_id} on {provider} callback")]
FetchWorkspaceInvites { user_id: Uuid, provider: String }, FetchWorkspaceInvites { user_id: Uuid, provider: String },
@ -83,7 +90,15 @@ pub enum OAuthError {
ConnectSocialMedia { user_id: Uuid, provider: String }, 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 { pub enum AuthError {
#[display(fmt = "New account creation is disabled. Please contact your site administrator")] #[display(fmt = "New account creation is disabled. Please contact your site administrator")]
RegisterOff, RegisterOff,
@ -232,33 +247,199 @@ pub fn random_password() -> String {
pub mod password { pub mod password {
const HAS_NUM: u8 = 1; const HAS_NUM: u8 = 1;
const HAS_UPPER: u8 = 2; const HAS_UPPER: u8 = 1 << 1;
const HAS_LOWER: u8 = 4; const HAS_LOWER: u8 = 1 << 2;
const HAS_SPECIAL: u8 = 8; 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 { pub fn is_valid(pass: &str) -> bool {
pass.len() >= 8 (8..60).contains(&pass.len())
|| pass.chars().fold(0, |memo, c| match c { || 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_numeric() => memo | HAS_NUM,
_ if c.is_uppercase() => memo | HAS_UPPER, _ if c.is_uppercase() => memo | HAS_UPPER,
_ if c.is_lowercase() => memo | HAS_UPPER, _ if c.is_lowercase() => memo | HAS_LOWER,
_ => memo | HAS_SPECIAL, _ => 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 { pub mod magic_link {
use crate::models::Error;
use crate::{http::AuthError, redis_c, RedisClient};
use actix_web::web::Data; use actix_web::web::Data;
use jet_contract::*; use jet_contract::*;
use rand::prelude::*; use rand::prelude::*;
use redis::AsyncCommands; use redis::AsyncCommands;
use crate::http::AuthError;
use crate::models::Error;
use crate::{redis_c, RedisClient};
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub enum AttemptValidity { pub enum AttemptValidity {
Allowed, Allowed,
@ -366,10 +547,11 @@ pub mod magic_link {
#[cfg(test)] #[cfg(test)]
mod create_magic_link_tests { mod create_magic_link_tests {
use super::*;
use actix_web::web::Data; use actix_web::web::Data;
use jet_contract::deadpool_redis; use jet_contract::deadpool_redis;
use super::*;
#[tokio::test] #[tokio::test]
async fn full() { async fn full() {
let email = "foo@bar.com".to_string(); let email = "foo@bar.com".to_string();

View 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" }));
}
}

View File

@ -1,21 +1,21 @@
use super::{AuthError, PublishError};
use actix_web::http::header::USER_AGENT; use actix_web::http::header::USER_AGENT;
use actix_web::web::{Data, Json}; use actix_web::web::{Data, Json};
use actix_web::{post, HttpRequest, HttpResponse}; use actix_web::{post, HttpRequest, HttpResponse};
use entities::prelude::{Users, WorkspaceMemberInvites}; use entities::prelude::WorkspaceMemberInvites;
use entities::users::Model as User; use entities::users::Model as User;
use jet_contract::event_bus::SignInMedium; use jet_contract::event_bus::SignInMedium;
use sea_orm::{prelude::*, DatabaseTransaction}; use sea_orm::prelude::*;
use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter}; use sea_orm::{DatabaseConnection, DatabaseTransaction, EntityTrait, QueryFilter};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_email::Email; use serde_email::Email;
use super::{AuthError, PublishError};
use crate::config::ApplicationConfig; use crate::config::ApplicationConfig;
use crate::extractors::RequireInstanceConfigured; use crate::extractors::RequireInstanceConfigured;
use crate::http::magic_link::create_magic_link; use crate::http::magic_link::create_magic_link;
use crate::models::*;
use crate::utils::user_by_email; use crate::utils::user_by_email;
use crate::{db_commit, db_rollback, db_t, models::*}; use crate::{db_commit, db_rollback, db_t, EventBusClient, RedisClient};
use crate::{EventBusClient, RedisClient};
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct EmailCheckPayload { 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( async fn register(
req: HttpRequest, req: HttpRequest,
payload: Json<EmailCheckPayload>, payload: Json<EmailCheckPayload>,

View File

@ -1,20 +1,17 @@
use actix_web::{ use actix_web::web::{Data, Json};
post, use actix_web::{post, HttpRequest, HttpResponse};
web::{Data, Json},
HttpRequest, HttpResponse,
};
use jet_contract::event_bus::{EmailMsg, Topic}; use jet_contract::event_bus::{EmailMsg, Topic};
use sea_orm::DatabaseConnection; use sea_orm::prelude::*;
use sea_orm::{prelude::*, DatabaseTransaction}; use sea_orm::{DatabaseConnection, DatabaseTransaction};
use serde::Deserialize; use serde::Deserialize;
use serde_json::json; use serde_json::json;
use tracing::error; use tracing::{error, warn};
use super::{create_user, random_password}; use super::{create_user, random_password};
use crate::{ use crate::extractors::RequireInstanceConfigured;
db_commit, db_rollback, db_t, extractors::RequireInstanceConfigured, models::JsonError, use crate::models::{JsonError, *};
models::*, utils::extract_req_current_site, EventBusClient, RedisClient, use crate::utils::extract_req_current_site;
}; use crate::{db_commit, db_rollback, db_t, EventBusClient, RedisClient};
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct Input { 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 (key, token, _validity) = super::magic_link::create_magic_link(&email, redis).await?;
let current_site = extract_req_current_site(&req)?; let current_site = extract_req_current_site(&req)?;
event_bus if let Err(e) = event_bus
.publish( .publish(
Topic::Email, Topic::Email,
jet_contract::event_bus::Msg::Email(EmailMsg::MagicLink { jet_contract::event_bus::Msg::Email(EmailMsg::MagicLink {
@ -80,7 +77,10 @@ async fn try_create_magic_link(
rumqttc::QoS::AtLeastOnce, rumqttc::QoS::AtLeastOnce,
true, true,
) )
.await; .await
{
warn!("Failed to send event to MQTT: {e}")
};
Ok(HttpResponse::Ok().json(json!({ "key": key }))) Ok(HttpResponse::Ok().json(json!({ "key": key })))
} }

View File

@ -1,33 +1,19 @@
use actix_jwt_session::{Duration, JwtTtl, RefreshTtl, SessionStorage}; use actix_jwt_session::SessionStorage;
use actix_web::{ use actix_web::web::{Data, Json};
post, use actix_web::{post, HttpRequest, HttpResponse};
web::{Data, Json}, use jet_contract::event_bus::{Msg, SignInMedium, Topic, UserMsg};
HttpRequest, HttpResponse, use jet_contract::redis::AsyncCommands;
};
use jet_contract::{
event_bus::{Msg, SignInMedium, Topic, UserMsg},
redis::AsyncCommands,
};
use reqwest::StatusCode; use reqwest::StatusCode;
use rumqttc::QoS; use rumqttc::QoS;
use sea_orm::prelude::*; use sea_orm::prelude::*;
use sea_orm::*; use sea_orm::*;
use serde::Deserialize; use serde::Deserialize;
use serde_json::json;
use tracing::error;
use crate::{ use super::auth_http_response;
db_commit, db_rollback, db_t, use crate::extractors::RequireInstanceConfigured;
extractors::RequireInstanceConfigured, use crate::models::{Error, JsonError};
models::{Error, JsonError}, use crate::utils::{extract_req_ip, extract_req_uagent, user_by_email};
redis_c, use crate::{db_commit, db_rollback, db_t, redis_c, EventBusClient, RedisClient};
session::AppClaims,
utils::{extract_req_ip, extract_req_uagent, user_by_email},
};
use crate::{EventBusClient, RedisClient};
use super::{AuthResponseBody, auth_http_response};
#[post("/magic-sign-in")] #[post("/magic-sign-in")]
async fn magic_sign_in( async fn magic_sign_in(

View 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)
}

View File

@ -1,6 +1,5 @@
use actix_jwt_session::SessionStorage; use actix_jwt_session::SessionStorage;
use actix_web::web::{Data, Json}; use actix_web::web::{Data, Json};
use actix_web::ResponseError;
use actix_web::{post, HttpRequest, HttpResponse}; use actix_web::{post, HttpRequest, HttpResponse};
use entities::prelude::Users; use entities::prelude::Users;
use entities::users::ActiveModel as UserModel; use entities::users::ActiveModel as UserModel;
@ -11,14 +10,13 @@ use rumqttc::QoS;
use sea_orm::*; use sea_orm::*;
use validators::prelude::*; use validators::prelude::*;
use super::EmailAllowComment;
use crate::config::ApplicationConfig; use crate::config::ApplicationConfig;
use crate::extractors::RequireInstanceConfigured; use crate::extractors::RequireInstanceConfigured;
use crate::http::api::authentication::{auth_http_response, create_user, has_workspace_invites}; use crate::http::api::authentication::{auth_http_response, create_user, has_workspace_invites};
use crate::models::{Error, JsonError}; use crate::models::{Error, JsonError};
use crate::utils::user_by_email; use crate::utils::user_by_email;
use super::EmailAllowComment;
#[post("/sign-in")] #[post("/sign-in")]
pub async fn sign_in( pub async fn sign_in(
_: RequireInstanceConfigured, _: RequireInstanceConfigured,

View File

@ -1,6 +1,6 @@
use actix_jwt_session::{Authenticated, RefreshToken, SessionStorage}; use actix_jwt_session::{Authenticated, RefreshToken, SessionStorage};
use actix_web::HttpRequest; use actix_web::web::Data;
use actix_web::{post, web::Data, HttpResponse}; use actix_web::{post, HttpRequest, HttpResponse};
use entities::prelude::Users; use entities::prelude::Users;
use entities::users::{ActiveModel as UserModel, Column}; use entities::users::{ActiveModel as UserModel, Column};
use sea_orm::prelude::*; use sea_orm::prelude::*;

View File

@ -1,25 +1,22 @@
use actix_jwt_session::SessionStorage; use actix_jwt_session::SessionStorage;
use actix_web::web::{Data, Json}; use actix_web::web::{Data, Json};
use actix_web::ResponseError; use actix_web::{post, HttpRequest, HttpResponse, ResponseError};
use actix_web::{post, HttpRequest, HttpResponse};
use entities::prelude::Users; use entities::prelude::Users;
use entities::users::ActiveModel as UserModel; use entities::users::ActiveModel as UserModel;
use jet_contract::event_bus::{Msg, SignInMedium, Topic, UserMsg}; use jet_contract::event_bus::{Msg, SignInMedium, Topic, UserMsg};
use jet_contract::UserId; use jet_contract::UserId;
use reqwest::StatusCode; use reqwest::StatusCode;
use rumqttc::QoS; use rumqttc::QoS;
use sea_orm::DatabaseConnection; use sea_orm::{DatabaseConnection, *};
use sea_orm::*;
use validators::prelude::*; use validators::prelude::*;
use super::EmailAllowComment;
use crate::config::ApplicationConfig; use crate::config::ApplicationConfig;
use crate::extractors::RequireInstanceConfigured; use crate::extractors::RequireInstanceConfigured;
use crate::http::api::authentication::{auth_http_response, create_user, has_workspace_invites}; use crate::http::api::authentication::{auth_http_response, create_user, has_workspace_invites};
use crate::models::{Error, JsonError}; use crate::models::{Error, JsonError};
use crate::utils::{extract_req_ip, extract_req_uagent}; use crate::utils::{extract_req_ip, extract_req_uagent};
use super::EmailAllowComment;
#[post("/sign-up")] #[post("/sign-up")]
pub async fn sign_up( pub async fn sign_up(
_: RequireInstanceConfigured, _: RequireInstanceConfigured,

View File

@ -1,19 +1,11 @@
use std::env::var as env_var; use std::env::var as env_var;
use super::{auth_http_response, AuthError}; use actix_web::web::{self, Data, ServiceConfig};
use actix_web::{get, HttpRequest, HttpResponse};
use actix_web::{ use entities::users::ActiveModel as UserModel;
get,
web::{self, Data, ServiceConfig},
HttpRequest, HttpResponse,
};
use entities::prelude::Users;
use entities::users::{ActiveModel as UserModel, Column as UserColumn};
use http_api_isahc_client::IsahcClient; use http_api_isahc_client::IsahcClient;
use jet_contract::{ use jet_contract::event_bus::{Msg, SignInMedium, Topic, UserMsg};
event_bus::{Msg, SignInMedium, Topic, UserMsg}, use jet_contract::UserId;
UserId,
};
use oauth2_amazon::{ use oauth2_amazon::{
AmazonExtensionsBuilder, AmazonProviderWithWebServices, AmazonScope, AmazonTokenUrlRegion, AmazonExtensionsBuilder, AmazonProviderWithWebServices, AmazonScope, AmazonTokenUrlRegion,
}; };
@ -27,20 +19,18 @@ use oauth2_google::{
GoogleProviderForWebServerAppsAccessType, GoogleScope, GoogleProviderForWebServerAppsAccessType, GoogleScope,
}; };
use oauth2_signin::web_app::{SigninFlow, SigninFlowHandleCallbackByQueryConfiguration}; use oauth2_signin::web_app::{SigninFlow, SigninFlowHandleCallbackByQueryConfiguration};
use reqwest::{header::LOCATION, StatusCode}; use reqwest::header::LOCATION;
use sea_orm::{ use reqwest::StatusCode;
ActiveModelTrait, ActiveValue::NotSet, ColumnTrait, DatabaseConnection, DatabaseTransaction, use sea_orm::ActiveValue::NotSet;
EntityTrait, QueryFilter, Set, TransactionTrait, use sea_orm::{ActiveModelTrait, DatabaseConnection, DatabaseTransaction, Set};
};
use tracing::{debug, error, warn}; use tracing::{debug, error, warn};
use crate::{ use super::{auth_http_response, AuthError};
db_commit, db_rollback, db_t, use crate::extractors::RequireInstanceConfigured;
extractors::RequireInstanceConfigured, use crate::http::OAuthError;
http::OAuthError, use crate::models::{Error, JsonError};
models::{Error, JsonError}, use crate::utils::user_by_email;
utils::user_by_email, use crate::{db_commit, db_rollback, db_t};
};
macro_rules! oauth_envs { macro_rules! oauth_envs {
($env: expr, 2) => {{ ($env: expr, 2) => {{

View 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 })))
}

View File

@ -1,13 +1,11 @@
use actix_web::{ use actix_web::web::{Data, ServiceConfig};
get, use actix_web::{get, HttpResponse};
web::{Data, ServiceConfig},
HttpResponse,
};
use crate::config::ApplicationConfig; use crate::config::ApplicationConfig;
pub fn configure(config: &mut ServiceConfig) { 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); config.service(configs);
} }

View File

@ -1,15 +1,20 @@
use crate::extractors::RequireInstanceConfigured; use actix_jwt_session::{Authenticated, Hashing};
use crate::session::AppClaims; use actix_web::web::{scope, Data, Json, ServiceConfig};
use actix_jwt_session::Authenticated; use actix_web::{get, post, HttpResponse};
use actix_web::get;
use actix_web::web::{scope, Data, ServiceConfig};
use actix_web::HttpResponse;
use entities::prelude::Users; use entities::prelude::Users;
use entities::users::ActiveModel as UserModel;
use sea_orm::prelude::*; 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) { pub fn configure(config: &mut ServiceConfig) {
config.service(scope("/users").service(retrieve)); config.service(scope("/users").service(retrieve).service(change_password));
} }
#[get("/me")] #[get("/me")]
@ -23,3 +28,53 @@ async fn retrieve(
Err(_error) => HttpResponse::BadRequest().finish(), 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" })))
}

View File

@ -1,6 +1,5 @@
use actix_web::get;
use actix_web::web::{Path, ServiceConfig}; use actix_web::web::{Path, ServiceConfig};
use actix_web::HttpResponse; use actix_web::{get, HttpResponse};
use uuid::Uuid; use uuid::Uuid;
pub fn configure(config: &mut ServiceConfig) { pub fn configure(config: &mut ServiceConfig) {

View File

@ -1,6 +1,5 @@
use actix_web::get;
use actix_web::web::{Path, ServiceConfig}; use actix_web::web::{Path, ServiceConfig};
use actix_web::HttpResponse; use actix_web::{get, HttpResponse};
use uuid::Uuid; use uuid::Uuid;
pub fn configure(config: &mut ServiceConfig) { pub fn configure(config: &mut ServiceConfig) {

View File

@ -1,10 +1,11 @@
use std::env; use std::env;
use actix_jwt_session::*; use actix_jwt_session::*;
use actix_web::middleware::NormalizePath; use actix_web::web::Data;
use actix_web::{web::Data, App, HttpServer}; use actix_web::{App, HttpServer};
pub use jet_contract::event_bus::Client as EventBusClient; pub use jet_contract::event_bus::Client as EventBusClient;
pub use jet_contract::{deadpool_redis, redis, RedisClient}; pub use jet_contract::{deadpool_redis, redis, RedisClient};
use models::PasswordResetSecret;
pub use sea_orm::{Database, DatabaseConnection}; pub use sea_orm::{Database, DatabaseConnection};
pub mod config; pub mod config;
@ -81,6 +82,12 @@ async fn main() {
let http_client = reqwest::Client::new(); let http_client = reqwest::Client::new();
let application_config = Data::new(application_config); let application_config = Data::new(application_config);
let event_bus = Data::new(jet_contract::event_bus::Client::new(eb_client)); 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 || { HttpServer::new(move || {
App::new() App::new()
@ -91,9 +98,11 @@ async fn main() {
.app_data(Data::new(db.clone())) .app_data(Data::new(db.clone()))
.app_data(application_config.clone()) .app_data(application_config.clone())
.app_data(event_bus.clone()) .app_data(event_bus.clone())
.wrap(NormalizePath::trim()) .app_data(pass_reset_secret.clone())
.wrap(factory.clone()) .app_data(pass_reset_timeout.clone())
.wrap(actix_web::middleware::NormalizePath::trim())
.wrap(actix_web::middleware::Logger::default()) .wrap(actix_web::middleware::Logger::default())
.wrap(factory.clone())
.configure(|config| http::configure(http_client.clone(), config)) .configure(|config| http::configure(http_client.clone(), config))
}) })
.bind(addr) .bind(addr)

View File

@ -1,26 +1,67 @@
use std::fmt::{Debug, Display};
use actix_web::HttpResponse; use actix_web::HttpResponse;
use derive_more::Display; use derive_more::{Constructor, Deref};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::http::AuthError; use crate::http::AuthError;
#[derive(Debug, Serialize, Deserialize, Display)] #[derive(Debug, Deref, Constructor)]
#[display(fmt = "{{\"error\":\"{error}\"}}")] 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 struct JsonError {
pub error: String, 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 { impl JsonError {
pub fn new(error: impl std::fmt::Display) -> Self { pub fn new(error: impl std::fmt::Display) -> Self {
Self { Self {
error: format!("{error}"), 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 { impl actix_web::ResponseError for JsonError {
fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> { 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 { pub enum Error {
#[display(fmt = "Database connection error")] #[display(fmt = "Database connection error")]
DatabaseError, DatabaseError,
@ -52,6 +155,10 @@ pub enum Error {
ContactSupport, ContactSupport,
#[display(fmt = "User not found")] #[display(fmt = "User not found")]
UserRequired, UserRequired,
#[display(
fmt = "Password couldn't be encrypted. Please try again later or contact the support team."
)]
EncryptPass,
} }
impl From<AuthError> for Error { impl From<AuthError> for Error {
@ -66,6 +173,7 @@ impl actix_web::ResponseError for Error {
Self::DatabaseError | Self::NoHost => { Self::DatabaseError | Self::NoHost => {
HttpResponse::InternalServerError().json(JsonError::new(self)) HttpResponse::InternalServerError().json(JsonError::new(self))
} }
Self::UserRequired => HttpResponse::Unauthorized().json(JsonError::new(self)),
_ => HttpResponse::BadRequest().json(JsonError::new(self)), _ => HttpResponse::BadRequest().json(JsonError::new(self)),
} }
} }

View File

@ -1,28 +1,26 @@
use actix_web::web::Data; use actix_jwt_session::Hashing;
use actix_web::HttpRequest; use actix_web::HttpRequest;
use chrono::Utc; use chrono::Utc;
use entities::prelude::{
ProjectMemberInvites, ProjectMembers, WorkspaceMemberInvites, WorkspaceMembers,
};
use entities::project_member_invites::{ use entities::project_member_invites::{
Column as ProjectMemberInviteColumn, Model as ProjectMemberInvite, 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::{ use entities::workspace_member_invites::{
Column as WorkspaceMemberInviteColumn, Model as WorkspaceMemberInvite, Column as WorkspaceMemberInviteColumn, Model as WorkspaceMemberInvite,
}; };
use entities::workspace_members::ActiveModel as WorkspaceMemberModel; 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 reqwest::header::USER_AGENT;
use sea_orm::sea_query::OnConflict; use sea_orm::sea_query::OnConflict;
use sea_orm::*; use sea_orm::*;
use tracing::error; use tracing::error;
use uuid::Uuid; use uuid::Uuid;
use crate::http::OAuthError; use crate::http::{AuthError, OAuthError};
use crate::{http::AuthError, models::Error}; use crate::models::Error;
#[macro_export] #[macro_export]
macro_rules! db_t { macro_rules! db_t {
@ -38,13 +36,23 @@ macro_rules! db_t {
#[macro_export] #[macro_export]
macro_rules! db_commit { macro_rules! db_commit {
($db: expr) => {{ ($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}"); tracing::error!("Failed to commit db tracation: {e}");
crate::models::Error::DatabaseError crate::models::Error::DatabaseError
}) })
}}; }};
($db: expr, $msg: expr) => {{ ($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); tracing::error!(std::concat!($msg, ": {}"), e);
crate::models::Error::DatabaseError 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( pub async fn user_by_email(
email: &str, email: &str,
db: &mut DatabaseTransaction, db: &mut DatabaseTransaction,
@ -265,3 +292,75 @@ pub async fn invites_to_membership(
}; };
Ok(()) 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()
)
}
}

View File

@ -1,15 +1,11 @@
use chrono::{NaiveDate, NaiveDateTime};
use derive_more::*;
use std::path::PathBuf; use std::path::PathBuf;
pub use deadpool_redis; use chrono::{NaiveDate, NaiveDateTime};
pub use deadpool_redis::Pool as RedisClient; pub use deadpool_redis::Pool as RedisClient;
pub use redis; use derive_more::*;
pub use rumqttc;
pub use serde;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
pub use {deadpool_redis, redis, rumqttc, serde};
pub type Id = Uuid; pub type Id = Uuid;

View File

@ -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 //! # Examples
//! //!

7
rustfmt.toml Normal file
View 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"