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]]
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",
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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_aux::field_attributes::deserialize_bool_from_anything;
use serde_aux::serde_introspection::serde_introspect;

View File

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

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

View File

@ -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();

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::web::{Data, Json};
use actix_web::{post, HttpRequest, HttpResponse};
use entities::prelude::{Users, WorkspaceMemberInvites};
use entities::prelude::WorkspaceMemberInvites;
use entities::users::Model as User;
use jet_contract::event_bus::SignInMedium;
use sea_orm::{prelude::*, DatabaseTransaction};
use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter};
use sea_orm::prelude::*;
use sea_orm::{DatabaseConnection, DatabaseTransaction, EntityTrait, QueryFilter};
use serde::{Deserialize, Serialize};
use serde_email::Email;
use super::{AuthError, PublishError};
use crate::config::ApplicationConfig;
use crate::extractors::RequireInstanceConfigured;
use crate::http::magic_link::create_magic_link;
use crate::models::*;
use crate::utils::user_by_email;
use crate::{db_commit, db_rollback, db_t, models::*};
use crate::{EventBusClient, RedisClient};
use crate::{db_commit, db_rollback, db_t, EventBusClient, RedisClient};
#[derive(Debug, Deserialize)]
struct EmailCheckPayload {
@ -138,7 +138,8 @@ async fn handle_existing_user(
}))
}
/// If there's no user for given email address we will register new one and send magic link sign-in
/// If there's no user for given email address we will register new one and send
/// magic link sign-in
async fn register(
req: HttpRequest,
payload: Json<EmailCheckPayload>,

View File

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

View File

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

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_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,

View File

@ -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::*;

View File

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

View File

@ -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) => {{

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::{
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);
}

View File

@ -1,15 +1,20 @@
use crate::extractors::RequireInstanceConfigured;
use crate::session::AppClaims;
use actix_jwt_session::Authenticated;
use actix_web::get;
use actix_web::web::{scope, Data, ServiceConfig};
use actix_web::HttpResponse;
use actix_jwt_session::{Authenticated, Hashing};
use actix_web::web::{scope, Data, Json, ServiceConfig};
use actix_web::{get, post, HttpResponse};
use entities::prelude::Users;
use entities::users::ActiveModel as UserModel;
use sea_orm::prelude::*;
use sea_orm::EntityTrait;
use sea_orm::*;
use serde::Deserialize;
use serde_json::json;
use crate::extractors::RequireInstanceConfigured;
use crate::models::JsonError;
use crate::session::AppClaims;
use crate::{db_commit, db_rollback, db_t};
pub fn configure(config: &mut ServiceConfig) {
config.service(scope("/users").service(retrieve));
config.service(scope("/users").service(retrieve).service(change_password));
}
#[get("/me")]
@ -23,3 +28,53 @@ async fn retrieve(
Err(_error) => HttpResponse::BadRequest().finish(),
}
}
#[derive(Debug, Deserialize)]
struct ChangePassInput {
new_password: String,
}
#[post("/me/change-password")]
async fn change_password(
_: RequireInstanceConfigured,
payload: Json<ChangePassInput>,
session: Authenticated<AppClaims>,
db: Data<DatabaseConnection>,
) -> Result<HttpResponse, JsonError> {
let mut t = db_t!(db)?;
match try_change_password(payload, session, &mut t).await {
Ok(r) => {
db_commit!(t)?;
Ok(r)
}
Err(e) => {
db_rollback!(t).ok();
Err(e)
}
}
}
async fn try_change_password(
payload: Json<ChangePassInput>,
session: Authenticated<AppClaims>,
t: &mut DatabaseTransaction,
) -> Result<HttpResponse, JsonError> {
let mut user: UserModel = match Users::find_by_id(session.account_id()).one(&mut *t).await {
Ok(Some(user)) => user,
Ok(None) => return Err(JsonError::new("User not found")),
Err(_error) => {
return Err(JsonError::new(
"Unable to change password. Please contact administrator",
))
}
}
.into();
user.is_password_autoset = Set(false);
let hash = Hashing::encrypt(payload.new_password.as_str()).map_err(|e| {
tracing::error!("Hashing password failed: {e}");
JsonError::new("Internal error while saving password")
})?;
user.password = Set(hash);
Ok(HttpResponse::Ok().json(json!({ "message": "Password updated successfully" })))
}

View File

@ -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) {

View File

@ -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) {

View File

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

View File

@ -1,26 +1,67 @@
use std::fmt::{Debug, Display};
use actix_web::HttpResponse;
use derive_more::Display;
use derive_more::{Constructor, Deref};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use crate::http::AuthError;
#[derive(Debug, Serialize, Deserialize, Display)]
#[display(fmt = "{{\"error\":\"{error}\"}}")]
#[derive(Debug, Deref, Constructor)]
pub struct PasswordResetSecret(String);
#[derive(Debug, Deref)]
pub struct PasswordResetTimeout(chrono::Duration);
impl PasswordResetTimeout {
pub fn new(s: &str) -> Self {
use std::time::Duration;
let d: Duration = s
.parse::<humantime::Duration>()
.unwrap_or_else(|e| panic!("Invalid duration for password reset timeout: {e}"))
.into();
Self(chrono::Duration::from_std(d).expect("Invalid duration for password reset timeout"))
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct JsonError {
pub error: String,
#[serde(skip)]
pub status: StatusCode,
}
impl std::fmt::Display for JsonError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&serde_json::to_string(self).unwrap_or_default())
}
}
impl JsonError {
pub fn new(error: impl std::fmt::Display) -> Self {
Self {
error: format!("{error}"),
status: StatusCode::BAD_REQUEST,
}
}
pub fn with_status(mut self, status: StatusCode) -> Self {
self.status = status;
self
}
pub fn with_details<D: Serialize + std::fmt::Debug>(self, d: D) -> JsonErrorDetails<D> {
JsonErrorDetails {
status: self.status,
error: self.error,
details: Some(d),
}
}
}
impl actix_web::ResponseError for JsonError {
fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> {
HttpResponse::BadRequest().json(self)
HttpResponse::build(self.status).json(self)
}
}
@ -30,7 +71,69 @@ impl From<Error> for JsonError {
}
}
#[derive(Debug, Clone, derive_more::Display)]
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct JsonErrorDetails<Details>
where
Details: Serialize + Debug,
{
pub error: String,
#[serde(skip)]
pub status: StatusCode,
#[serde(flatten)]
pub details: Option<Details>,
}
impl<D: Serialize + Debug> Display for JsonErrorDetails<D> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&serde_json::to_string(self).unwrap_or_default())
}
}
impl<D: Serialize + Debug> JsonErrorDetails<D> {
pub fn new(error: impl std::fmt::Display) -> Self {
Self {
error: format!("{error}"),
status: StatusCode::BAD_REQUEST,
details: None,
}
}
pub fn with_status(mut self, status: StatusCode) -> Self {
self.status = status;
self
}
pub fn with_details(mut self, d: D) -> Self {
self.details = Some(d);
self
}
}
impl<D: Serialize + Debug> actix_web::ResponseError for JsonErrorDetails<D> {
fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> {
HttpResponse::build(self.status).json(self)
}
}
impl<D: Serialize + Debug> From<Error> for JsonErrorDetails<D> {
fn from(e: Error) -> Self {
Self::new(e)
}
}
impl<D: Serialize + Debug> From<JsonError> for JsonErrorDetails<D> {
fn from(value: JsonError) -> Self {
let JsonError { error, status } = value;
Self {
error,
status,
details: None,
}
}
}
#[derive(Debug, Clone, PartialEq, derive_more::Display)]
#[cfg_attr(test, derive(serde::Deserialize))]
pub enum Error {
#[display(fmt = "Database connection error")]
DatabaseError,
@ -52,6 +155,10 @@ pub enum Error {
ContactSupport,
#[display(fmt = "User not found")]
UserRequired,
#[display(
fmt = "Password couldn't be encrypted. Please try again later or contact the support team."
)]
EncryptPass,
}
impl From<AuthError> for Error {
@ -66,6 +173,7 @@ impl actix_web::ResponseError for Error {
Self::DatabaseError | Self::NoHost => {
HttpResponse::InternalServerError().json(JsonError::new(self))
}
Self::UserRequired => HttpResponse::Unauthorized().json(JsonError::new(self)),
_ => HttpResponse::BadRequest().json(JsonError::new(self)),
}
}

View File

@ -1,28 +1,26 @@
use actix_web::web::Data;
use actix_jwt_session::Hashing;
use actix_web::HttpRequest;
use chrono::Utc;
use entities::prelude::{
ProjectMemberInvites, ProjectMembers, WorkspaceMemberInvites, WorkspaceMembers,
};
use entities::project_member_invites::{
Column as ProjectMemberInviteColumn, Model as ProjectMemberInvite,
};
use entities::project_members::ActiveModel as ProjectMemberModel;
use entities::sea_orm_active_enums::{ProjectMemberRoles, Roles};
use entities::workspace_member_invites::{
Column as WorkspaceMemberInviteColumn, Model as WorkspaceMemberInvite,
};
use entities::workspace_members::ActiveModel as WorkspaceMemberModel;
use entities::{
prelude::{ProjectMemberInvites, ProjectMembers, WorkspaceMemberInvites, WorkspaceMembers},
sea_orm_active_enums::Roles,
};
use entities::{
project_members::ActiveModel as ProjectMemberModel, sea_orm_active_enums::ProjectMemberRoles,
};
use reqwest::header::USER_AGENT;
use sea_orm::sea_query::OnConflict;
use sea_orm::*;
use tracing::error;
use uuid::Uuid;
use crate::http::OAuthError;
use crate::{http::AuthError, models::Error};
use crate::http::{AuthError, OAuthError};
use crate::models::Error;
#[macro_export]
macro_rules! db_t {
@ -38,13 +36,23 @@ macro_rules! db_t {
#[macro_export]
macro_rules! db_commit {
($db: expr) => {{
$db.commit().await.map_err(|e| {
let res = if cfg!(test) {
$db.rollback().await
} else {
$db.commit().await
};
res.map_err(|e| {
tracing::error!("Failed to commit db tracation: {e}");
crate::models::Error::DatabaseError
})
}};
($db: expr, $msg: expr) => {{
$db.commit().await.map_err(|e| {
let res = if cfg!(test) {
$db.rollback().await
} else {
$db.commit().await
};
res.map_err(|e| {
tracing::error!(std::concat!($msg, ": {}"), e);
crate::models::Error::DatabaseError
})
@ -75,6 +83,25 @@ macro_rules! redis_c {
};
}
pub trait SetPassword {
fn set_password(&mut self, password: &str) -> Result<(), password_hash::Error>;
}
impl SetPassword for entities::users::ActiveModel {
fn set_password(&mut self, password: &str) -> Result<(), password_hash::Error> {
self.password = Set(encrypt_password(password)?);
Ok(())
}
}
pub fn encrypt_password(pass: &str) -> Result<String, password_hash::Error> {
Hashing::encrypt(pass)
}
pub fn verify_password(password_hash: &str, password: &str) -> Result<(), password_hash::Error> {
Hashing::verify(password_hash, password)
}
pub async fn user_by_email(
email: &str,
db: &mut DatabaseTransaction,
@ -265,3 +292,75 @@ pub async fn invites_to_membership(
};
Ok(())
}
pub mod uidb {
use base64::prelude::*;
use reqwest::StatusCode;
use uuid::Uuid;
use crate::models::JsonError;
pub fn decode(uidb: &str) -> Result<Uuid, JsonError> {
let Ok(bytes) = BASE64_URL_SAFE.decode(uidb) else {
return Err(JsonError::new("Token is invalid").with_status(StatusCode::UNAUTHORIZED));
};
Uuid::from_slice(&bytes)
.map_err(|_| JsonError::new("Token is invalid").with_status(StatusCode::UNAUTHORIZED))
}
pub fn encode(uuid: Uuid) -> String {
BASE64_URL_SAFE.encode(uuid.as_bytes())
}
}
pub mod pass_reset_token {
use chrono::{Duration, NaiveDateTime, Utc};
use entities::users::Model as User;
use hmac::*;
use sha2::Sha256;
use tracing::warn;
pub fn check(user: &User, token: &str, secret: &str, password_reset_timeout: Duration) -> bool {
let Some((ts_b36, _)) = token.split_once('-') else {
return false;
};
let Some(ts): Option<u64> = basen::BASE36.decode_var_len(&ts_b36) else {
return false;
};
let valid_token = make_token_with_timestamp(ts as i64, user, secret);
if valid_token != token {
return false;
}
let Some(timestamp) = NaiveDateTime::from_timestamp_millis(ts as i64) else {
warn!("Invalid timestamp in token. Not a milliseconds");
return false;
};
chrono::Utc::now().naive_utc() - timestamp < password_reset_timeout
}
pub fn make_token(user: &User, secret: &str) -> String {
make_token_with_timestamp(Utc::now().naive_utc().timestamp_millis(), user, secret)
}
fn make_token_with_timestamp(timestamp: i64, user: &User, secret: &str) -> String {
let ts_b36 = basen::BASE36.encode_var_len(&(timestamp as u64));
let hash_value = user_to_hash_value(timestamp, user);
let mut mac =
Hmac::<Sha256>::new_from_slice(secret.as_bytes()).expect("Invalid hmac secret");
mac.update(hash_value.as_bytes());
let result = mac.finalize();
let s = String::from_utf8(result.into_bytes()[..].to_vec()).unwrap();
format!("{ts_b36}-{s}")
}
fn user_to_hash_value(timestamp: i64, user: &User) -> String {
format!(
"{pk}{pass}{login_timestamp}{timestamp}{email}",
pk = user.id,
pass = user.password,
login_timestamp = user.last_login_time.unwrap_or_default(),
email = user.email.as_deref().unwrap_or_default()
)
}
}

View File

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

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
//!

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"