From 09d164576fd5afcefd60b8b57c5fe97cc0e06e96 Mon Sep 17 00:00:00 2001 From: eraden Date: Thu, 8 Feb 2024 10:14:00 +0100 Subject: [PATCH] Test change pass --- Cargo.lock | 217 +++++---- crates/entities/src/instance_admins.rs | 3 +- crates/entities/src/project_member_invites.rs | 3 +- crates/entities/src/project_members.rs | 3 +- .../entities/src/workspace_member_invites.rs | 3 +- crates/entities/src/workspace_members.rs | 3 +- crates/jet-api/Cargo.toml | 10 + crates/jet-api/README.md | 2 + crates/jet-api/config/jwt-decoding.bin | Bin 0 -> 32 bytes crates/jet-api/config/jwt-encoding.bin | Bin 0 -> 85 bytes crates/jet-api/src/config.rs | 3 +- crates/jet-api/src/config/redis_provider.rs | 6 +- .../src/extractors/require_instance.rs | 13 +- crates/jet-api/src/http/api/authentication.rs | 224 +++++++++- .../api/authentication/change_password.rs | 421 ++++++++++++++++++ .../http/api/authentication/email_check.rs | 15 +- .../http/api/authentication/magic_generate.rs | 28 +- .../http/api/authentication/magic_sign_in.rs | 34 +- .../http/api/authentication/reset_password.rs | 112 +++++ .../src/http/api/authentication/sign_in.rs | 4 +- .../src/http/api/authentication/sign_out.rs | 4 +- .../src/http/api/authentication/sign_up.rs | 9 +- .../http/api/authentication/social_auth.rs | 40 +- .../http/api/authentication/token_refresh.rs | 34 ++ crates/jet-api/src/http/api/config.rs | 10 +- crates/jet-api/src/http/api/users.rs | 71 ++- crates/jet-api/src/http/api_v1/projects.rs | 3 +- crates/jet-api/src/http/api_v1/states.rs | 3 +- crates/jet-api/src/main.rs | 17 +- crates/jet-api/src/models.rs | 118 ++++- crates/jet-api/src/utils/mod.rs | 123 ++++- crates/jet-contract/src/lib.rs | 10 +- crates/jet-plug/src/lib.rs | 3 +- rustfmt.toml | 7 + 34 files changed, 1314 insertions(+), 242 deletions(-) create mode 100644 crates/jet-api/config/jwt-decoding.bin create mode 100644 crates/jet-api/config/jwt-encoding.bin create mode 100644 crates/jet-api/src/http/api/authentication/change_password.rs create mode 100644 crates/jet-api/src/http/api/authentication/reset_password.rs create mode 100644 crates/jet-api/src/http/api/authentication/token_refresh.rs create mode 100644 rustfmt.toml diff --git a/Cargo.lock b/Cargo.lock index 848b4f1..7293b08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/crates/entities/src/instance_admins.rs b/crates/entities/src/instance_admins.rs index 8521e95..a8e1e98 100644 --- a/crates/entities/src/instance_admins.rs +++ b/crates/entities/src/instance_admins.rs @@ -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 { diff --git a/crates/entities/src/project_member_invites.rs b/crates/entities/src/project_member_invites.rs index 0483327..fd0adc3 100644 --- a/crates/entities/src/project_member_invites.rs +++ b/crates/entities/src/project_member_invites.rs @@ -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 { diff --git a/crates/entities/src/project_members.rs b/crates/entities/src/project_members.rs index e328f35..9868369 100644 --- a/crates/entities/src/project_members.rs +++ b/crates/entities/src/project_members.rs @@ -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 { diff --git a/crates/entities/src/workspace_member_invites.rs b/crates/entities/src/workspace_member_invites.rs index 30c112e..bf46707 100644 --- a/crates/entities/src/workspace_member_invites.rs +++ b/crates/entities/src/workspace_member_invites.rs @@ -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 { diff --git a/crates/entities/src/workspace_members.rs b/crates/entities/src/workspace_members.rs index d59e7bb..cb9be26 100644 --- a/crates/entities/src/workspace_members.rs +++ b/crates/entities/src/workspace_members.rs @@ -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 { diff --git a/crates/jet-api/Cargo.toml b/crates/jet-api/Cargo.toml index 5be8717..1790a5e 100644 --- a/crates/jet-api/Cargo.toml +++ b/crates/jet-api/Cargo.toml @@ -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" diff --git a/crates/jet-api/README.md b/crates/jet-api/README.md index 270dcd0..ac83898 100644 --- a/crates/jet-api/README.md +++ b/crates/jet-api/README.md @@ -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 diff --git a/crates/jet-api/config/jwt-decoding.bin b/crates/jet-api/config/jwt-decoding.bin new file mode 100644 index 0000000000000000000000000000000000000000..9d559f2fa604b215451ec2f5fcce3e7a4ca97c2e GIT binary patch literal 32 qcmV+*0N?+v9c`qD5U;b8rglHmyo&n(WRj#;cCDAkJf?S+Zqii{mJrYY literal 0 HcmV?d00001 diff --git a/crates/jet-api/config/jwt-encoding.bin b/crates/jet-api/config/jwt-encoding.bin new file mode 100644 index 0000000000000000000000000000000000000000..eac1507a1f8695f570a2eed382af829d2cc7241d GIT binary patch literal 85 zcmV-b0IL5mQvv}2Fa-t!D`jv5A_O3k@2{HhTKWg_5v2Of4C9;s+Xs6>86~iR>4D&Y r{RwxWBLg7-t{rWph!C%{l%{q+)4YoN0A!M+S9YzJ$2_KYmTuBjxPB%m literal 0 HcmV?d00001 diff --git a/crates/jet-api/src/config.rs b/crates/jet-api/src/config.rs index 2193312..e92dd90 100644 --- a/crates/jet-api/src/config.rs +++ b/crates/jet-api/src/config.rs @@ -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; diff --git a/crates/jet-api/src/config/redis_provider.rs b/crates/jet-api/src/config/redis_provider.rs index 4ee69b0..9cbdc4f 100644 --- a/crates/jet-api/src/config/redis_provider.rs +++ b/crates/jet-api/src/config/redis_provider.rs @@ -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; diff --git a/crates/jet-api/src/extractors/require_instance.rs b/crates/jet-api/src/extractors/require_instance.rs index 2b4211c..ec7e445 100644 --- a/crates/jet-api/src/extractors/require_instance.rs +++ b/crates/jet-api/src/extractors/require_instance.rs @@ -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::>().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() diff --git a/crates/jet-api/src/http/api/authentication.rs b/crates/jet-api/src/http/api/authentication.rs index 1d171ff..4ae1866 100644 --- a/crates/jet-api/src/http/api/authentication.rs +++ b/crates/jet-api/src/http/api/authentication.rs @@ -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(); diff --git a/crates/jet-api/src/http/api/authentication/change_password.rs b/crates/jet-api/src/http/api/authentication/change_password.rs new file mode 100644 index 0000000..13219ab --- /dev/null +++ b/crates/jet-api/src/http/api/authentication/change_password.rs @@ -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, + auth: Authenticated, + db: Data, +) -> Result> { + 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, + auth: Authenticated, + t: &mut DatabaseTransaction, +) -> Result> { + 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::::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) { + 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, + 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 = + 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" })); + } +} diff --git a/crates/jet-api/src/http/api/authentication/email_check.rs b/crates/jet-api/src/http/api/authentication/email_check.rs index 3cade35..8ee4d83 100644 --- a/crates/jet-api/src/http/api/authentication/email_check.rs +++ b/crates/jet-api/src/http/api/authentication/email_check.rs @@ -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, diff --git a/crates/jet-api/src/http/api/authentication/magic_generate.rs b/crates/jet-api/src/http/api/authentication/magic_generate.rs index 0b6e750..53dd7cd 100644 --- a/crates/jet-api/src/http/api/authentication/magic_generate.rs +++ b/crates/jet-api/src/http/api/authentication/magic_generate.rs @@ -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 }))) } diff --git a/crates/jet-api/src/http/api/authentication/magic_sign_in.rs b/crates/jet-api/src/http/api/authentication/magic_sign_in.rs index 29eb23e..8608abd 100644 --- a/crates/jet-api/src/http/api/authentication/magic_sign_in.rs +++ b/crates/jet-api/src/http/api/authentication/magic_sign_in.rs @@ -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( diff --git a/crates/jet-api/src/http/api/authentication/reset_password.rs b/crates/jet-api/src/http/api/authentication/reset_password.rs new file mode 100644 index 0000000..d21bffc --- /dev/null +++ b/crates/jet-api/src/http/api/authentication/reset_password.rs @@ -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, + db: Data, + session: Data, + pass_reset_secret: Data, + pass_reset_timeout: Data, +) -> Result> { + 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, + t: &mut DatabaseTransaction, + session: Data, + pass_reset_secret: Data, + pass_reset_timeout: Data, +) -> Result> { + 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) +} diff --git a/crates/jet-api/src/http/api/authentication/sign_in.rs b/crates/jet-api/src/http/api/authentication/sign_in.rs index b29d49a..ebe1d5a 100644 --- a/crates/jet-api/src/http/api/authentication/sign_in.rs +++ b/crates/jet-api/src/http/api/authentication/sign_in.rs @@ -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, diff --git a/crates/jet-api/src/http/api/authentication/sign_out.rs b/crates/jet-api/src/http/api/authentication/sign_out.rs index 3c0b936..3bac1b7 100644 --- a/crates/jet-api/src/http/api/authentication/sign_out.rs +++ b/crates/jet-api/src/http/api/authentication/sign_out.rs @@ -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::*; diff --git a/crates/jet-api/src/http/api/authentication/sign_up.rs b/crates/jet-api/src/http/api/authentication/sign_up.rs index 5cb7647..f7bc3b5 100644 --- a/crates/jet-api/src/http/api/authentication/sign_up.rs +++ b/crates/jet-api/src/http/api/authentication/sign_up.rs @@ -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, diff --git a/crates/jet-api/src/http/api/authentication/social_auth.rs b/crates/jet-api/src/http/api/authentication/social_auth.rs index de500d3..8ae684c 100644 --- a/crates/jet-api/src/http/api/authentication/social_auth.rs +++ b/crates/jet-api/src/http/api/authentication/social_auth.rs @@ -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) => {{ diff --git a/crates/jet-api/src/http/api/authentication/token_refresh.rs b/crates/jet-api/src/http/api/authentication/token_refresh.rs new file mode 100644 index 0000000..e71ea37 --- /dev/null +++ b/crates/jet-api/src/http/api/authentication/token_refresh.rs @@ -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, + session: Data, +) -> Result { + let pair = session + .refresh::(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 }))) +} diff --git a/crates/jet-api/src/http/api/config.rs b/crates/jet-api/src/http/api/config.rs index 3051ae0..2f9ade2 100644 --- a/crates/jet-api/src/http/api/config.rs +++ b/crates/jet-api/src/http/api/config.rs @@ -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); } diff --git a/crates/jet-api/src/http/api/users.rs b/crates/jet-api/src/http/api/users.rs index 74fd9f7..bfc8e83 100644 --- a/crates/jet-api/src/http/api/users.rs +++ b/crates/jet-api/src/http/api/users.rs @@ -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, + session: Authenticated, + db: Data, +) -> Result { + 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, + session: Authenticated, + t: &mut DatabaseTransaction, +) -> Result { + 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" }))) +} diff --git a/crates/jet-api/src/http/api_v1/projects.rs b/crates/jet-api/src/http/api_v1/projects.rs index 81e0080..0c73fc1 100644 --- a/crates/jet-api/src/http/api_v1/projects.rs +++ b/crates/jet-api/src/http/api_v1/projects.rs @@ -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) { diff --git a/crates/jet-api/src/http/api_v1/states.rs b/crates/jet-api/src/http/api_v1/states.rs index 17631ad..f08d1ae 100644 --- a/crates/jet-api/src/http/api_v1/states.rs +++ b/crates/jet-api/src/http/api_v1/states.rs @@ -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) { diff --git a/crates/jet-api/src/main.rs b/crates/jet-api/src/main.rs index b914266..33fd88d 100644 --- a/crates/jet-api/src/main.rs +++ b/crates/jet-api/src/main.rs @@ -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) diff --git a/crates/jet-api/src/models.rs b/crates/jet-api/src/models.rs index b4974af..9c61681 100644 --- a/crates/jet-api/src/models.rs +++ b/crates/jet-api/src/models.rs @@ -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::() + .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(self, d: D) -> JsonErrorDetails { + JsonErrorDetails { + status: self.status, + error: self.error, + details: Some(d), } } } impl actix_web::ResponseError for JsonError { fn error_response(&self) -> HttpResponse { - HttpResponse::BadRequest().json(self) + HttpResponse::build(self.status).json(self) } } @@ -30,7 +71,69 @@ impl From for JsonError { } } -#[derive(Debug, Clone, derive_more::Display)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct JsonErrorDetails
+where + Details: Serialize + Debug, +{ + pub error: String, + #[serde(skip)] + pub status: StatusCode, + #[serde(flatten)] + pub details: Option
, +} + +impl Display for JsonErrorDetails { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&serde_json::to_string(self).unwrap_or_default()) + } +} + +impl JsonErrorDetails { + 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 actix_web::ResponseError for JsonErrorDetails { + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status).json(self) + } +} + +impl From for JsonErrorDetails { + fn from(e: Error) -> Self { + Self::new(e) + } +} + +impl From for JsonErrorDetails { + 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 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)), } } diff --git a/crates/jet-api/src/utils/mod.rs b/crates/jet-api/src/utils/mod.rs index cf637c9..d80276e 100644 --- a/crates/jet-api/src/utils/mod.rs +++ b/crates/jet-api/src/utils/mod.rs @@ -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 { + 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 { + 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 = 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::::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() + ) + } +} diff --git a/crates/jet-contract/src/lib.rs b/crates/jet-contract/src/lib.rs index 6c0534d..afb7600 100644 --- a/crates/jet-contract/src/lib.rs +++ b/crates/jet-contract/src/lib.rs @@ -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; diff --git a/crates/jet-plug/src/lib.rs b/crates/jet-plug/src/lib.rs index 64983fa..8bb03f9 100644 --- a/crates/jet-plug/src/lib.rs +++ b/crates/jet-plug/src/lib.rs @@ -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 //! diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..9413ba7 --- /dev/null +++ b/rustfmt.toml @@ -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"