From 96881816b2f55ef0cfd6c8f4030e73bc1037ff40 Mon Sep 17 00:00:00 2001 From: Micah Thomas Date: Tue, 27 Feb 2024 10:41:26 -0700 Subject: [PATCH 1/9] feature: switch to axum --- Cargo.lock | 736 +++++++++-------------- apps/server/Cargo.toml | 17 +- apps/server/src/authentication.rs | 162 ++--- apps/server/src/bigcommerce/client.rs | 4 +- apps/server/src/configuration.rs | 38 +- apps/server/src/data.rs | 7 +- apps/server/src/liq_pay.rs | 1 + apps/server/src/main.rs | 2 +- apps/server/src/routes/bigcommerce.rs | 106 ++-- apps/server/src/routes/mod.rs | 20 +- apps/server/src/routes/pay.rs | 36 +- apps/server/src/routes/widget.rs | 179 +++--- apps/server/src/startup.rs | 86 +-- apps/server/tests/e2e/bigcommerce/mod.rs | 4 +- apps/server/tests/e2e/helpers.rs | 11 +- 15 files changed, 617 insertions(+), 792 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e3c618..d6dce7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,215 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags 2.4.2", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-cors" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0346d8c1f762b41b458ed3145eea914966bb9ad20b9be0d6d463b20d45586370" -dependencies = [ - "actix-utils", - "actix-web", - "derive_more", - "futures-util", - "log", - "once_cell", - "smallvec", -] - -[[package]] -name = "actix-http" -version = "3.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d223b13fd481fc0d1f83bb12659ae774d9e3601814c68a0bc539731698cca743" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "ahash 0.8.9", - "base64 0.21.7", - "bitflags 2.4.2", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "futures-core", - "h2", - "http", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding 2.3.1", - "pin-project-lite", - "rand 0.8.5", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn 2.0.50", -] - -[[package]] -name = "actix-router" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22475596539443685426b6bdadb926ad0ecaefdfc5fb05e5e3441f15463c511" -dependencies = [ - "bytestring", - "http", - "regex", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb13e7eef0423ea6eab0e59f6c72e7cb46d33691ad56a726b3cd07ddec2c2d4" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" -dependencies = [ - "futures-core", - "paste", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a6556ddebb638c2358714d853257ed226ece6023ef9364f23f0c70737ea984" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "ahash 0.8.9", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "futures-core", - "futures-util", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2", - "time", - "url 2.5.0", -] - -[[package]] -name = "actix-web-codegen" -version = "4.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1f50ebbb30eca122b188319a4398b3f7bb4a8cdf50ecfb73bfc6a3c3ce54f5" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 2.0.50", -] - -[[package]] -name = "actix-web-httpauth" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d613edf08a42ccc6864c941d30fe14e1b676a77d16f1dbadc1174d065a0a775" -dependencies = [ - "actix-utils", - "actix-web", - "base64 0.21.7", - "futures-core", - "futures-util", - "log", - "pin-project-lite", -] - [[package]] name = "addr2line" version = "0.21.0" @@ -259,21 +50,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "allocator-api2" version = "0.2.16" @@ -365,13 +141,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.3.4", "bitflags 1.3.2", "bytes", "futures-util", - "http", - "http-body", - "hyper", + "http 0.2.11", + "http-body 0.4.6", + "hyper 0.14.28", "itoa", "matchit", "memchr", @@ -386,6 +162,41 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" +dependencies = [ + "async-trait", + "axum-core 0.4.3", + "axum-macros", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.2.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding 2.3.1", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-core" version = "0.3.4" @@ -395,14 +206,87 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 0.2.11", + "http-body 0.4.6", "mime", "rustversion", "tower-layer", "tower-service", ] +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "895ff42f72016617773af68fb90da2a9677d89c62338ec09162d4909d86fdd8f" +dependencies = [ + "axum 0.7.4", + "axum-core 0.4.3", + "bytes", + "futures-util", + "headers", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.50", +] + +[[package]] +name = "axum-tracing-opentelemetry" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91387a3e9f6aa45f112cd05d1e5430f9ba40b51440849e4760a5dd51b736149f" +dependencies = [ + "axum 0.7.4", + "futures-core", + "futures-util", + "http 1.0.0", + "opentelemetry", + "pin-project-lite", + "tower", + "tracing", + "tracing-opentelemetry", + "tracing-opentelemetry-instrumentation-sdk", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -460,27 +344,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "brotli" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bumpalo" version = "3.15.3" @@ -499,23 +362,11 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" -[[package]] -name = "bytestring" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" -dependencies = [ - "bytes", -] - [[package]] name = "cc" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f9fa1897e4325be0d68d48df6aa1a71ac2ed4d27723887e7754192705350730" -dependencies = [ - "libc", -] [[package]] name = "cfg-if" @@ -579,23 +430,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding 2.3.1", - "time", - "version_check", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -636,15 +470,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" -[[package]] -name = "crc32fast" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" -dependencies = [ - "cfg-if", -] - [[package]] name = "crossbeam-channel" version = "0.5.11" @@ -754,19 +579,6 @@ dependencies = [ "serde", ] -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 1.0.109", -] - [[package]] name = "digest" version = "0.10.7" @@ -882,16 +694,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" -[[package]] -name = "flate2" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "flume" version = "0.11.0" @@ -1118,8 +920,8 @@ checksum = "34c72c9baded4d06742eaaa5def6158f9e28d20a679ad1d5f5deb2bae8358052" dependencies = [ "base64 0.13.1", "chrono", - "http", - "hyper", + "http 0.2.11", + "hyper 0.14.28", "itertools 0.10.5", "mime", "serde", @@ -1139,8 +941,8 @@ checksum = "380e1cd4fe2c975c3b79c6e6548abf06ce67968d6faf3e19111697cdebf6dead" dependencies = [ "anyhow", "google-apis-common", - "http", - "hyper", + "http 0.2.11", + "hyper 0.14.28", "hyper-rustls", "itertools 0.10.5", "mime", @@ -1162,7 +964,26 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.11", + "indexmap 2.2.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 1.0.0", "indexmap 2.2.3", "slab", "tokio", @@ -1198,6 +1019,30 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http 1.0.0", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.0.0", +] + [[package]] name = "heck" version = "0.4.1" @@ -1257,6 +1102,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -1264,7 +1120,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.11", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.0.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", "pin-project-lite", ] @@ -1278,7 +1157,7 @@ dependencies = [ "async-channel", "base64 0.13.1", "futures-lite", - "http", + "http 0.2.11", "infer", "pin-project-lite", "rand 0.7.3", @@ -1311,9 +1190,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.24", + "http 0.2.11", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -1325,6 +1204,26 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.2", + "http 1.0.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + [[package]] name = "hyper-rustls" version = "0.24.2" @@ -1332,8 +1231,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", - "hyper", + "http 0.2.11", + "hyper 0.14.28", "log", "rustls 0.21.10", "rustls-native-certs", @@ -1347,12 +1246,28 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper", + "hyper 0.14.28", "pin-project-lite", "tokio", "tokio-io-timeout", ] +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "hyper 1.2.0", + "pin-project-lite", + "socket2", + "tokio", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -1504,12 +1419,6 @@ dependencies = [ "simple_asn1", ] -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - [[package]] name = "lazy_static" version = "1.4.0" @@ -1554,23 +1463,6 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - [[package]] name = "lock_api" version = "0.4.11" @@ -1652,17 +1544,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] -[[package]] -name = "mutually_exclusive_features" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d02c0b00610773bb7fc61d85e13d86c7858cbdf00e1a120bfc41bc055dbaa0e" - [[package]] name = "nias" version = "0.5.0" @@ -1820,19 +1705,6 @@ dependencies = [ "urlencoding", ] -[[package]] -name = "opentelemetry-http" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f51189ce8be654f9b5f7e70e49967ed894e84a06fc35c6c042e64ac1fc5399e" -dependencies = [ - "async-trait", - "bytes", - "http", - "opentelemetry", - "reqwest", -] - [[package]] name = "opentelemetry-otlp" version = "0.14.0" @@ -1841,14 +1713,12 @@ checksum = "f24cda83b20ed2433c68241f918d0f6fdec8b1d43b7a9590ab4420c5095ca930" dependencies = [ "async-trait", "futures-core", - "http", + "http 0.2.11", "opentelemetry", - "opentelemetry-http", "opentelemetry-proto", "opentelemetry-semantic-conventions", "opentelemetry_sdk", "prost", - "reqwest", "thiserror", "tokio", "tonic", @@ -2292,10 +2162,10 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.24", + "http 0.2.11", + "http-body 0.4.6", + "hyper 0.14.28", "hyper-rustls", "ipnet", "js-sys", @@ -2650,6 +2520,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_qs" version = "0.8.5" @@ -2732,15 +2612,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - [[package]] name = "signature" version = "2.2.0" @@ -3059,12 +2930,11 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" name = "swu-app" version = "0.1.0" dependencies = [ - "actix-cors", - "actix-utils", - "actix-web", - "actix-web-httpauth", "anyhow", "assert-json-diff", + "axum 0.7.4", + "axum-extra", + "axum-tracing-opentelemetry", "base64 0.21.7", "config", "dotenvy", @@ -3086,8 +2956,8 @@ dependencies = [ "thiserror", "time", "tokio", + "tower-http", "tracing", - "tracing-actix-web", "tracing-bunyan-formatter", "tracing-opentelemetry", "tracing-subscriber", @@ -3263,9 +3133,7 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.48.0", @@ -3343,15 +3211,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" dependencies = [ "async-trait", - "axum", + "axum 0.6.20", "base64 0.21.7", "bytes", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.24", + "http 0.2.11", + "http-body 0.4.6", + "hyper 0.14.28", "hyper-timeout", "percent-encoding 2.3.1", "pin-project", @@ -3384,6 +3252,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.4.2", + "bytes", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.2" @@ -3408,19 +3293,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-actix-web" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe0d5feac3f4ca21ba33496bcb1ccab58cca6412b1405ae80f0581541e0ca78" -dependencies = [ - "actix-web", - "mutually_exclusive_features", - "pin-project", - "tracing", - "uuid", -] - [[package]] name = "tracing-attributes" version = "0.1.27" @@ -3500,6 +3372,18 @@ dependencies = [ "web-time", ] +[[package]] +name = "tracing-opentelemetry-instrumentation-sdk" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9db99a4f5224920c499515a737e2749eb9a19b729b3880afc24594524e9861de" +dependencies = [ + "http 1.0.0", + "opentelemetry", + "tracing", + "tracing-opentelemetry", +] + [[package]] name = "tracing-subscriber" version = "0.3.18" @@ -3948,7 +3832,7 @@ dependencies = [ "futures", "futures-timer", "http-types", - "hyper", + "hyper 0.14.28", "log", "once_cell", "regex", @@ -3976,8 +3860,8 @@ dependencies = [ "async-trait", "base64 0.21.7", "futures", - "http", - "hyper", + "http 0.2.11", + "hyper 0.14.28", "hyper-rustls", "itertools 0.12.1", "log", @@ -4018,31 +3902,3 @@ name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" - -[[package]] -name = "zstd" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml index 6b7768c..b432e1e 100644 --- a/apps/server/Cargo.toml +++ b/apps/server/Cargo.toml @@ -11,10 +11,6 @@ path = "src/main.rs" name = "swu-app" [dependencies] -actix-cors = "0.6.4" -actix-utils = "3.0.1" -actix-web = "4.4.0" -actix-web-httpauth = "0.8.1" anyhow = "1.0.79" base64 = "0.21.4" config = "0.13.3" @@ -28,7 +24,6 @@ serde-aux = { version = "4.2.0", default-features = false } thiserror = "1.0.50" time = { version = "0.3.32", features = ["std"] } tracing = { version = "0.1.40", features = ["log"] } -tracing-actix-web = "0.7.9" tracing-bunyan-formatter = "0.3.9" tracing-opentelemetry = "0.22.0" tracing-subscriber = { version = "0.3.17", features = [ @@ -40,8 +35,18 @@ dotenvy = "0.15.7" email_address = "0.2.4" sha1 = "0.10.5" assert-json-diff = "2.0.2" -opentelemetry-otlp = { version = "0.14.0", features = ["tonic", "grpc-tonic", "http-proto", "reqwest-client"] } +opentelemetry-otlp = { version = "0.14.0" } opentelemetry_sdk = { version = "0.21.2", features = ["rt-tokio"] } +axum = { version = "0.7.4", features = [ + "default", + "tokio", + "tracing", + "macros", + "json", +] } +tower-http = { version = "0.5.2", features = ["cors", "trace"] } +axum-extra = { version = "0.9.2", features = ["typed-header"] } +axum-tracing-opentelemetry = "0.17.1" [dependencies.reqwest] version = "0.11.24" diff --git a/apps/server/src/authentication.rs b/apps/server/src/authentication.rs index 5e03aee..6ebfc90 100644 --- a/apps/server/src/authentication.rs +++ b/apps/server/src/authentication.rs @@ -1,14 +1,21 @@ -use actix_utils::future::{ready, Ready}; -use actix_web::{ - dev::Payload, error::ParseError, http::StatusCode, web, FromRequest, HttpRequest, HttpResponse, - ResponseError, +use anyhow::Context; +use axum::RequestPartsExt; +use axum::{ + async_trait, + extract::FromRequestParts, + http::{request::Parts, StatusCode}, + response::{IntoResponse, Response}, + Extension, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, }; -use actix_web_httpauth::headers::authorization; use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; use secrecy::{ExposeSecret, Secret}; use time::{Duration, OffsetDateTime}; -use crate::configuration::JWTSecret; +use crate::startup::AppState; #[derive(serde::Deserialize, serde::Serialize, Debug)] pub struct AuthClaims { @@ -17,35 +24,47 @@ pub struct AuthClaims { pub exp: i64, } -impl FromRequest for AuthClaims { - type Future = Ready>; - type Error = Error; +#[async_trait] +impl FromRequestParts for AuthClaims +where + S: Send + Sync, +{ + type Rejection = Error; - fn from_request(req: &HttpRequest, _payload: &mut Payload) -> ::Future { - let Some(jwt_secret) = req.app_data::>() else { - return ready(Err(Error::InvalidServerConfiguration)); - }; + #[tracing::instrument(name = "decode auth from request", skip(parts, _state))] + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + // Extract the token from the authorization header + let TypedHeader(Authorization(bearer)) = parts + .extract::>>() + .await + .map_err(|_| Error::NoToken)?; - let bearer = match as actix_web::http::header::Header>::parse(req) { - Ok(auth) => auth.into_scheme(), - Err(err) => { - return ready(Err(Error::NoTokenProvided(err))) - } - }; + let Extension(AppState { jwt_secret, .. }) = parts + .extract::>() + .await + .context("extract state")?; - let claims = match decode_token(bearer.token(), jwt_secret.as_ref()) { - Ok(claims) => claims, - Err(err) => return ready(Err(err)), - }; + decode_token(bearer.token(), jwt_secret) + } +} - ready(Ok(claims)) +impl IntoResponse for Error { + fn into_response(self) -> Response { + match self { + Self::InvalidServerConfiguration => { + (StatusCode::INTERNAL_SERVER_ERROR, "Unknown error") + } + Self::Unexpected(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Unknown error"), + Self::InvalidToken(_) | Self::NoToken => (StatusCode::BAD_REQUEST, "Invalid token"), + } + .into_response() } } -pub fn create_jwt(store_hash: &str, secret: S) -> Result -where - S: AsRef>, -{ +pub fn create_jwt( + store_hash: &str, + secret: &Secret, +) -> Result { let expiration = OffsetDateTime::now_utc() + Duration::days(1); let claims = AuthClaims { @@ -54,64 +73,46 @@ where exp: expiration.unix_timestamp(), }; let header = Header::new(Algorithm::HS512); - let key = EncodingKey::from_secret(secret.as_ref().expose_secret().as_bytes()); + let key = EncodingKey::from_secret(secret.expose_secret().as_bytes()); encode(&header, &claims, &key) } pub struct AuthorizedUser(pub String); -pub fn decode_token(token: &str, secret: S) -> Result -where - S: AsRef>, -{ - let key = DecodingKey::from_secret(secret.as_ref().expose_secret().as_bytes()); +#[tracing::instrument(name = "decode token")] +pub fn decode_token(token: &str, secret: Secret) -> Result { + let key = DecodingKey::from_secret(secret.expose_secret().as_bytes()); let validation = Validation::new(Algorithm::HS512); - let decoded = - decode::(token, &key, &validation).map_err(Error::InvalidTokenError)?; + let decoded = decode::(token, &key, &validation).map_err(Error::InvalidToken)?; Ok(decoded.claims) } #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("No Bearer Token")] - NoTokenProvided(#[source] ParseError), + #[error("Token is not provided.")] + NoToken, #[error("Token is invalid.")] - InvalidTokenError(#[source] jsonwebtoken::errors::Error), + InvalidToken(#[source] jsonwebtoken::errors::Error), #[error("Server Configuration Invalid")] InvalidServerConfiguration, #[error(transparent)] - UnexpectedError(#[from] anyhow::Error), -} - -impl ResponseError for Error { - fn error_response(&self) -> HttpResponse { - match self { - Self::InvalidServerConfiguration => { - HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR) - } - Self::InvalidTokenError(_) | Self::NoTokenProvided(_) => { - HttpResponse::new(StatusCode::UNAUTHORIZED) - } - Self::UnexpectedError(_) => HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR), - } - } + Unexpected(#[from] anyhow::Error), } #[cfg(test)] mod tests { use super::*; - use actix_web::{http::header::AUTHORIZATION, test::TestRequest, web::Data}; #[test] fn should_encode_and_decode_jwt_in_correct_format() { let store_hash = "test_store"; - let secret = JWTSecret(Secret::from("abcdefg".to_owned())); - let token = create_jwt(store_hash, secret).unwrap(); + let secret = Secret::from("abcdefg".to_owned()); + let token = create_jwt(store_hash, &secret).unwrap(); let parts: Vec<&str> = token.splitn(3, '.').collect(); @@ -119,7 +120,7 @@ mod tests { assert!(!parts[1].is_empty()); assert!(!parts[2].is_empty()); - let secret = JWTSecret(Secret::from("abcdefg".to_owned())); + let secret = Secret::from("abcdefg".to_owned()); let claims = decode_token(token.as_str(), secret).unwrap(); assert_eq!("test_store", claims.sub); @@ -128,49 +129,4 @@ mod tests { "Expiration should be more than 30 mins" ) } - - #[actix_web::test] - async fn auth_claims_extractor_fails_without_jwt_secret() { - let req = TestRequest::default() - .insert_header((AUTHORIZATION, "test-token")) - .to_http_request(); - let mut payload = Payload::None; - - let err = AuthClaims::from_request(&req, &mut payload) - .await - .unwrap_err(); - - assert_eq!(err.to_string(), "Server Configuration Invalid"); - } - - #[actix_web::test] - async fn auth_claims_extractor_fails_with_no_header() { - let jwt_secret = Data::new(JWTSecret(Secret::from("test-token".to_owned()))); - let req = TestRequest::default() - .app_data(jwt_secret.clone()) - .to_http_request(); - let mut payload = Payload::None; - - let err = AuthClaims::from_request(&req, &mut payload) - .await - .unwrap_err(); - - assert_eq!(err.to_string(), "No Bearer Token"); - } - - #[actix_web::test] - async fn auth_claims_extractor_fails_with_bad_token() { - let jwt_secret = Data::new(JWTSecret(Secret::from("test-secret".to_owned()))); - let req = TestRequest::default() - .app_data(jwt_secret.clone()) - .insert_header((AUTHORIZATION, "Bearer test-token")) - .to_http_request(); - let mut payload = Payload::None; - - let err = AuthClaims::from_request(&req, &mut payload) - .await - .unwrap_err(); - - assert_eq!(err.to_string(), "Token is invalid."); - } } diff --git a/apps/server/src/bigcommerce/client.rs b/apps/server/src/bigcommerce/client.rs index 565b5e8..e27a33e 100644 --- a/apps/server/src/bigcommerce/client.rs +++ b/apps/server/src/bigcommerce/client.rs @@ -12,6 +12,7 @@ use super::{ store::{APIToken, Information}, }; +#[derive(Clone)] pub struct HttpAPI { api_base_url: String, login_base_url: String, @@ -179,8 +180,7 @@ impl HttpAPI { pub fn decode_jwt(&self, token: &str) -> Result { let key = DecodingKey::from_secret(self.client_secret.expose_secret().as_bytes()); let validation = Validation::new(Algorithm::HS256); - let decoded = - decode::(token, &key, &validation).map_err(Error::InvalidTokenError)?; + let decoded = decode::(token, &key, &validation).map_err(Error::InvalidToken)?; Ok(decoded.claims) } diff --git a/apps/server/src/configuration.rs b/apps/server/src/configuration.rs index fa9df96..5507cf7 100644 --- a/apps/server/src/configuration.rs +++ b/apps/server/src/configuration.rs @@ -7,6 +7,12 @@ use sqlx::{ ConnectOptions, }; +use crate::{ + bigcommerce::client::HttpAPI as BigCommerceHttpAPI, + liq_pay::HttpAPI as LiqPayHttpAPI, + startup::{get_connection_pool, AppState}, +}; + #[derive(serde::Deserialize, Clone)] pub struct Configuration { pub database: Database, @@ -110,6 +116,30 @@ impl Configuration { .build()? .try_deserialize() } + + pub fn get_app_state(&self) -> AppState { + let db_pool = get_connection_pool(&self.database); + let bigcommerce_client = BigCommerceHttpAPI::new( + self.bigcommerce.api_base_url.clone(), + self.bigcommerce.login_base_url.clone(), + self.bigcommerce.client_id.clone(), + self.bigcommerce.client_secret.clone(), + self.bigcommerce.install_redirect_uri.clone(), + std::time::Duration::from_millis(self.bigcommerce.timeout.into()), + ); + let liq_pay_client = LiqPayHttpAPI::new( + self.liq_pay.public_key.clone(), + self.liq_pay.private_key.clone(), + ); + + AppState { + db_pool, + base_url: self.application.base_url.clone(), + jwt_secret: self.application.jwt_secret.clone(), + bigcommerce_client, + liq_pay_client, + } + } } pub struct BaseURL(pub String); @@ -120,14 +150,6 @@ impl std::fmt::Display for BaseURL { } } -pub struct JWTSecret(pub Secret); - -impl AsRef> for JWTSecret { - fn as_ref(&self) -> &Secret { - &self.0 - } -} - pub struct LightstepAccessToken(pub Secret); impl AsRef> for LightstepAccessToken { diff --git a/apps/server/src/data.rs b/apps/server/src/data.rs index 739979d..2bc1b69 100644 --- a/apps/server/src/data.rs +++ b/apps/server/src/data.rs @@ -6,10 +6,7 @@ use secrecy::Secret; use sqlx::{types::time::OffsetDateTime, PgPool}; use uuid::Uuid; -use crate::{ - bigcommerce::{script::Script, store::APIToken}, - configuration::BaseURL, -}; +use crate::bigcommerce::{script::Script, store::APIToken}; #[tracing::instrument(name = "Write store credentials to database", skip(store, pool))] pub async fn write_store_credentials(store: &APIToken, pool: &PgPool) -> Result<(), sqlx::Error> { @@ -163,7 +160,7 @@ impl WidgetConfiguration { pub fn generate_script( &self, store_hash: &str, - base_url: &BaseURL, + base_url: &str, ) -> Result { Ok(Script::new( "Stand With Ukraine".to_owned(), diff --git a/apps/server/src/liq_pay.rs b/apps/server/src/liq_pay.rs index 5954fc2..5b7c932 100644 --- a/apps/server/src/liq_pay.rs +++ b/apps/server/src/liq_pay.rs @@ -42,6 +42,7 @@ pub struct InputQuery { pub action: Action, } +#[derive(Clone)] pub struct HttpAPI { public_key: Secret, private_key: Secret, diff --git a/apps/server/src/main.rs b/apps/server/src/main.rs index 304af2c..f7a52de 100644 --- a/apps/server/src/main.rs +++ b/apps/server/src/main.rs @@ -8,7 +8,7 @@ async fn main() -> std::io::Result<()> { let configuration = Configuration::generate_from_environment().expect("Failed to read configuration."); - let application = Application::build(configuration)?; + let application = Application::build(configuration).await?; application.run_until_stopped().await?; diff --git a/apps/server/src/routes/bigcommerce.rs b/apps/server/src/routes/bigcommerce.rs index 23aa819..d50f1b9 100644 --- a/apps/server/src/routes/bigcommerce.rs +++ b/apps/server/src/routes/bigcommerce.rs @@ -1,22 +1,23 @@ -use actix_web::{http::StatusCode, web, HttpResponse, ResponseError}; use anyhow::Context; -use reqwest::header::LOCATION; -use sqlx::PgPool; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::{IntoResponse, Redirect, Response}, + routing::get, + Router, +}; use crate::{ authentication::{create_jwt, Error}, - bigcommerce::client::HttpAPI, - configuration::{BaseURL, JWTSecret}, data::{write_store_as_uninstalled, write_store_credentials}, + startup::AppState, }; -pub fn register_routes(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/bigcommerce") - .route("/install", web::get().to(install)) - .route("/uninstall", web::get().to(uninstall)) - .route("/load", web::get().to(load)), - ); +pub fn router() -> Router { + Router::new() + .route("/install", get(install)) + .route("/uninstall", get(uninstall)) + .route("/load", get(load)) } #[derive(serde::Deserialize)] @@ -32,10 +33,10 @@ enum InstallError { UnexpectedError(#[from] anyhow::Error), } -impl ResponseError for InstallError { - fn error_response(&self) -> HttpResponse { +impl IntoResponse for InstallError { + fn into_response(self) -> axum::response::Response { match self { - Self::UnexpectedError(_) => HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR), + Self::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } } @@ -46,12 +47,15 @@ impl ResponseError for InstallError { fields(context=tracing::field::Empty, user_email=tracing::field::Empty) )] async fn install( - query: web::Query, - bigcommerce_client: web::Data, - base_url: web::Data, - db_pool: web::Data, - jwt_secret: web::Data, -) -> Result { + Query(query): Query, + State(AppState { + bigcommerce_client, + db_pool, + jwt_secret, + base_url, + .. + }): State, +) -> Result { tracing::Span::current().record("context", &tracing::field::display(&query.context)); let oauth_credentials = bigcommerce_client @@ -74,16 +78,16 @@ async fn install( .context("Failed to store credentials in database") .map_err(InstallError::UnexpectedError)?; - let jwt = create_jwt(store.get_store_hash(), jwt_secret.get_ref()) + let jwt = create_jwt(store.get_store_hash(), &jwt_secret) .context("Failed to encode jwt token") .map_err(InstallError::UnexpectedError)?; - Ok(HttpResponse::Found() - .append_header(( - LOCATION, - generate_dashboard_url(&base_url.0, &jwt, store.get_store_hash()), - )) - .finish()) + Ok(Redirect::to(&generate_dashboard_url( + &base_url, + &jwt, + store.get_store_hash(), + )) + .into_response()) } #[derive(serde::Deserialize)] @@ -102,14 +106,13 @@ enum LoadError { UnexpectedError(#[from] anyhow::Error), } -impl ResponseError for LoadError { - fn error_response(&self) -> HttpResponse { +impl IntoResponse for LoadError { + fn into_response(self) -> Response { match self { - Self::NotStoreOwnerError | Self::InvalidCredentials(_) => { - HttpResponse::new(StatusCode::UNAUTHORIZED) - } - Self::UnexpectedError(_) => HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR), + Self::NotStoreOwnerError | Self::InvalidCredentials(_) => StatusCode::UNAUTHORIZED, + Self::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR, } + .into_response() } } @@ -118,11 +121,14 @@ impl ResponseError for LoadError { skip(query, bigcommerce_client, base_url, jwt_secret) )] async fn load( - query: web::Query, - bigcommerce_client: web::Data, - base_url: web::Data, - jwt_secret: web::Data, -) -> Result { + Query(query): Query, + State(AppState { + bigcommerce_client, + base_url, + jwt_secret, + .. + }): State, +) -> Result { let claims = bigcommerce_client .decode_jwt(&query.signed_payload_jwt) .map_err(LoadError::InvalidCredentials)?; @@ -131,16 +137,11 @@ async fn load( .get_store_hash() .map_err(LoadError::UnexpectedError)?; - let jwt = create_jwt(store_hash, jwt_secret.as_ref()) + let jwt = create_jwt(store_hash, &jwt_secret) .context("Failed to encode token") .map_err(LoadError::UnexpectedError)?; - Ok(HttpResponse::Found() - .append_header(( - LOCATION, - generate_dashboard_url(&base_url.0, &jwt, store_hash), - )) - .finish()) + Ok(Redirect::to(&generate_dashboard_url(&base_url, &jwt, store_hash)).into_response()) } #[tracing::instrument( @@ -148,10 +149,13 @@ async fn load( skip(query, bigcommerce_client, db_pool) )] async fn uninstall( - query: web::Query, - bigcommerce_client: web::Data, - db_pool: web::Data, -) -> Result { + Query(query): Query, + State(AppState { + bigcommerce_client, + db_pool, + .. + }): State, +) -> Result { let claims = bigcommerce_client .decode_jwt(&query.signed_payload_jwt) .map_err(LoadError::InvalidCredentials)?; @@ -169,7 +173,7 @@ async fn uninstall( .context("Failed to set store as uninstalled") .map_err(LoadError::UnexpectedError)?; - Ok(HttpResponse::Ok().finish()) + Ok(StatusCode::OK.into_response()) } fn generate_dashboard_url(base_url: &str, token: &str, store_hash: &str) -> String { diff --git a/apps/server/src/routes/mod.rs b/apps/server/src/routes/mod.rs index 26600aa..3539b3f 100644 --- a/apps/server/src/routes/mod.rs +++ b/apps/server/src/routes/mod.rs @@ -1,17 +1,19 @@ -use actix_web::{web, HttpResponse}; +use axum::{response::Response, routing::get, Router}; + +use crate::startup::AppState; mod bigcommerce; mod pay; mod widget; -pub async fn health_check() -> HttpResponse { - HttpResponse::Ok().finish() +pub async fn health_check() -> Response { + Response::new("".into()) } -pub fn register(cfg: &mut web::ServiceConfig) { - cfg.service(web::resource("/health_check").route(web::get().to(health_check))); - - pay::register_routes(cfg); - bigcommerce::register_routes(cfg); - widget::register_routes(cfg); +pub fn router() -> Router { + Router::new() + .route("/health_check", get(health_check)) + .nest("/pay", pay::router()) + .nest("/api", widget::router()) + .nest("/bigcommerce", bigcommerce::router()) } diff --git a/apps/server/src/routes/pay.rs b/apps/server/src/routes/pay.rs index 216f42d..dc9f2d9 100644 --- a/apps/server/src/routes/pay.rs +++ b/apps/server/src/routes/pay.rs @@ -1,11 +1,13 @@ -use crate::liq_pay::HttpAPI; use crate::liq_pay::InputQuery; -use actix_web::web::Redirect; -use actix_web::{web, HttpResponse, ResponseError}; -use reqwest::StatusCode; +use crate::startup::AppState; +use axum::extract::{Query, State}; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Redirect}; +use axum::routing::get; +use axum::Router; -pub fn register_routes(cfg: &mut web::ServiceConfig) { - cfg.service(web::resource("/pay").route(web::get().to(pay))); +pub fn router() -> Router { + Router::new().route("/", get(pay)) } #[derive(thiserror::Error, Debug)] @@ -14,27 +16,25 @@ enum PayError { UnexpectedError(#[from] anyhow::Error), } -impl ResponseError for PayError { - fn error_response(&self) -> HttpResponse { +impl IntoResponse for PayError { + fn into_response(self) -> axum::response::Response { match self { - Self::UnexpectedError(_) => HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR), + Self::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } } -#[tracing::instrument(name = "Process pay request", skip(query, liq_pay))] +#[tracing::instrument(name = "Process pay request", skip(query, liq_pay_client))] async fn pay( - query: web::Query, - liq_pay: web::Data, + Query(query): Query, + State(AppState { liq_pay_client, .. }): State, ) -> Result { - let checkout_request = liq_pay.generate_request_payload( - query.into_inner(), - "Support BigCommerce colleagues defending Ukraine", - )?; + let checkout_request = liq_pay_client + .generate_request_payload(query, "Support BigCommerce colleagues defending Ukraine")?; - let url = liq_pay + let url = liq_pay_client .link(checkout_request) .map_err(PayError::UnexpectedError)?; - Ok(Redirect::to(url)) + Ok(Redirect::to(&url)) } diff --git a/apps/server/src/routes/widget.rs b/apps/server/src/routes/widget.rs index 10eb42b..82d19ab 100644 --- a/apps/server/src/routes/widget.rs +++ b/apps/server/src/routes/widget.rs @@ -1,7 +1,5 @@ use crate::{ authentication::AuthClaims, - bigcommerce::client::HttpAPI, - configuration::BaseURL, data::{ read_store_credentials, read_store_published, read_widget_configuration, write_charity_visited_event, write_general_feedback, write_store_published, @@ -9,40 +7,44 @@ use crate::{ write_widget_event, CharityEvent, FeedbackForm, UniversalConfiguratorEvent, WidgetConfiguration, WidgetEvent, }, + startup::AppState, }; -use actix_web::{http::StatusCode, web, HttpResponse, ResponseError}; -use actix_web_httpauth::extractors::bearer::Config; +use tower_http::cors::{Any, CorsLayer}; + use anyhow::Context; -use sqlx::PgPool; - -pub fn register_routes(cfg: &mut web::ServiceConfig) { - let bearer_auth_config = Config::default().realm("api-v1").scope("modify"); - - cfg.service( - web::scope("/api/v1") - .app_data(bearer_auth_config) - .route("/configuration", web::post().to(save_widget_configuration)) - .route("/configuration", web::get().to(get_widget_configuration)) - .route("/publish", web::post().to(publish_widget)) - .route("/publish", web::get().to(get_published_status)) - .route("/publish", web::delete().to(remove_widget)) - .route("/preview", web::get().to(preview_widget)), - ); - - let cors = actix_cors::Cors::permissive(); - - cfg.service( - web::scope("/api/v2") - .wrap(cors) - .route("/widget-event", web::post().to(log_widget_event)) - .route("/charity-event", web::post().to(log_charity_event)) - .route("/feedback-form", web::post().to(submit_general_feedback)) - .route( - "/universal-event", - web::post().to(submit_universal_configurator_event), - ), - ); +use axum::{ + extract::{Query, State}, + http::{Method, StatusCode}, + response::{IntoResponse, Response}, + routing::{delete, get, post}, + Json, Router, +}; + +pub fn router() -> Router { + let v1_router = Router::new() + .route("/configuration", post(save_widget_configuration)) + .route("/configuration", get(get_widget_configuration)) + .route("/publish", post(publish_widget)) + .route("/publish", get(get_published_status)) + .route("/publish", delete(remove_widget)) + .route("/preview", get(preview_widget)); + + let cors = CorsLayer::new() + .allow_methods([Method::GET, Method::POST]) + .allow_origin(Any); + + let v2_router = Router::new() + .layer(cors) + .route("/widget-event", post(log_widget_event)) + .route("/charity-event", post(log_charity_event)) + .route("/feedback-form", post(submit_general_feedback)) + .route( + "/universal-event", + post(submit_universal_configurator_event), + ); + + Router::new().nest("/v1", v1_router).nest("/v2", v2_router) } #[derive(thiserror::Error, Debug)] @@ -51,10 +53,10 @@ enum ConfigurationError { UnexpectedError(#[from] anyhow::Error), } -impl ResponseError for ConfigurationError { - fn error_response(&self) -> HttpResponse { +impl IntoResponse for ConfigurationError { + fn into_response(self) -> axum::response::Response { match self { - Self::UnexpectedError(_) => HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR), + Self::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } } @@ -62,26 +64,26 @@ impl ResponseError for ConfigurationError { #[tracing::instrument(name = "Save widget configuration", skip(auth, db_pool))] async fn save_widget_configuration( auth: AuthClaims, - widget_configuration: web::Json, - db_pool: web::Data, -) -> Result { + State(AppState { db_pool, .. }): State, + Json(widget_configuration): Json, +) -> Result { write_widget_configuration(auth.sub.as_str(), &widget_configuration, &db_pool) .await .map_err(ConfigurationError::UnexpectedError)?; - Ok(HttpResponse::Ok().finish()) + Ok(StatusCode::OK.into_response()) } #[tracing::instrument(name = "Get widget configuration", skip(auth, db_pool))] async fn get_widget_configuration( auth: AuthClaims, - db_pool: web::Data, -) -> Result { + State(AppState { db_pool, .. }): State, +) -> Result { let widget_configuration = read_widget_configuration(auth.sub.as_str(), &db_pool) .await .map_err(ConfigurationError::UnexpectedError)?; - Ok(HttpResponse::Ok().json(widget_configuration)) + Ok(Json(widget_configuration).into_response()) } #[derive(thiserror::Error, Debug)] @@ -90,11 +92,12 @@ enum PublishError { UnexpectedError(#[from] anyhow::Error), } -impl ResponseError for PublishError { - fn error_response(&self) -> HttpResponse { +impl IntoResponse for PublishError { + fn into_response(self) -> Response { match self { - Self::UnexpectedError(_) => HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR), + Self::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR, } + .into_response() } } @@ -104,10 +107,13 @@ impl ResponseError for PublishError { )] async fn publish_widget( auth: AuthClaims, - db_pool: web::Data, - base_url: web::Data, - bigcommerce_client: web::Data, -) -> Result { + State(AppState { + db_pool, + base_url, + bigcommerce_client, + .. + }): State, +) -> Result { let store_hash = auth.sub.as_str(); let widget_configuration = read_widget_configuration(store_hash, &db_pool) .await @@ -142,7 +148,7 @@ async fn publish_widget( .context("Failed to set store as published") .map_err(PublishError::UnexpectedError)?; - Ok(HttpResponse::Ok().finish()) + Ok(StatusCode::OK.into_response()) } #[derive(serde::Deserialize)] @@ -156,10 +162,13 @@ struct Feedback { )] async fn remove_widget( auth: AuthClaims, - db_pool: web::Data, - bigcommerce_client: web::Data, - feedback: web::Query, -) -> Result { + State(AppState { + db_pool, + bigcommerce_client, + .. + }): State, + Query(feedback): Query, +) -> Result { let store_hash = auth.sub.as_str(); let store = read_store_credentials(store_hash, &db_pool) @@ -178,7 +187,6 @@ async fn remove_widget( .context("Failed to set store as not published") .map_err(PublishError::UnexpectedError)?; - let feedback = feedback.into_inner(); if let Some(reason) = feedback.reason { write_unpublish_feedback(store_hash, reason.as_str(), &db_pool) .await @@ -186,15 +194,18 @@ async fn remove_widget( .map_err(PublishError::UnexpectedError)?; } - Ok(HttpResponse::Ok().finish()) + Ok(StatusCode::OK.into_response()) } #[tracing::instrument(name = "Preview widget", skip(auth, db_pool, bigcommerce_client))] async fn preview_widget( auth: AuthClaims, - db_pool: web::Data, - bigcommerce_client: web::Data, -) -> Result { + State(AppState { + db_pool, + bigcommerce_client, + .. + }): State, +) -> Result { let store_hash = auth.sub.as_str(); let store = read_store_credentials(store_hash, &db_pool) @@ -208,14 +219,14 @@ async fn preview_widget( .context("Failed to get store information") .map_err(PublishError::UnexpectedError)?; - Ok(HttpResponse::Ok().json(store_information)) + Ok(Json(store_information).into_response()) } #[tracing::instrument(name = "Get widget status", skip(auth, db_pool))] async fn get_published_status( auth: AuthClaims, - db_pool: web::Data, -) -> Result { + State(AppState { db_pool, .. }): State, +) -> Result { let store_hash = auth.sub.as_str(); let store_status = read_store_published(store_hash, &db_pool) @@ -223,53 +234,53 @@ async fn get_published_status( .context("Failed to get store status") .map_err(PublishError::UnexpectedError)?; - Ok(HttpResponse::Ok().json(store_status)) + Ok(Json(store_status).into_response()) } #[tracing::instrument(name = "Log charity event", skip(db_pool))] async fn log_charity_event( - event: web::Query, - db_pool: web::Data, -) -> HttpResponse { - if let Err(error) = write_charity_visited_event(&event.into_inner(), &db_pool).await { + Query(event): Query, + State(AppState { db_pool, .. }): State, +) -> Response { + if let Err(error) = write_charity_visited_event(&event, &db_pool).await { tracing::warn!("Error while saving event {}", error); }; - HttpResponse::Ok().finish() + StatusCode::OK.into_response() } #[tracing::instrument(name = "Save feedback form", skip(db_pool))] async fn submit_general_feedback( - event: web::Query, - db_pool: web::Data, -) -> HttpResponse { - if let Err(error) = write_general_feedback(&event.into_inner(), &db_pool).await { + Query(event): Query, + State(AppState { db_pool, .. }): State, +) -> Response { + if let Err(error) = write_general_feedback(&event, &db_pool).await { tracing::warn!("Error while saving event {}", error); }; - HttpResponse::Ok().finish() + StatusCode::OK.into_response() } #[tracing::instrument(name = "Save universal configurator event", skip(db_pool))] async fn submit_universal_configurator_event( - event: web::Query, - db_pool: web::Data, -) -> HttpResponse { - if let Err(error) = write_universal_widget_event(&event.into_inner(), &db_pool).await { + Query(event): Query, + State(AppState { db_pool, .. }): State, +) -> Response { + if let Err(error) = write_universal_widget_event(&event, &db_pool).await { tracing::warn!("Error while saving event {}", error); }; - HttpResponse::Ok().finish() + StatusCode::OK.into_response() } #[tracing::instrument(name = "Log widget event", skip(db_pool))] async fn log_widget_event( - event: web::Query, - db_pool: web::Data, -) -> HttpResponse { - if let Err(error) = write_widget_event(&event.into_inner(), &db_pool).await { + Query(event): Query, + State(AppState { db_pool, .. }): State, +) -> Response { + if let Err(error) = write_widget_event(&event, &db_pool).await { tracing::warn!("Error while saving event {}", error); }; - HttpResponse::Ok().finish() + StatusCode::OK.into_response() } diff --git a/apps/server/src/startup.rs b/apps/server/src/startup.rs index 4579870..7c1b949 100644 --- a/apps/server/src/startup.rs +++ b/apps/server/src/startup.rs @@ -1,58 +1,45 @@ -use std::net::TcpListener; - use crate::liq_pay::HttpAPI as LiqPayHttpAPI; +use crate::routes; use crate::{ bigcommerce::client::HttpAPI as BigCommerceHttpAPI, - configuration::{BaseURL, Configuration, Database, JWTSecret, LightstepAccessToken}, - routes::register, + configuration::{Configuration, Database}, }; -use actix_web::{dev::Server, web::Data, App, HttpServer}; +use axum::serve::Serve; +use axum::{Extension, Router}; +use axum_tracing_opentelemetry::middleware::{OtelAxumLayer, OtelInResponseLayer}; use secrecy::Secret; use sqlx::{postgres::PgPoolOptions, PgPool}; -use tracing_actix_web::TracingLogger; +use tokio::net::TcpListener; pub struct Application { port: u16, - server: Server, + server: Serve, +} + +#[derive(Clone)] +pub struct AppState { + pub db_pool: PgPool, + pub base_url: String, + pub jwt_secret: Secret, + pub bigcommerce_client: BigCommerceHttpAPI, + pub liq_pay_client: LiqPayHttpAPI, } impl Application { /// # Errors /// /// Will return `std::io::Error` if listener could not be setup on the port provided - pub fn build(configuration: Configuration) -> Result { - let db_pool = get_connection_pool(&configuration.database); - let bigcommerce_client = BigCommerceHttpAPI::new( - configuration.bigcommerce.api_base_url, - configuration.bigcommerce.login_base_url, - configuration.bigcommerce.client_id, - configuration.bigcommerce.client_secret, - configuration.bigcommerce.install_redirect_uri, - std::time::Duration::from_millis(configuration.bigcommerce.timeout.into()), - ); - let liq_pay_client = LiqPayHttpAPI::new( - configuration.liq_pay.public_key, - configuration.liq_pay.private_key, - ); - + pub async fn build(configuration: Configuration) -> Result { let address = format!( "{}:{}", configuration.application.host, configuration.application.port ); - let listener = TcpListener::bind(address)?; + let listener = TcpListener::bind(address).await?; let port = listener .local_addr() .expect("listener does not have an address") .port(); - let server = run( - listener, - db_pool, - configuration.application.base_url, - configuration.application.jwt_secret, - configuration.application.lightstep_access_token, - bigcommerce_client, - liq_pay_client, - )?; + let server = run(listener, configuration.get_app_state())?; Ok(Self { port, server }) } @@ -81,33 +68,16 @@ pub fn get_connection_pool(configuration: &Database) -> PgPool { /// Will return `std::io::Error` if server could not bind to the listener pub fn run( listener: TcpListener, - db_pool: PgPool, - base_url: String, - jwt_secret: Secret, - lightstep_access_token: Secret, - bigcommerce_client: BigCommerceHttpAPI, - liq_pay_client: LiqPayHttpAPI, -) -> Result { - let db_pool = Data::new(db_pool); - let base_url = Data::new(BaseURL(base_url)); - let bigcommerce_client = Data::new(bigcommerce_client); - let liq_pay_client = Data::new(liq_pay_client); - let jwt_secret = Data::new(JWTSecret(jwt_secret)); - let lightstep_access_token = Data::new(LightstepAccessToken(lightstep_access_token)); + state: AppState, +) -> Result, std::io::Error> { + let app = Router::new() + .merge(routes::router()) + .layer(OtelInResponseLayer::default()) + .layer(OtelAxumLayer::default()) + .with_state(state.clone()) + .layer(Extension(state)); - let server = HttpServer::new(move || { - App::new() - .wrap(TracingLogger::default()) - .app_data(db_pool.clone()) - .app_data(base_url.clone()) - .app_data(bigcommerce_client.clone()) - .app_data(liq_pay_client.clone()) - .app_data(jwt_secret.clone()) - .app_data(lightstep_access_token.clone()) - .configure(register) - }) - .listen(listener)? - .run(); + let server = axum::serve(listener, app); Ok(server) } diff --git a/apps/server/tests/e2e/bigcommerce/mod.rs b/apps/server/tests/e2e/bigcommerce/mod.rs index 9a17f04..3e463ab 100644 --- a/apps/server/tests/e2e/bigcommerce/mod.rs +++ b/apps/server/tests/e2e/bigcommerce/mod.rs @@ -95,7 +95,7 @@ async fn install_request_succeeds() { .await .expect("Failed to execute the request"); - assert_eq!(response.status().as_u16(), 302); + assert!(response.status().is_redirection()); assert!( response .headers() @@ -165,7 +165,7 @@ async fn load_request_succeeds() { .await .expect("Failed to execute the request"); - assert_eq!(response.status().as_u16(), 302); + assert!(response.status().is_redirection()); assert!( response .headers() diff --git a/apps/server/tests/e2e/helpers.rs b/apps/server/tests/e2e/helpers.rs index 47c6d32..8a256a5 100644 --- a/apps/server/tests/e2e/helpers.rs +++ b/apps/server/tests/e2e/helpers.rs @@ -6,7 +6,7 @@ use sqlx::{Connection, Executor, PgConnection, PgPool}; use swu_app::{ authentication::create_jwt, bigcommerce::auth::User, - configuration::{Configuration, Database, JWTSecret}, + configuration::{Configuration, Database}, data::WidgetConfiguration, startup::{get_connection_pool, Application}, telemetry::init_tracing, @@ -27,7 +27,7 @@ pub struct TestApp { pub db_pool: PgPool, pub bigcommerce_server: MockServer, - pub jwt_secret: JWTSecret, + pub jwt_secret: Secret, pub base_url: String, pub bc_secret: Secret, pub bc_redirect_uri: String, @@ -55,8 +55,9 @@ pub async fn spawn_app() -> TestApp { configure_database(&configuration.database).await; - let application = - Application::build(configuration.clone()).expect("Failed to build application."); + let application = Application::build(configuration.clone()) + .await + .expect("Failed to build application."); let application_port = application.port(); let _ = tokio::spawn(application.run_until_stopped()); @@ -65,7 +66,7 @@ pub async fn spawn_app() -> TestApp { port: application_port, bigcommerce_server, db_pool: get_connection_pool(&configuration.database), - jwt_secret: JWTSecret(configuration.application.jwt_secret), + jwt_secret: configuration.application.jwt_secret, bc_secret: configuration.bigcommerce.client_secret, bc_redirect_uri: configuration.bigcommerce.install_redirect_uri, base_url: configuration.application.base_url, From cf3023fa35f4dd8cd2bb5f4fa90b68f00b4189e2 Mon Sep 17 00:00:00 2001 From: Micah Thomas Date: Tue, 27 Feb 2024 13:21:37 -0700 Subject: [PATCH 2/9] fix: use shared state with Arc to make state safer to share --- README.md | 8 +- apps/server/src/authentication.rs | 12 +-- apps/server/src/configuration.rs | 30 ++++--- apps/server/src/routes/bigcommerce.rs | 52 ++++++------ apps/server/src/routes/mod.rs | 4 +- apps/server/src/routes/pay.rs | 10 ++- apps/server/src/routes/widget.rs | 112 ++++++++++++++------------ apps/server/src/startup.rs | 19 ++--- 8 files changed, 130 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index 991f2cf..630fd21 100644 --- a/README.md +++ b/README.md @@ -65,17 +65,19 @@ We hope this sample gives you a good reference point for building your next kill - Recommend using `rustup` to setup `rust`, `cargo`, `fmt` - SQLX command - Recommend setup using `cargo install sqlx-cli --force --version=0.7.3 --features=postgres --no-default-features` + - Nextest command + - Recommend setup using `cargo install nextest` - Docker - Recommended setup for `macos` or `linux` is `podman` and creating an alias for docker from podman - Editor - Recommended setup is `vscode` and the `rust-analyzer` extension. - - Logs + - Tracing / Logs - Open telemetry is supported so you can enable it: - Run jaeger locally: `docker run -d -p16686:16686 -p4317:4317 jaegertracing/all-in-one:latest` - Run the app with flag enabled `RUST_LOG=trace OTEL_ENABLE=true cargo run --bin swu-app` - View spans in the jaeger ui - http://localhost:16686 - - Or you can use stdout + bunyan + - Or you can use console output + bunyan - Recommended setup is `cargo install bunyan` - Set log level during testing and pass it through bunyan `RUST_LOG=trace cargo test | bunyan` @@ -89,7 +91,7 @@ We hope this sample gives you a good reference point for building your next kill - `bigcommerce.client_secret` - Set `application.jwt_secret` to a long random string, such as that generated by your password manager or [random.org](https://random.org). 6. Start the database container `CREATE_LOCAL_DB=TRUE ./scripts/init_db.sh` -7. Run tests: `cargo test` +7. Run tests: `cargo nextest run` 8. Run app: `cargo run --bin swu-app` ### Hosting the app diff --git a/apps/server/src/authentication.rs b/apps/server/src/authentication.rs index 6ebfc90..652f80f 100644 --- a/apps/server/src/authentication.rs +++ b/apps/server/src/authentication.rs @@ -15,7 +15,7 @@ use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, use secrecy::{ExposeSecret, Secret}; use time::{Duration, OffsetDateTime}; -use crate::startup::AppState; +use crate::configuration::SharedState; #[derive(serde::Deserialize, serde::Serialize, Debug)] pub struct AuthClaims { @@ -39,12 +39,12 @@ where .await .map_err(|_| Error::NoToken)?; - let Extension(AppState { jwt_secret, .. }) = parts - .extract::>() + let Extension(state) = parts + .extract::>() .await .context("extract state")?; - decode_token(bearer.token(), jwt_secret) + decode_token(bearer.token(), &state.jwt_secret) } } @@ -81,7 +81,7 @@ pub fn create_jwt( pub struct AuthorizedUser(pub String); #[tracing::instrument(name = "decode token")] -pub fn decode_token(token: &str, secret: Secret) -> Result { +pub fn decode_token(token: &str, secret: &Secret) -> Result { let key = DecodingKey::from_secret(secret.expose_secret().as_bytes()); let validation = Validation::new(Algorithm::HS512); let decoded = decode::(token, &key, &validation).map_err(Error::InvalidToken)?; @@ -121,7 +121,7 @@ mod tests { assert!(!parts[2].is_empty()); let secret = Secret::from("abcdefg".to_owned()); - let claims = decode_token(token.as_str(), secret).unwrap(); + let claims = decode_token(token.as_str(), &secret).unwrap(); assert_eq!("test_store", claims.sub); assert!( diff --git a/apps/server/src/configuration.rs b/apps/server/src/configuration.rs index 5507cf7..8e14ac9 100644 --- a/apps/server/src/configuration.rs +++ b/apps/server/src/configuration.rs @@ -1,16 +1,17 @@ +use std::sync::Arc; + use config::{Config, ConfigError, Environment, File}; use dotenvy::dotenv; use secrecy::{ExposeSecret, Secret}; use serde_aux::field_attributes::deserialize_number_from_string; use sqlx::{ postgres::{PgConnectOptions, PgSslMode}, - ConnectOptions, + ConnectOptions, PgPool, }; use crate::{ - bigcommerce::client::HttpAPI as BigCommerceHttpAPI, - liq_pay::HttpAPI as LiqPayHttpAPI, - startup::{get_connection_pool, AppState}, + bigcommerce::client::HttpAPI as BigCommerceHttpAPI, liq_pay::HttpAPI as LiqPayHttpAPI, + startup::get_connection_pool, }; #[derive(serde::Deserialize, Clone)] @@ -117,7 +118,7 @@ impl Configuration { .try_deserialize() } - pub fn get_app_state(&self) -> AppState { + pub fn get_app_state(&self) -> SharedState { let db_pool = get_connection_pool(&self.database); let bigcommerce_client = BigCommerceHttpAPI::new( self.bigcommerce.api_base_url.clone(), @@ -132,24 +133,27 @@ impl Configuration { self.liq_pay.private_key.clone(), ); - AppState { + Arc::new(AppState { db_pool, base_url: self.application.base_url.clone(), jwt_secret: self.application.jwt_secret.clone(), bigcommerce_client, liq_pay_client, - } + }) } } -pub struct BaseURL(pub String); - -impl std::fmt::Display for BaseURL { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } +#[derive(Clone)] +pub struct AppState { + pub db_pool: PgPool, + pub base_url: String, + pub jwt_secret: Secret, + pub bigcommerce_client: BigCommerceHttpAPI, + pub liq_pay_client: LiqPayHttpAPI, } +pub type SharedState = Arc; + pub struct LightstepAccessToken(pub Secret); impl AsRef> for LightstepAccessToken { diff --git a/apps/server/src/routes/bigcommerce.rs b/apps/server/src/routes/bigcommerce.rs index d50f1b9..b509065 100644 --- a/apps/server/src/routes/bigcommerce.rs +++ b/apps/server/src/routes/bigcommerce.rs @@ -9,11 +9,11 @@ use axum::{ use crate::{ authentication::{create_jwt, Error}, + configuration::{AppState, SharedState}, data::{write_store_as_uninstalled, write_store_credentials}, - startup::AppState, }; -pub fn router() -> Router { +pub fn router() -> Router { Router::new() .route("/install", get(install)) .route("/uninstall", get(uninstall)) @@ -43,19 +43,21 @@ impl IntoResponse for InstallError { #[tracing::instrument( name = "Process install request", - skip(query, bigcommerce_client, base_url, db_pool, jwt_secret), + skip(query, state), fields(context=tracing::field::Empty, user_email=tracing::field::Empty) )] async fn install( Query(query): Query, - State(AppState { + State(state): State, +) -> Result { + let AppState { bigcommerce_client, db_pool, jwt_secret, base_url, .. - }): State, -) -> Result { + } = state.as_ref(); + tracing::Span::current().record("context", &tracing::field::display(&query.context)); let oauth_credentials = bigcommerce_client @@ -73,17 +75,17 @@ async fn install( .get_bigcommerce_store() .map_err(InstallError::UnexpectedError)?; - write_store_credentials(&store, &db_pool) + write_store_credentials(&store, db_pool) .await .context("Failed to store credentials in database") .map_err(InstallError::UnexpectedError)?; - let jwt = create_jwt(store.get_store_hash(), &jwt_secret) + let jwt = create_jwt(store.get_store_hash(), jwt_secret) .context("Failed to encode jwt token") .map_err(InstallError::UnexpectedError)?; Ok(Redirect::to(&generate_dashboard_url( - &base_url, + base_url, &jwt, store.get_store_hash(), )) @@ -116,19 +118,18 @@ impl IntoResponse for LoadError { } } -#[tracing::instrument( - name = "Process load request", - skip(query, bigcommerce_client, base_url, jwt_secret) -)] +#[tracing::instrument(name = "Process load request", skip(query, state))] async fn load( Query(query): Query, - State(AppState { + State(state): State, +) -> Result { + let AppState { bigcommerce_client, base_url, jwt_secret, .. - }): State, -) -> Result { + } = state.as_ref(); + let claims = bigcommerce_client .decode_jwt(&query.signed_payload_jwt) .map_err(LoadError::InvalidCredentials)?; @@ -137,25 +138,24 @@ async fn load( .get_store_hash() .map_err(LoadError::UnexpectedError)?; - let jwt = create_jwt(store_hash, &jwt_secret) + let jwt = create_jwt(store_hash, jwt_secret) .context("Failed to encode token") .map_err(LoadError::UnexpectedError)?; - Ok(Redirect::to(&generate_dashboard_url(&base_url, &jwt, store_hash)).into_response()) + Ok(Redirect::to(&generate_dashboard_url(base_url, &jwt, store_hash)).into_response()) } -#[tracing::instrument( - name = "Process uninstall request", - skip(query, bigcommerce_client, db_pool) -)] +#[tracing::instrument(name = "Process uninstall request", skip(query, state))] async fn uninstall( Query(query): Query, - State(AppState { + State(state): State, +) -> Result { + let AppState { bigcommerce_client, db_pool, .. - }): State, -) -> Result { + } = state.as_ref(); + let claims = bigcommerce_client .decode_jwt(&query.signed_payload_jwt) .map_err(LoadError::InvalidCredentials)?; @@ -168,7 +168,7 @@ async fn uninstall( .get_store_hash() .map_err(LoadError::UnexpectedError)?; - write_store_as_uninstalled(store_hash, &db_pool) + write_store_as_uninstalled(store_hash, db_pool) .await .context("Failed to set store as uninstalled") .map_err(LoadError::UnexpectedError)?; diff --git a/apps/server/src/routes/mod.rs b/apps/server/src/routes/mod.rs index 3539b3f..78b35df 100644 --- a/apps/server/src/routes/mod.rs +++ b/apps/server/src/routes/mod.rs @@ -1,6 +1,6 @@ use axum::{response::Response, routing::get, Router}; -use crate::startup::AppState; +use crate::configuration::SharedState; mod bigcommerce; mod pay; @@ -10,7 +10,7 @@ pub async fn health_check() -> Response { Response::new("".into()) } -pub fn router() -> Router { +pub fn router() -> Router { Router::new() .route("/health_check", get(health_check)) .nest("/pay", pay::router()) diff --git a/apps/server/src/routes/pay.rs b/apps/server/src/routes/pay.rs index dc9f2d9..6699de7 100644 --- a/apps/server/src/routes/pay.rs +++ b/apps/server/src/routes/pay.rs @@ -1,12 +1,12 @@ +use crate::configuration::{AppState, SharedState}; use crate::liq_pay::InputQuery; -use crate::startup::AppState; use axum::extract::{Query, State}; use axum::http::StatusCode; use axum::response::{IntoResponse, Redirect}; use axum::routing::get; use axum::Router; -pub fn router() -> Router { +pub fn router() -> Router { Router::new().route("/", get(pay)) } @@ -24,11 +24,13 @@ impl IntoResponse for PayError { } } -#[tracing::instrument(name = "Process pay request", skip(query, liq_pay_client))] +#[tracing::instrument(name = "Process pay request", skip(query, state))] async fn pay( Query(query): Query, - State(AppState { liq_pay_client, .. }): State, + State(state): State, ) -> Result { + let AppState { liq_pay_client, .. } = state.as_ref(); + let checkout_request = liq_pay_client .generate_request_payload(query, "Support BigCommerce colleagues defending Ukraine")?; diff --git a/apps/server/src/routes/widget.rs b/apps/server/src/routes/widget.rs index 82d19ab..17d3d01 100644 --- a/apps/server/src/routes/widget.rs +++ b/apps/server/src/routes/widget.rs @@ -1,5 +1,6 @@ use crate::{ authentication::AuthClaims, + configuration::{AppState, SharedState}, data::{ read_store_credentials, read_store_published, read_widget_configuration, write_charity_visited_event, write_general_feedback, write_store_published, @@ -7,7 +8,6 @@ use crate::{ write_widget_event, CharityEvent, FeedbackForm, UniversalConfiguratorEvent, WidgetConfiguration, WidgetEvent, }, - startup::AppState, }; use tower_http::cors::{Any, CorsLayer}; @@ -21,7 +21,7 @@ use axum::{ Json, Router, }; -pub fn router() -> Router { +pub fn router() -> Router { let v1_router = Router::new() .route("/configuration", post(save_widget_configuration)) .route("/configuration", get(get_widget_configuration)) @@ -61,25 +61,31 @@ impl IntoResponse for ConfigurationError { } } -#[tracing::instrument(name = "Save widget configuration", skip(auth, db_pool))] +#[tracing::instrument(name = "Save widget configuration", skip(auth, state))] async fn save_widget_configuration( auth: AuthClaims, - State(AppState { db_pool, .. }): State, + State(state): State, Json(widget_configuration): Json, ) -> Result { - write_widget_configuration(auth.sub.as_str(), &widget_configuration, &db_pool) + let AppState { db_pool, .. } = state.as_ref(); + let store_hash = auth.sub.as_str(); + + write_widget_configuration(store_hash, &widget_configuration, db_pool) .await .map_err(ConfigurationError::UnexpectedError)?; Ok(StatusCode::OK.into_response()) } -#[tracing::instrument(name = "Get widget configuration", skip(auth, db_pool))] +#[tracing::instrument(name = "Get widget configuration", skip(auth, state))] async fn get_widget_configuration( auth: AuthClaims, - State(AppState { db_pool, .. }): State, + State(state): State, ) -> Result { - let widget_configuration = read_widget_configuration(auth.sub.as_str(), &db_pool) + let AppState { db_pool, .. } = state.as_ref(); + let store_hash = auth.sub.as_str(); + + let widget_configuration = read_widget_configuration(store_hash, db_pool) .await .map_err(ConfigurationError::UnexpectedError)?; @@ -101,30 +107,28 @@ impl IntoResponse for PublishError { } } -#[tracing::instrument( - name = "Publish the widget", - skip(auth, base_url, db_pool, bigcommerce_client) -)] +#[tracing::instrument(name = "Publish the widget", skip(auth, state))] async fn publish_widget( auth: AuthClaims, - State(AppState { + State(state): State, +) -> Result { + let AppState { db_pool, base_url, bigcommerce_client, .. - }): State, -) -> Result { + } = state.as_ref(); let store_hash = auth.sub.as_str(); - let widget_configuration = read_widget_configuration(store_hash, &db_pool) + let widget_configuration = read_widget_configuration(store_hash, db_pool) .await .map_err(PublishError::UnexpectedError)?; let script = widget_configuration - .generate_script(store_hash, &base_url) + .generate_script(store_hash, base_url) .context("Failed to generate script content") .map_err(PublishError::UnexpectedError)?; - let store = read_store_credentials(store_hash, &db_pool) + let store = read_store_credentials(store_hash, db_pool) .await .map_err(PublishError::UnexpectedError)?; @@ -143,7 +147,7 @@ async fn publish_widget( } .map_err(PublishError::UnexpectedError)?; - write_store_published(store_hash, true, &db_pool) + write_store_published(store_hash, true, db_pool) .await .context("Failed to set store as published") .map_err(PublishError::UnexpectedError)?; @@ -156,22 +160,20 @@ struct Feedback { reason: Option, } -#[tracing::instrument( - name = "Remove widget", - skip(auth, db_pool, bigcommerce_client, feedback) -)] +#[tracing::instrument(name = "Remove widget", skip(auth, state, feedback))] async fn remove_widget( auth: AuthClaims, - State(AppState { + State(state): State, + Query(feedback): Query, +) -> Result { + let AppState { db_pool, bigcommerce_client, .. - }): State, - Query(feedback): Query, -) -> Result { + } = state.as_ref(); let store_hash = auth.sub.as_str(); - let store = read_store_credentials(store_hash, &db_pool) + let store = read_store_credentials(store_hash, db_pool) .await .context("Failed to get store credentials") .map_err(PublishError::UnexpectedError)?; @@ -182,13 +184,13 @@ async fn remove_widget( .context("Failed to remove scripts in BigCommerce") .map_err(PublishError::UnexpectedError)?; - write_store_published(store_hash, false, &db_pool) + write_store_published(store_hash, false, db_pool) .await .context("Failed to set store as not published") .map_err(PublishError::UnexpectedError)?; if let Some(reason) = feedback.reason { - write_unpublish_feedback(store_hash, reason.as_str(), &db_pool) + write_unpublish_feedback(store_hash, reason.as_str(), db_pool) .await .context("Failed to record unpublish feedback") .map_err(PublishError::UnexpectedError)?; @@ -197,18 +199,19 @@ async fn remove_widget( Ok(StatusCode::OK.into_response()) } -#[tracing::instrument(name = "Preview widget", skip(auth, db_pool, bigcommerce_client))] +#[tracing::instrument(name = "Preview widget", skip(auth, state))] async fn preview_widget( auth: AuthClaims, - State(AppState { + State(state): State, +) -> Result { + let AppState { db_pool, bigcommerce_client, .. - }): State, -) -> Result { + } = state.as_ref(); let store_hash = auth.sub.as_str(); - let store = read_store_credentials(store_hash, &db_pool) + let store = read_store_credentials(store_hash, db_pool) .await .context("Failed to get store credentials") .map_err(PublishError::UnexpectedError)?; @@ -222,14 +225,15 @@ async fn preview_widget( Ok(Json(store_information).into_response()) } -#[tracing::instrument(name = "Get widget status", skip(auth, db_pool))] +#[tracing::instrument(name = "Get widget status", skip(auth, state))] async fn get_published_status( auth: AuthClaims, - State(AppState { db_pool, .. }): State, + State(state): State, ) -> Result { + let AppState { db_pool, .. } = state.as_ref(); let store_hash = auth.sub.as_str(); - let store_status = read_store_published(store_hash, &db_pool) + let store_status = read_store_published(store_hash, db_pool) .await .context("Failed to get store status") .map_err(PublishError::UnexpectedError)?; @@ -237,48 +241,56 @@ async fn get_published_status( Ok(Json(store_status).into_response()) } -#[tracing::instrument(name = "Log charity event", skip(db_pool))] +#[tracing::instrument(name = "Log charity event", skip(state))] async fn log_charity_event( Query(event): Query, - State(AppState { db_pool, .. }): State, + State(state): State, ) -> Response { - if let Err(error) = write_charity_visited_event(&event, &db_pool).await { + let AppState { db_pool, .. } = state.as_ref(); + + if let Err(error) = write_charity_visited_event(&event, db_pool).await { tracing::warn!("Error while saving event {}", error); }; StatusCode::OK.into_response() } -#[tracing::instrument(name = "Save feedback form", skip(db_pool))] +#[tracing::instrument(name = "Save feedback form", skip(state))] async fn submit_general_feedback( Query(event): Query, - State(AppState { db_pool, .. }): State, + State(state): State, ) -> Response { - if let Err(error) = write_general_feedback(&event, &db_pool).await { + let AppState { db_pool, .. } = state.as_ref(); + + if let Err(error) = write_general_feedback(&event, db_pool).await { tracing::warn!("Error while saving event {}", error); }; StatusCode::OK.into_response() } -#[tracing::instrument(name = "Save universal configurator event", skip(db_pool))] +#[tracing::instrument(name = "Save universal configurator event", skip(state))] async fn submit_universal_configurator_event( Query(event): Query, - State(AppState { db_pool, .. }): State, + State(state): State, ) -> Response { - if let Err(error) = write_universal_widget_event(&event, &db_pool).await { + let AppState { db_pool, .. } = state.as_ref(); + + if let Err(error) = write_universal_widget_event(&event, db_pool).await { tracing::warn!("Error while saving event {}", error); }; StatusCode::OK.into_response() } -#[tracing::instrument(name = "Log widget event", skip(db_pool))] +#[tracing::instrument(name = "Log widget event", skip(state))] async fn log_widget_event( Query(event): Query, - State(AppState { db_pool, .. }): State, + State(state): State, ) -> Response { - if let Err(error) = write_widget_event(&event, &db_pool).await { + let AppState { db_pool, .. } = state.as_ref(); + + if let Err(error) = write_widget_event(&event, db_pool).await { tracing::warn!("Error while saving event {}", error); }; diff --git a/apps/server/src/startup.rs b/apps/server/src/startup.rs index 7c1b949..d4c2ae7 100644 --- a/apps/server/src/startup.rs +++ b/apps/server/src/startup.rs @@ -1,3 +1,4 @@ +use crate::configuration::SharedState; use crate::liq_pay::HttpAPI as LiqPayHttpAPI; use crate::routes; use crate::{ @@ -39,7 +40,7 @@ impl Application { .local_addr() .expect("listener does not have an address") .port(); - let server = run(listener, configuration.get_app_state())?; + let server = run(listener, configuration.get_app_state()); Ok(Self { port, server }) } @@ -63,21 +64,13 @@ pub fn get_connection_pool(configuration: &Database) -> PgPool { .connect_lazy_with(configuration.with_db()) } -/// # Errors -/// -/// Will return `std::io::Error` if server could not bind to the listener -pub fn run( - listener: TcpListener, - state: AppState, -) -> Result, std::io::Error> { +pub fn run(listener: TcpListener, shared_state: SharedState) -> Serve { let app = Router::new() .merge(routes::router()) .layer(OtelInResponseLayer::default()) .layer(OtelAxumLayer::default()) - .with_state(state.clone()) - .layer(Extension(state)); + .with_state(shared_state.clone()) + .layer(Extension(shared_state)); - let server = axum::serve(listener, app); - - Ok(server) + axum::serve(listener, app) } From ff3c139d04f0bef08c1a5620953d75903b86e4d2 Mon Sep 17 00:00:00 2001 From: Micah Thomas Date: Wed, 28 Feb 2024 11:51:38 -0700 Subject: [PATCH 3/9] feature: improve ergonomics of state extraction and use state inside authentication extractor --- apps/server/src/authentication.rs | 15 ++-- apps/server/src/configuration.rs | 21 ++--- apps/server/src/lib.rs | 1 + apps/server/src/routes/bigcommerce.rs | 52 ++++++------- apps/server/src/routes/mod.rs | 4 +- apps/server/src/routes/pay.rs | 10 +-- apps/server/src/routes/widget.rs | 108 ++++++++++++-------------- apps/server/src/startup.rs | 27 ++----- apps/server/src/state.rs | 26 +++++++ 9 files changed, 128 insertions(+), 136 deletions(-) create mode 100644 apps/server/src/state.rs diff --git a/apps/server/src/authentication.rs b/apps/server/src/authentication.rs index 652f80f..9435334 100644 --- a/apps/server/src/authentication.rs +++ b/apps/server/src/authentication.rs @@ -1,11 +1,10 @@ -use anyhow::Context; +use axum::extract::FromRef; use axum::RequestPartsExt; use axum::{ async_trait, extract::FromRequestParts, http::{request::Parts, StatusCode}, response::{IntoResponse, Response}, - Extension, }; use axum_extra::{ headers::{authorization::Bearer, Authorization}, @@ -15,7 +14,7 @@ use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, use secrecy::{ExposeSecret, Secret}; use time::{Duration, OffsetDateTime}; -use crate::configuration::SharedState; +use crate::state::Shared; #[derive(serde::Deserialize, serde::Serialize, Debug)] pub struct AuthClaims { @@ -27,22 +26,20 @@ pub struct AuthClaims { #[async_trait] impl FromRequestParts for AuthClaims where + Shared: FromRef, S: Send + Sync, { type Rejection = Error; - #[tracing::instrument(name = "decode auth from request", skip(parts, _state))] - async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + #[tracing::instrument(name = "decode auth from request", skip(parts, state))] + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { // Extract the token from the authorization header let TypedHeader(Authorization(bearer)) = parts .extract::>>() .await .map_err(|_| Error::NoToken)?; - let Extension(state) = parts - .extract::>() - .await - .context("extract state")?; + let state = Shared::from_ref(state); decode_token(bearer.token(), &state.jwt_secret) } diff --git a/apps/server/src/configuration.rs b/apps/server/src/configuration.rs index 8e14ac9..effb589 100644 --- a/apps/server/src/configuration.rs +++ b/apps/server/src/configuration.rs @@ -6,12 +6,14 @@ use secrecy::{ExposeSecret, Secret}; use serde_aux::field_attributes::deserialize_number_from_string; use sqlx::{ postgres::{PgConnectOptions, PgSslMode}, - ConnectOptions, PgPool, + ConnectOptions, }; use crate::{ - bigcommerce::client::HttpAPI as BigCommerceHttpAPI, liq_pay::HttpAPI as LiqPayHttpAPI, + bigcommerce::client::HttpAPI as BigCommerceHttpAPI, + liq_pay::HttpAPI as LiqPayHttpAPI, startup::get_connection_pool, + state::{App, Shared}, }; #[derive(serde::Deserialize, Clone)] @@ -118,7 +120,7 @@ impl Configuration { .try_deserialize() } - pub fn get_app_state(&self) -> SharedState { + pub fn get_app_state(&self) -> Shared { let db_pool = get_connection_pool(&self.database); let bigcommerce_client = BigCommerceHttpAPI::new( self.bigcommerce.api_base_url.clone(), @@ -133,7 +135,7 @@ impl Configuration { self.liq_pay.private_key.clone(), ); - Arc::new(AppState { + Arc::new(App { db_pool, base_url: self.application.base_url.clone(), jwt_secret: self.application.jwt_secret.clone(), @@ -143,17 +145,6 @@ impl Configuration { } } -#[derive(Clone)] -pub struct AppState { - pub db_pool: PgPool, - pub base_url: String, - pub jwt_secret: Secret, - pub bigcommerce_client: BigCommerceHttpAPI, - pub liq_pay_client: LiqPayHttpAPI, -} - -pub type SharedState = Arc; - pub struct LightstepAccessToken(pub Secret); impl AsRef> for LightstepAccessToken { diff --git a/apps/server/src/lib.rs b/apps/server/src/lib.rs index 77d9c60..2ae26a3 100644 --- a/apps/server/src/lib.rs +++ b/apps/server/src/lib.rs @@ -5,4 +5,5 @@ pub mod data; pub mod liq_pay; pub mod routes; pub mod startup; +pub mod state; pub mod telemetry; diff --git a/apps/server/src/routes/bigcommerce.rs b/apps/server/src/routes/bigcommerce.rs index b509065..1411651 100644 --- a/apps/server/src/routes/bigcommerce.rs +++ b/apps/server/src/routes/bigcommerce.rs @@ -9,11 +9,11 @@ use axum::{ use crate::{ authentication::{create_jwt, Error}, - configuration::{AppState, SharedState}, data::{write_store_as_uninstalled, write_store_credentials}, + state::{App, Shared}, }; -pub fn router() -> Router { +pub fn router() -> Router { Router::new() .route("/install", get(install)) .route("/uninstall", get(uninstall)) @@ -43,21 +43,19 @@ impl IntoResponse for InstallError { #[tracing::instrument( name = "Process install request", - skip(query, state), + skip(query, bigcommerce_client, db_pool, jwt_secret, base_url), fields(context=tracing::field::Empty, user_email=tracing::field::Empty) )] async fn install( Query(query): Query, - State(state): State, -) -> Result { - let AppState { + State(App { bigcommerce_client, db_pool, jwt_secret, base_url, .. - } = state.as_ref(); - + }): State, +) -> Result { tracing::Span::current().record("context", &tracing::field::display(&query.context)); let oauth_credentials = bigcommerce_client @@ -75,17 +73,17 @@ async fn install( .get_bigcommerce_store() .map_err(InstallError::UnexpectedError)?; - write_store_credentials(&store, db_pool) + write_store_credentials(&store, &db_pool) .await .context("Failed to store credentials in database") .map_err(InstallError::UnexpectedError)?; - let jwt = create_jwt(store.get_store_hash(), jwt_secret) + let jwt = create_jwt(store.get_store_hash(), &jwt_secret) .context("Failed to encode jwt token") .map_err(InstallError::UnexpectedError)?; Ok(Redirect::to(&generate_dashboard_url( - base_url, + &base_url, &jwt, store.get_store_hash(), )) @@ -118,18 +116,19 @@ impl IntoResponse for LoadError { } } -#[tracing::instrument(name = "Process load request", skip(query, state))] +#[tracing::instrument( + name = "Process load request", + skip(query, bigcommerce_client, base_url, jwt_secret) +)] async fn load( Query(query): Query, - State(state): State, -) -> Result { - let AppState { + State(App { bigcommerce_client, base_url, jwt_secret, .. - } = state.as_ref(); - + }): State, +) -> Result { let claims = bigcommerce_client .decode_jwt(&query.signed_payload_jwt) .map_err(LoadError::InvalidCredentials)?; @@ -138,24 +137,25 @@ async fn load( .get_store_hash() .map_err(LoadError::UnexpectedError)?; - let jwt = create_jwt(store_hash, jwt_secret) + let jwt = create_jwt(store_hash, &jwt_secret) .context("Failed to encode token") .map_err(LoadError::UnexpectedError)?; - Ok(Redirect::to(&generate_dashboard_url(base_url, &jwt, store_hash)).into_response()) + Ok(Redirect::to(&generate_dashboard_url(&base_url, &jwt, store_hash)).into_response()) } -#[tracing::instrument(name = "Process uninstall request", skip(query, state))] +#[tracing::instrument( + name = "Process uninstall request", + skip(query, bigcommerce_client, db_pool) +)] async fn uninstall( Query(query): Query, - State(state): State, -) -> Result { - let AppState { + State(App { bigcommerce_client, db_pool, .. - } = state.as_ref(); - + }): State, +) -> Result { let claims = bigcommerce_client .decode_jwt(&query.signed_payload_jwt) .map_err(LoadError::InvalidCredentials)?; @@ -168,7 +168,7 @@ async fn uninstall( .get_store_hash() .map_err(LoadError::UnexpectedError)?; - write_store_as_uninstalled(store_hash, db_pool) + write_store_as_uninstalled(store_hash, &db_pool) .await .context("Failed to set store as uninstalled") .map_err(LoadError::UnexpectedError)?; diff --git a/apps/server/src/routes/mod.rs b/apps/server/src/routes/mod.rs index 78b35df..9022333 100644 --- a/apps/server/src/routes/mod.rs +++ b/apps/server/src/routes/mod.rs @@ -1,6 +1,6 @@ use axum::{response::Response, routing::get, Router}; -use crate::configuration::SharedState; +use crate::state::Shared; mod bigcommerce; mod pay; @@ -10,7 +10,7 @@ pub async fn health_check() -> Response { Response::new("".into()) } -pub fn router() -> Router { +pub fn router() -> Router { Router::new() .route("/health_check", get(health_check)) .nest("/pay", pay::router()) diff --git a/apps/server/src/routes/pay.rs b/apps/server/src/routes/pay.rs index 6699de7..ab5997c 100644 --- a/apps/server/src/routes/pay.rs +++ b/apps/server/src/routes/pay.rs @@ -1,12 +1,12 @@ -use crate::configuration::{AppState, SharedState}; use crate::liq_pay::InputQuery; +use crate::state::{App, Shared}; use axum::extract::{Query, State}; use axum::http::StatusCode; use axum::response::{IntoResponse, Redirect}; use axum::routing::get; use axum::Router; -pub fn router() -> Router { +pub fn router() -> Router { Router::new().route("/", get(pay)) } @@ -24,13 +24,11 @@ impl IntoResponse for PayError { } } -#[tracing::instrument(name = "Process pay request", skip(query, state))] +#[tracing::instrument(name = "Process pay request", skip(query, liq_pay_client))] async fn pay( Query(query): Query, - State(state): State, + State(App { liq_pay_client, .. }): State, ) -> Result { - let AppState { liq_pay_client, .. } = state.as_ref(); - let checkout_request = liq_pay_client .generate_request_payload(query, "Support BigCommerce colleagues defending Ukraine")?; diff --git a/apps/server/src/routes/widget.rs b/apps/server/src/routes/widget.rs index 17d3d01..9d34f71 100644 --- a/apps/server/src/routes/widget.rs +++ b/apps/server/src/routes/widget.rs @@ -1,6 +1,5 @@ use crate::{ authentication::AuthClaims, - configuration::{AppState, SharedState}, data::{ read_store_credentials, read_store_published, read_widget_configuration, write_charity_visited_event, write_general_feedback, write_store_published, @@ -8,6 +7,7 @@ use crate::{ write_widget_event, CharityEvent, FeedbackForm, UniversalConfiguratorEvent, WidgetConfiguration, WidgetEvent, }, + state::{App, Shared}, }; use tower_http::cors::{Any, CorsLayer}; @@ -21,7 +21,7 @@ use axum::{ Json, Router, }; -pub fn router() -> Router { +pub fn router() -> Router { let v1_router = Router::new() .route("/configuration", post(save_widget_configuration)) .route("/configuration", get(get_widget_configuration)) @@ -61,31 +61,29 @@ impl IntoResponse for ConfigurationError { } } -#[tracing::instrument(name = "Save widget configuration", skip(auth, state))] +#[tracing::instrument(name = "Save widget configuration", skip(auth, db_pool))] async fn save_widget_configuration( auth: AuthClaims, - State(state): State, + State(App { db_pool, .. }): State, Json(widget_configuration): Json, ) -> Result { - let AppState { db_pool, .. } = state.as_ref(); let store_hash = auth.sub.as_str(); - write_widget_configuration(store_hash, &widget_configuration, db_pool) + write_widget_configuration(store_hash, &widget_configuration, &db_pool) .await .map_err(ConfigurationError::UnexpectedError)?; Ok(StatusCode::OK.into_response()) } -#[tracing::instrument(name = "Get widget configuration", skip(auth, state))] +#[tracing::instrument(name = "Get widget configuration", skip(auth, db_pool))] async fn get_widget_configuration( auth: AuthClaims, - State(state): State, + State(App { db_pool, .. }): State, ) -> Result { - let AppState { db_pool, .. } = state.as_ref(); let store_hash = auth.sub.as_str(); - let widget_configuration = read_widget_configuration(store_hash, db_pool) + let widget_configuration = read_widget_configuration(store_hash, &db_pool) .await .map_err(ConfigurationError::UnexpectedError)?; @@ -107,28 +105,30 @@ impl IntoResponse for PublishError { } } -#[tracing::instrument(name = "Publish the widget", skip(auth, state))] +#[tracing::instrument( + name = "Publish the widget", + skip(auth, db_pool, base_url, bigcommerce_client) +)] async fn publish_widget( auth: AuthClaims, - State(state): State, -) -> Result { - let AppState { + State(App { db_pool, base_url, bigcommerce_client, .. - } = state.as_ref(); + }): State, +) -> Result { let store_hash = auth.sub.as_str(); - let widget_configuration = read_widget_configuration(store_hash, db_pool) + let widget_configuration = read_widget_configuration(store_hash, &db_pool) .await .map_err(PublishError::UnexpectedError)?; let script = widget_configuration - .generate_script(store_hash, base_url) + .generate_script(store_hash, &base_url) .context("Failed to generate script content") .map_err(PublishError::UnexpectedError)?; - let store = read_store_credentials(store_hash, db_pool) + let store = read_store_credentials(store_hash, &db_pool) .await .map_err(PublishError::UnexpectedError)?; @@ -147,7 +147,7 @@ async fn publish_widget( } .map_err(PublishError::UnexpectedError)?; - write_store_published(store_hash, true, db_pool) + write_store_published(store_hash, true, &db_pool) .await .context("Failed to set store as published") .map_err(PublishError::UnexpectedError)?; @@ -160,20 +160,22 @@ struct Feedback { reason: Option, } -#[tracing::instrument(name = "Remove widget", skip(auth, state, feedback))] +#[tracing::instrument( + name = "Remove widget", + skip(auth, db_pool, bigcommerce_client, feedback) +)] async fn remove_widget( auth: AuthClaims, - State(state): State, - Query(feedback): Query, -) -> Result { - let AppState { + State(App { db_pool, bigcommerce_client, .. - } = state.as_ref(); + }): State, + Query(feedback): Query, +) -> Result { let store_hash = auth.sub.as_str(); - let store = read_store_credentials(store_hash, db_pool) + let store = read_store_credentials(store_hash, &db_pool) .await .context("Failed to get store credentials") .map_err(PublishError::UnexpectedError)?; @@ -184,13 +186,13 @@ async fn remove_widget( .context("Failed to remove scripts in BigCommerce") .map_err(PublishError::UnexpectedError)?; - write_store_published(store_hash, false, db_pool) + write_store_published(store_hash, false, &db_pool) .await .context("Failed to set store as not published") .map_err(PublishError::UnexpectedError)?; if let Some(reason) = feedback.reason { - write_unpublish_feedback(store_hash, reason.as_str(), db_pool) + write_unpublish_feedback(store_hash, reason.as_str(), &db_pool) .await .context("Failed to record unpublish feedback") .map_err(PublishError::UnexpectedError)?; @@ -199,19 +201,18 @@ async fn remove_widget( Ok(StatusCode::OK.into_response()) } -#[tracing::instrument(name = "Preview widget", skip(auth, state))] +#[tracing::instrument(name = "Preview widget", skip(auth, db_pool, bigcommerce_client))] async fn preview_widget( auth: AuthClaims, - State(state): State, -) -> Result { - let AppState { + State(App { db_pool, bigcommerce_client, .. - } = state.as_ref(); + }): State, +) -> Result { let store_hash = auth.sub.as_str(); - let store = read_store_credentials(store_hash, db_pool) + let store = read_store_credentials(store_hash, &db_pool) .await .context("Failed to get store credentials") .map_err(PublishError::UnexpectedError)?; @@ -225,15 +226,14 @@ async fn preview_widget( Ok(Json(store_information).into_response()) } -#[tracing::instrument(name = "Get widget status", skip(auth, state))] +#[tracing::instrument(name = "Get widget status", skip(auth, db_pool))] async fn get_published_status( auth: AuthClaims, - State(state): State, + State(App { db_pool, .. }): State, ) -> Result { - let AppState { db_pool, .. } = state.as_ref(); let store_hash = auth.sub.as_str(); - let store_status = read_store_published(store_hash, db_pool) + let store_status = read_store_published(store_hash, &db_pool) .await .context("Failed to get store status") .map_err(PublishError::UnexpectedError)?; @@ -241,56 +241,48 @@ async fn get_published_status( Ok(Json(store_status).into_response()) } -#[tracing::instrument(name = "Log charity event", skip(state))] +#[tracing::instrument(name = "Log charity event", skip(db_pool))] async fn log_charity_event( Query(event): Query, - State(state): State, + State(App { db_pool, .. }): State, ) -> Response { - let AppState { db_pool, .. } = state.as_ref(); - - if let Err(error) = write_charity_visited_event(&event, db_pool).await { + if let Err(error) = write_charity_visited_event(&event, &db_pool).await { tracing::warn!("Error while saving event {}", error); }; StatusCode::OK.into_response() } -#[tracing::instrument(name = "Save feedback form", skip(state))] +#[tracing::instrument(name = "Save feedback form", skip(db_pool))] async fn submit_general_feedback( Query(event): Query, - State(state): State, + State(App { db_pool, .. }): State, ) -> Response { - let AppState { db_pool, .. } = state.as_ref(); - - if let Err(error) = write_general_feedback(&event, db_pool).await { + if let Err(error) = write_general_feedback(&event, &db_pool).await { tracing::warn!("Error while saving event {}", error); }; StatusCode::OK.into_response() } -#[tracing::instrument(name = "Save universal configurator event", skip(state))] +#[tracing::instrument(name = "Save universal configurator event", skip(db_pool))] async fn submit_universal_configurator_event( Query(event): Query, - State(state): State, + State(App { db_pool, .. }): State, ) -> Response { - let AppState { db_pool, .. } = state.as_ref(); - - if let Err(error) = write_universal_widget_event(&event, db_pool).await { + if let Err(error) = write_universal_widget_event(&event, &db_pool).await { tracing::warn!("Error while saving event {}", error); }; StatusCode::OK.into_response() } -#[tracing::instrument(name = "Log widget event", skip(state))] +#[tracing::instrument(name = "Log widget event", skip(db_pool))] async fn log_widget_event( Query(event): Query, - State(state): State, + State(App { db_pool, .. }): State, ) -> Response { - let AppState { db_pool, .. } = state.as_ref(); - - if let Err(error) = write_widget_event(&event, db_pool).await { + if let Err(error) = write_widget_event(&event, &db_pool).await { tracing::warn!("Error while saving event {}", error); }; diff --git a/apps/server/src/startup.rs b/apps/server/src/startup.rs index d4c2ae7..e3aa7bf 100644 --- a/apps/server/src/startup.rs +++ b/apps/server/src/startup.rs @@ -1,14 +1,9 @@ -use crate::configuration::SharedState; -use crate::liq_pay::HttpAPI as LiqPayHttpAPI; +use crate::configuration::{Configuration, Database}; use crate::routes; -use crate::{ - bigcommerce::client::HttpAPI as BigCommerceHttpAPI, - configuration::{Configuration, Database}, -}; +use crate::state::Shared; use axum::serve::Serve; -use axum::{Extension, Router}; +use axum::Router; use axum_tracing_opentelemetry::middleware::{OtelAxumLayer, OtelInResponseLayer}; -use secrecy::Secret; use sqlx::{postgres::PgPoolOptions, PgPool}; use tokio::net::TcpListener; @@ -17,15 +12,6 @@ pub struct Application { server: Serve, } -#[derive(Clone)] -pub struct AppState { - pub db_pool: PgPool, - pub base_url: String, - pub jwt_secret: Secret, - pub bigcommerce_client: BigCommerceHttpAPI, - pub liq_pay_client: LiqPayHttpAPI, -} - impl Application { /// # Errors /// @@ -64,13 +50,14 @@ pub fn get_connection_pool(configuration: &Database) -> PgPool { .connect_lazy_with(configuration.with_db()) } -pub fn run(listener: TcpListener, shared_state: SharedState) -> Serve { +#[allow(clippy::default_constructed_unit_structs)] +// reason = "OtelInResponseLayer struct is external and might change" +pub fn run(listener: TcpListener, shared_state: Shared) -> Serve { let app = Router::new() .merge(routes::router()) .layer(OtelInResponseLayer::default()) .layer(OtelAxumLayer::default()) - .with_state(shared_state.clone()) - .layer(Extension(shared_state)); + .with_state(shared_state); axum::serve(listener, app) } diff --git a/apps/server/src/state.rs b/apps/server/src/state.rs new file mode 100644 index 0000000..efbb126 --- /dev/null +++ b/apps/server/src/state.rs @@ -0,0 +1,26 @@ +use std::sync::Arc; + +use axum::extract::FromRef; +use secrecy::Secret; +use sqlx::PgPool; + +use crate::{ + bigcommerce::client::HttpAPI as BigCommerceHttpAPI, liq_pay::HttpAPI as LiqPayHttpAPI, +}; + +#[derive(Clone)] +pub struct App { + pub db_pool: PgPool, + pub base_url: String, + pub jwt_secret: Secret, + pub bigcommerce_client: BigCommerceHttpAPI, + pub liq_pay_client: LiqPayHttpAPI, +} + +pub type Shared = Arc; + +impl FromRef for App { + fn from_ref(shared_state: &Shared) -> Self { + shared_state.as_ref().clone() + } +} From fb627995fb81a534ece16feba8eda9d666d32d19 Mon Sep 17 00:00:00 2001 From: Micah Thomas Date: Wed, 28 Feb 2024 12:38:59 -0700 Subject: [PATCH 4/9] chore: remove unused derives --- apps/server/src/authentication.rs | 22 ++++----- apps/server/src/configuration.rs | 6 +-- apps/server/src/liq_pay.rs | 66 +++++++++++++-------------- apps/server/src/routes/bigcommerce.rs | 16 +++---- apps/server/src/routes/mod.rs | 4 +- apps/server/src/routes/pay.rs | 33 ++++---------- apps/server/src/routes/widget.rs | 30 ++++++------ apps/server/src/startup.rs | 6 +-- apps/server/src/state.rs | 12 +++-- apps/server/tests/e2e/helpers.rs | 2 +- 10 files changed, 87 insertions(+), 110 deletions(-) diff --git a/apps/server/src/authentication.rs b/apps/server/src/authentication.rs index 9435334..e3b20e2 100644 --- a/apps/server/src/authentication.rs +++ b/apps/server/src/authentication.rs @@ -12,11 +12,13 @@ use axum_extra::{ }; use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; use secrecy::{ExposeSecret, Secret}; +use serde::{Deserialize, Serialize}; use time::{Duration, OffsetDateTime}; +use tracing::trace; -use crate::state::Shared; +use crate::state::SharedState; -#[derive(serde::Deserialize, serde::Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct AuthClaims { pub sub: String, pub role: String, @@ -26,7 +28,7 @@ pub struct AuthClaims { #[async_trait] impl FromRequestParts for AuthClaims where - Shared: FromRef, + SharedState: FromRef, S: Send + Sync, { type Rejection = Error; @@ -39,7 +41,7 @@ where .await .map_err(|_| Error::NoToken)?; - let state = Shared::from_ref(state); + let state = SharedState::from_ref(state); decode_token(bearer.token(), &state.jwt_secret) } @@ -48,10 +50,6 @@ where impl IntoResponse for Error { fn into_response(self) -> Response { match self { - Self::InvalidServerConfiguration => { - (StatusCode::INTERNAL_SERVER_ERROR, "Unknown error") - } - Self::Unexpected(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Unknown error"), Self::InvalidToken(_) | Self::NoToken => (StatusCode::BAD_REQUEST, "Invalid token"), } .into_response() @@ -83,6 +81,8 @@ pub fn decode_token(token: &str, secret: &Secret) -> Result(token, &key, &validation).map_err(Error::InvalidToken)?; + trace!(?decoded, "token decoded"); + Ok(decoded.claims) } @@ -93,12 +93,6 @@ pub enum Error { #[error("Token is invalid.")] InvalidToken(#[source] jsonwebtoken::errors::Error), - - #[error("Server Configuration Invalid")] - InvalidServerConfiguration, - - #[error(transparent)] - Unexpected(#[from] anyhow::Error), } #[cfg(test)] diff --git a/apps/server/src/configuration.rs b/apps/server/src/configuration.rs index effb589..c9ab5ae 100644 --- a/apps/server/src/configuration.rs +++ b/apps/server/src/configuration.rs @@ -13,7 +13,7 @@ use crate::{ bigcommerce::client::HttpAPI as BigCommerceHttpAPI, liq_pay::HttpAPI as LiqPayHttpAPI, startup::get_connection_pool, - state::{App, Shared}, + state::{AppState, SharedState}, }; #[derive(serde::Deserialize, Clone)] @@ -120,7 +120,7 @@ impl Configuration { .try_deserialize() } - pub fn get_app_state(&self) -> Shared { + pub fn get_app_state(&self) -> SharedState { let db_pool = get_connection_pool(&self.database); let bigcommerce_client = BigCommerceHttpAPI::new( self.bigcommerce.api_base_url.clone(), @@ -135,7 +135,7 @@ impl Configuration { self.liq_pay.private_key.clone(), ); - Arc::new(App { + Arc::new(AppState { db_pool, base_url: self.application.base_url.clone(), jwt_secret: self.application.jwt_secret.clone(), diff --git a/apps/server/src/liq_pay.rs b/apps/server/src/liq_pay.rs index 5b7c932..d359782 100644 --- a/apps/server/src/liq_pay.rs +++ b/apps/server/src/liq_pay.rs @@ -21,14 +21,14 @@ pub enum Currency { UAH, } -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Language { UA, EN, } -#[derive(Debug, Deserialize, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize, Eq, PartialEq)] #[serde(rename_all = "lowercase")] pub enum SubscribePeriod { Month, @@ -95,7 +95,7 @@ impl HttpAPI { &self, query: InputQuery, description: &str, - ) -> anyhow::Result { + ) -> CheckoutRequest { let shared = BaseFields { public_key: self.public_key.expose_secret().clone(), language: query.language, @@ -107,28 +107,28 @@ impl HttpAPI { order_id: uuid::Uuid::new_v4().into(), }; - Ok(match query.action { + match query.action { Action::Subscribe => CheckoutRequest::Subscription { shared, subscribe: 1, subscribe_periodicity: SubscribePeriod::Month, - subscribe_date_start: OffsetDateTime::now_utc().format(DATE_TIME_FORMAT)?, + subscribe_date_start: OffsetDateTime::now_utc().format(DATE_TIME_FORMAT).unwrap(), }, Action::Pay | Action::PayDonate => CheckoutRequest::Pay { shared }, - }) + } } #[tracing::instrument(name = "Generate LiqPay link", skip(self))] - pub fn link(&self, request: CheckoutRequest) -> anyhow::Result { - let data = serde_json::to_string(&request)?; + pub fn link(&self, request: CheckoutRequest) -> String { + let data = serde_json::to_string(&request).unwrap(); let data = encoder.encode(data); - Ok(format!( + format!( "https://www.liqpay.ua/api/{}/checkout?data={}&signature={}", API_VERSION, data, self.signature(&data) - )) + ) } fn signature(&self, data: &String) -> String { @@ -158,19 +158,17 @@ mod tests { Secret::new("private_key".to_string()), ); - let checkout_request = client - .generate_request_payload( - InputQuery { - amount: 100.0, - language: Language::UA, - currency: Currency::UAH, - action: Action::Subscribe, - }, - "Stand with Ukraine", - ) - .unwrap(); + let checkout_request = client.generate_request_payload( + InputQuery { + amount: 100.0, + language: Language::UA, + currency: Currency::UAH, + action: Action::Subscribe, + }, + "Stand with Ukraine", + ); - let link = client.link(checkout_request).unwrap(); + let link = client.link(checkout_request); assert_eq!( link.starts_with("https://www.liqpay.ua/api/3/checkout?data="), @@ -251,19 +249,17 @@ mod tests { Secret::new("private_key".to_string()), ); - let checkout_request = client - .generate_request_payload( - InputQuery { - amount: 100.0, - language: Language::EN, - currency: Currency::USD, - action: Action::Pay, - }, - "Stand with Ukraine", - ) - .unwrap(); - - let link = client.link(checkout_request).unwrap(); + let checkout_request = client.generate_request_payload( + InputQuery { + amount: 100.0, + language: Language::EN, + currency: Currency::USD, + action: Action::Pay, + }, + "Stand with Ukraine", + ); + + let link = client.link(checkout_request); assert_eq!( link.starts_with("https://www.liqpay.ua/api/3/checkout?data="), diff --git a/apps/server/src/routes/bigcommerce.rs b/apps/server/src/routes/bigcommerce.rs index 1411651..e9d0b6b 100644 --- a/apps/server/src/routes/bigcommerce.rs +++ b/apps/server/src/routes/bigcommerce.rs @@ -10,10 +10,10 @@ use axum::{ use crate::{ authentication::{create_jwt, Error}, data::{write_store_as_uninstalled, write_store_credentials}, - state::{App, Shared}, + state::{AppState, SharedState}, }; -pub fn router() -> Router { +pub fn router() -> Router { Router::new() .route("/install", get(install)) .route("/uninstall", get(uninstall)) @@ -48,13 +48,13 @@ impl IntoResponse for InstallError { )] async fn install( Query(query): Query, - State(App { + State(AppState { bigcommerce_client, db_pool, jwt_secret, base_url, .. - }): State, + }): State, ) -> Result { tracing::Span::current().record("context", &tracing::field::display(&query.context)); @@ -122,12 +122,12 @@ impl IntoResponse for LoadError { )] async fn load( Query(query): Query, - State(App { + State(AppState { bigcommerce_client, base_url, jwt_secret, .. - }): State, + }): State, ) -> Result { let claims = bigcommerce_client .decode_jwt(&query.signed_payload_jwt) @@ -150,11 +150,11 @@ async fn load( )] async fn uninstall( Query(query): Query, - State(App { + State(AppState { bigcommerce_client, db_pool, .. - }): State, + }): State, ) -> Result { let claims = bigcommerce_client .decode_jwt(&query.signed_payload_jwt) diff --git a/apps/server/src/routes/mod.rs b/apps/server/src/routes/mod.rs index 9022333..84e29e1 100644 --- a/apps/server/src/routes/mod.rs +++ b/apps/server/src/routes/mod.rs @@ -1,6 +1,6 @@ use axum::{response::Response, routing::get, Router}; -use crate::state::Shared; +use crate::state::SharedState; mod bigcommerce; mod pay; @@ -10,7 +10,7 @@ pub async fn health_check() -> Response { Response::new("".into()) } -pub fn router() -> Router { +pub fn router() -> Router { Router::new() .route("/health_check", get(health_check)) .nest("/pay", pay::router()) diff --git a/apps/server/src/routes/pay.rs b/apps/server/src/routes/pay.rs index ab5997c..7cc4fce 100644 --- a/apps/server/src/routes/pay.rs +++ b/apps/server/src/routes/pay.rs @@ -1,40 +1,23 @@ use crate::liq_pay::InputQuery; -use crate::state::{App, Shared}; +use crate::state::{AppState, SharedState}; use axum::extract::{Query, State}; -use axum::http::StatusCode; -use axum::response::{IntoResponse, Redirect}; +use axum::response::Redirect; use axum::routing::get; use axum::Router; -pub fn router() -> Router { +pub fn router() -> Router { Router::new().route("/", get(pay)) } -#[derive(thiserror::Error, Debug)] -enum PayError { - #[error(transparent)] - UnexpectedError(#[from] anyhow::Error), -} - -impl IntoResponse for PayError { - fn into_response(self) -> axum::response::Response { - match self { - Self::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), - } - } -} - #[tracing::instrument(name = "Process pay request", skip(query, liq_pay_client))] async fn pay( Query(query): Query, - State(App { liq_pay_client, .. }): State, -) -> Result { + State(AppState { liq_pay_client, .. }): State, +) -> Redirect { let checkout_request = liq_pay_client - .generate_request_payload(query, "Support BigCommerce colleagues defending Ukraine")?; + .generate_request_payload(query, "Support BigCommerce colleagues defending Ukraine"); - let url = liq_pay_client - .link(checkout_request) - .map_err(PayError::UnexpectedError)?; + let url = liq_pay_client.link(checkout_request); - Ok(Redirect::to(&url)) + Redirect::to(&url) } diff --git a/apps/server/src/routes/widget.rs b/apps/server/src/routes/widget.rs index 9d34f71..0429e77 100644 --- a/apps/server/src/routes/widget.rs +++ b/apps/server/src/routes/widget.rs @@ -7,7 +7,7 @@ use crate::{ write_widget_event, CharityEvent, FeedbackForm, UniversalConfiguratorEvent, WidgetConfiguration, WidgetEvent, }, - state::{App, Shared}, + state::{AppState, SharedState}, }; use tower_http::cors::{Any, CorsLayer}; @@ -21,7 +21,7 @@ use axum::{ Json, Router, }; -pub fn router() -> Router { +pub fn router() -> Router { let v1_router = Router::new() .route("/configuration", post(save_widget_configuration)) .route("/configuration", get(get_widget_configuration)) @@ -64,7 +64,7 @@ impl IntoResponse for ConfigurationError { #[tracing::instrument(name = "Save widget configuration", skip(auth, db_pool))] async fn save_widget_configuration( auth: AuthClaims, - State(App { db_pool, .. }): State, + State(AppState { db_pool, .. }): State, Json(widget_configuration): Json, ) -> Result { let store_hash = auth.sub.as_str(); @@ -79,7 +79,7 @@ async fn save_widget_configuration( #[tracing::instrument(name = "Get widget configuration", skip(auth, db_pool))] async fn get_widget_configuration( auth: AuthClaims, - State(App { db_pool, .. }): State, + State(AppState { db_pool, .. }): State, ) -> Result { let store_hash = auth.sub.as_str(); @@ -111,12 +111,12 @@ impl IntoResponse for PublishError { )] async fn publish_widget( auth: AuthClaims, - State(App { + State(AppState { db_pool, base_url, bigcommerce_client, .. - }): State, + }): State, ) -> Result { let store_hash = auth.sub.as_str(); let widget_configuration = read_widget_configuration(store_hash, &db_pool) @@ -166,11 +166,11 @@ struct Feedback { )] async fn remove_widget( auth: AuthClaims, - State(App { + State(AppState { db_pool, bigcommerce_client, .. - }): State, + }): State, Query(feedback): Query, ) -> Result { let store_hash = auth.sub.as_str(); @@ -204,11 +204,11 @@ async fn remove_widget( #[tracing::instrument(name = "Preview widget", skip(auth, db_pool, bigcommerce_client))] async fn preview_widget( auth: AuthClaims, - State(App { + State(AppState { db_pool, bigcommerce_client, .. - }): State, + }): State, ) -> Result { let store_hash = auth.sub.as_str(); @@ -229,7 +229,7 @@ async fn preview_widget( #[tracing::instrument(name = "Get widget status", skip(auth, db_pool))] async fn get_published_status( auth: AuthClaims, - State(App { db_pool, .. }): State, + State(AppState { db_pool, .. }): State, ) -> Result { let store_hash = auth.sub.as_str(); @@ -244,7 +244,7 @@ async fn get_published_status( #[tracing::instrument(name = "Log charity event", skip(db_pool))] async fn log_charity_event( Query(event): Query, - State(App { db_pool, .. }): State, + State(AppState { db_pool, .. }): State, ) -> Response { if let Err(error) = write_charity_visited_event(&event, &db_pool).await { tracing::warn!("Error while saving event {}", error); @@ -256,7 +256,7 @@ async fn log_charity_event( #[tracing::instrument(name = "Save feedback form", skip(db_pool))] async fn submit_general_feedback( Query(event): Query, - State(App { db_pool, .. }): State, + State(AppState { db_pool, .. }): State, ) -> Response { if let Err(error) = write_general_feedback(&event, &db_pool).await { tracing::warn!("Error while saving event {}", error); @@ -268,7 +268,7 @@ async fn submit_general_feedback( #[tracing::instrument(name = "Save universal configurator event", skip(db_pool))] async fn submit_universal_configurator_event( Query(event): Query, - State(App { db_pool, .. }): State, + State(AppState { db_pool, .. }): State, ) -> Response { if let Err(error) = write_universal_widget_event(&event, &db_pool).await { tracing::warn!("Error while saving event {}", error); @@ -280,7 +280,7 @@ async fn submit_universal_configurator_event( #[tracing::instrument(name = "Log widget event", skip(db_pool))] async fn log_widget_event( Query(event): Query, - State(App { db_pool, .. }): State, + State(AppState { db_pool, .. }): State, ) -> Response { if let Err(error) = write_widget_event(&event, &db_pool).await { tracing::warn!("Error while saving event {}", error); diff --git a/apps/server/src/startup.rs b/apps/server/src/startup.rs index e3aa7bf..8c473f4 100644 --- a/apps/server/src/startup.rs +++ b/apps/server/src/startup.rs @@ -1,6 +1,6 @@ use crate::configuration::{Configuration, Database}; use crate::routes; -use crate::state::Shared; +use crate::state::SharedState; use axum::serve::Serve; use axum::Router; use axum_tracing_opentelemetry::middleware::{OtelAxumLayer, OtelInResponseLayer}; @@ -51,8 +51,8 @@ pub fn get_connection_pool(configuration: &Database) -> PgPool { } #[allow(clippy::default_constructed_unit_structs)] -// reason = "OtelInResponseLayer struct is external and might change" -pub fn run(listener: TcpListener, shared_state: Shared) -> Serve { +// reason = "`OtelInResponseLayer` struct is an external dependency that might change" +pub fn run(listener: TcpListener, shared_state: SharedState) -> Serve { let app = Router::new() .merge(routes::router()) .layer(OtelInResponseLayer::default()) diff --git a/apps/server/src/state.rs b/apps/server/src/state.rs index efbb126..ca85ec7 100644 --- a/apps/server/src/state.rs +++ b/apps/server/src/state.rs @@ -8,8 +8,10 @@ use crate::{ bigcommerce::client::HttpAPI as BigCommerceHttpAPI, liq_pay::HttpAPI as LiqPayHttpAPI, }; +#[allow(clippy::module_name_repetitions)] +// reason="`AppState` is clearer than just `App` and it is widespread across the app" #[derive(Clone)] -pub struct App { +pub struct AppState { pub db_pool: PgPool, pub base_url: String, pub jwt_secret: Secret, @@ -17,10 +19,12 @@ pub struct App { pub liq_pay_client: LiqPayHttpAPI, } -pub type Shared = Arc; +#[allow(clippy::module_name_repetitions)] +// reason="`SharedState` is clearer than just `Shared` and it is widespread across the app" +pub type SharedState = Arc; -impl FromRef for App { - fn from_ref(shared_state: &Shared) -> Self { +impl FromRef for AppState { + fn from_ref(shared_state: &SharedState) -> Self { shared_state.as_ref().clone() } } diff --git a/apps/server/tests/e2e/helpers.rs b/apps/server/tests/e2e/helpers.rs index 8a256a5..d59e79d 100644 --- a/apps/server/tests/e2e/helpers.rs +++ b/apps/server/tests/e2e/helpers.rs @@ -16,7 +16,7 @@ use uuid::Uuid; use wiremock::MockServer; static TRACING: Lazy<()> = Lazy::new(|| { - let default_filter_level = "off".into(); + let default_filter_level = "trace".into(); let subscriber_name = "test".into(); init_tracing(subscriber_name, default_filter_level); }); From a1fbbe82af1b9ed0235e21edbdc371ebbde7e308 Mon Sep 17 00:00:00 2001 From: Micah Thomas Date: Wed, 28 Feb 2024 16:51:28 -0700 Subject: [PATCH 5/9] chore: add tests for telemetry type --- apps/server/src/startup.rs | 2 +- apps/server/src/telemetry.rs | 85 +++++++++++++++++++++++++----------- 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/apps/server/src/startup.rs b/apps/server/src/startup.rs index 8c473f4..95427b3 100644 --- a/apps/server/src/startup.rs +++ b/apps/server/src/startup.rs @@ -37,7 +37,7 @@ impl Application { /// # Errors /// - /// Will return `std::io::Error` if actix server returns an error + /// Will return `std::io::Error` if axum server returns an error pub async fn run_until_stopped(self) -> Result<(), std::io::Error> { self.server.await } diff --git a/apps/server/src/telemetry.rs b/apps/server/src/telemetry.rs index d62d099..61bcef9 100644 --- a/apps/server/src/telemetry.rs +++ b/apps/server/src/telemetry.rs @@ -15,33 +15,68 @@ pub fn init_tracing(name: String, default_env_filter: String) { EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_env_filter)); let dispatcher: Dispatch = if std::env::var("OTEL_ENABLE").is_ok() { - let tracer = opentelemetry_otlp::new_pipeline() - .tracing() - .with_exporter( - opentelemetry_otlp::new_exporter() - .tonic() - .with_endpoint("http://localhost:4317"), - ) - .with_trace_config( - trace::config() - .with_sampler(Sampler::AlwaysOn) - .with_resource(Resource::new(vec![KeyValue::new("service.name", name)])), - ) - .install_batch(runtime::Tokio) - .expect("make tracing pipeline"); - - let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); - - Registry::default().with(env_filter).with(telemetry).into() + generate_otlp_tracing_subscriber(name, env_filter) } else { - let formatting_layer = BunyanFormattingLayer::new(name, std::io::stdout); - - Registry::default() - .with(env_filter) - .with(JsonStorageLayer) - .with(formatting_layer) - .into() + generate_bunyan_console_subscriber(name, env_filter) }; set_global_default(dispatcher).expect("failed to set dispatcher"); } + +pub fn generate_otlp_tracing_subscriber(name: String, env_filter: EnvFilter) -> Dispatch { + let tracer = opentelemetry_otlp::new_pipeline() + .tracing() + .with_exporter( + opentelemetry_otlp::new_exporter() + .tonic() + .with_endpoint("http://localhost:4317"), + ) + .with_trace_config( + trace::config() + .with_sampler(Sampler::AlwaysOn) + .with_resource(Resource::new(vec![KeyValue::new("service.name", name)])), + ) + .install_batch(runtime::Tokio) + .expect("make tracing pipeline"); + + let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); + + Registry::default().with(env_filter).with(telemetry).into() +} + +pub fn generate_bunyan_console_subscriber(name: String, env_filter: EnvFilter) -> Dispatch { + let formatting_layer = BunyanFormattingLayer::new(name, std::io::stdout); + + Registry::default() + .with(env_filter) + .with(JsonStorageLayer) + .with(formatting_layer) + .into() +} + +#[cfg(test)] +mod test { + use std::any::{Any, TypeId}; + + use tracing_subscriber::EnvFilter; + + use super::*; + + #[tokio::test] + async fn test_generate_otlp_subscriber() { + let env_filter = EnvFilter::new("trace"); + + let dispatch = generate_otlp_tracing_subscriber("swu-app".to_owned(), env_filter); + + assert_eq!(dispatch.type_id(), TypeId::of::()) + } + + #[tokio::test] + async fn test_generate_bunyan_subscriber() { + let env_filter = EnvFilter::new("trace"); + + let dispatch = generate_bunyan_console_subscriber("swu-app".to_owned(), env_filter); + + assert_eq!(dispatch.type_id(), TypeId::of::()) + } +} From e62afd257b3e257f05ccf807ca889eb0a01c3514 Mon Sep 17 00:00:00 2001 From: Micah Thomas Date: Wed, 28 Feb 2024 17:01:08 -0700 Subject: [PATCH 6/9] fix: use permissive cors like actix --- apps/server/src/routes/bigcommerce.rs | 2 +- apps/server/src/routes/widget.rs | 8 +++----- apps/server/src/startup.rs | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/server/src/routes/bigcommerce.rs b/apps/server/src/routes/bigcommerce.rs index e9d0b6b..2990193 100644 --- a/apps/server/src/routes/bigcommerce.rs +++ b/apps/server/src/routes/bigcommerce.rs @@ -34,7 +34,7 @@ enum InstallError { } impl IntoResponse for InstallError { - fn into_response(self) -> axum::response::Response { + fn into_response(self) -> Response { match self { Self::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } diff --git a/apps/server/src/routes/widget.rs b/apps/server/src/routes/widget.rs index 0429e77..e325a38 100644 --- a/apps/server/src/routes/widget.rs +++ b/apps/server/src/routes/widget.rs @@ -10,12 +10,12 @@ use crate::{ state::{AppState, SharedState}, }; -use tower_http::cors::{Any, CorsLayer}; +use tower_http::cors::CorsLayer; use anyhow::Context; use axum::{ extract::{Query, State}, - http::{Method, StatusCode}, + http::StatusCode, response::{IntoResponse, Response}, routing::{delete, get, post}, Json, Router, @@ -30,9 +30,7 @@ pub fn router() -> Router { .route("/publish", delete(remove_widget)) .route("/preview", get(preview_widget)); - let cors = CorsLayer::new() - .allow_methods([Method::GET, Method::POST]) - .allow_origin(Any); + let cors = CorsLayer::permissive(); let v2_router = Router::new() .layer(cors) diff --git a/apps/server/src/startup.rs b/apps/server/src/startup.rs index 95427b3..2df7393 100644 --- a/apps/server/src/startup.rs +++ b/apps/server/src/startup.rs @@ -26,7 +26,7 @@ impl Application { .local_addr() .expect("listener does not have an address") .port(); - let server = run(listener, configuration.get_app_state()); + let server = build_server(listener, configuration.get_app_state()); Ok(Self { port, server }) } @@ -52,7 +52,7 @@ pub fn get_connection_pool(configuration: &Database) -> PgPool { #[allow(clippy::default_constructed_unit_structs)] // reason = "`OtelInResponseLayer` struct is an external dependency that might change" -pub fn run(listener: TcpListener, shared_state: SharedState) -> Serve { +pub fn build_server(listener: TcpListener, shared_state: SharedState) -> Serve { let app = Router::new() .merge(routes::router()) .layer(OtelInResponseLayer::default()) From 2c3e4e9ced137f575e0406c088ac55aa79f2655d Mon Sep 17 00:00:00 2001 From: Micah Thomas Date: Wed, 28 Feb 2024 19:00:46 -0700 Subject: [PATCH 7/9] fix: make tests also support open telemetry - add graceful shutdown signal and close the tracing subscriber safetly on server shutdown - add drop trait for the `TestApp` so we can shutdown subscriber safetly --- Cargo.lock | 10 ++++++ README.md | 8 +++-- apps/server/Cargo.toml | 13 +++---- apps/server/src/startup.rs | 35 ++++++++++++++++++- apps/server/tests/e2e/bigcommerce/mod.rs | 14 ++++---- apps/server/tests/e2e/helpers.rs | 13 ++++--- apps/server/tests/e2e/main.rs | 4 +-- apps/server/tests/e2e/pay.rs | 2 +- apps/server/tests/e2e/widget/analytics.rs | 12 +++---- apps/server/tests/e2e/widget/configuration.rs | 8 ++--- apps/server/tests/e2e/widget/publish.rs | 20 +++++------ 11 files changed, 93 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6dce7c..617d523 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2612,6 +2612,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -3134,6 +3143,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.48.0", diff --git a/README.md b/README.md index 630fd21..c33cfab 100644 --- a/README.md +++ b/README.md @@ -74,10 +74,12 @@ We hope this sample gives you a good reference point for building your next kill - Tracing / Logs - Open telemetry is supported so you can enable it: - Run jaeger locally: `docker run -d -p16686:16686 -p4317:4317 jaegertracing/all-in-one:latest` - - Run the app with flag enabled `RUST_LOG=trace OTEL_ENABLE=true cargo run --bin swu-app` - - View spans in the jaeger ui + - Run + - the app with flag enabled `RUST_LOG=trace OTEL_ENABLE=true cargo run --bin swu-app` + - or the test suite `OTEL_ENABLE=true cargo nextest run` + - View spans in the jaeger ui (service = `swu-app` for server binary, or service = `test` for test suite) - http://localhost:16686 - - Or you can use console output + bunyan + - Or you can use console output + bunyan - Recommended setup is `cargo install bunyan` - Set log level during testing and pass it through bunyan `RUST_LOG=trace cargo test | bunyan` diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml index b432e1e..812d169 100644 --- a/apps/server/Cargo.toml +++ b/apps/server/Cargo.toml @@ -37,17 +37,14 @@ sha1 = "0.10.5" assert-json-diff = "2.0.2" opentelemetry-otlp = { version = "0.14.0" } opentelemetry_sdk = { version = "0.21.2", features = ["rt-tokio"] } -axum = { version = "0.7.4", features = [ - "default", - "tokio", - "tracing", - "macros", - "json", -] } tower-http = { version = "0.5.2", features = ["cors", "trace"] } axum-extra = { version = "0.9.2", features = ["typed-header"] } axum-tracing-opentelemetry = "0.17.1" +[dependencies.axum] +version = "0.7.4" +features = ["default", "tokio", "tracing", "macros", "json"] + [dependencies.reqwest] version = "0.11.24" features = ["json", "rustls-tls"] @@ -69,7 +66,7 @@ features = [ [dependencies.tokio] version = "1.35.1" -features = ["macros", "rt-multi-thread"] +features = ["macros", "rt-multi-thread", "signal"] [dev-dependencies] rstest = "0.18.2" diff --git a/apps/server/src/startup.rs b/apps/server/src/startup.rs index 2df7393..4da1b1f 100644 --- a/apps/server/src/startup.rs +++ b/apps/server/src/startup.rs @@ -6,6 +6,7 @@ use axum::Router; use axum_tracing_opentelemetry::middleware::{OtelAxumLayer, OtelInResponseLayer}; use sqlx::{postgres::PgPoolOptions, PgPool}; use tokio::net::TcpListener; +use tokio::signal; pub struct Application { port: u16, @@ -39,7 +40,7 @@ impl Application { /// /// Will return `std::io::Error` if axum server returns an error pub async fn run_until_stopped(self) -> Result<(), std::io::Error> { - self.server.await + self.server.with_graceful_shutdown(shutdown_signal()).await } } @@ -61,3 +62,35 @@ pub fn build_server(listener: TcpListener, shared_state: SharedState) -> Serve(); + + tokio::select! { + () = ctrl_c => { + }, + () = terminate => { + }, + } + + //TODO: remove when https://github.com/open-telemetry/opentelemetry-rust/issues/868 is fixed + //for now we have to use async task because global tracer is in a RWLock that will block otherwise + tokio::task::spawn_blocking(opentelemetry::global::shutdown_tracer_provider) + .await + .unwrap(); +} diff --git a/apps/server/tests/e2e/bigcommerce/mod.rs b/apps/server/tests/e2e/bigcommerce/mod.rs index 3e463ab..697e7d5 100644 --- a/apps/server/tests/e2e/bigcommerce/mod.rs +++ b/apps/server/tests/e2e/bigcommerce/mod.rs @@ -8,7 +8,7 @@ use swu_app::{ data::write_store_credentials, }; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn install_request_fails_without_bigcommerce_response() { let app = spawn_app().await; let client = create_test_server_client_no_redirect(); @@ -27,7 +27,7 @@ async fn install_request_fails_without_bigcommerce_response() { assert!(response.status().is_server_error()); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn install_request_fails_without_query_parameters() { let app = spawn_app().await; @@ -74,7 +74,7 @@ async fn install_request_fails_without_query_parameters() { ); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn install_request_succeeds() { let app = spawn_app().await; let client = create_test_server_client_no_redirect(); @@ -121,7 +121,7 @@ async fn install_request_succeeds() { assert_eq!(row.store_hash, "STORE_HASH"); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn load_request_fails_with_bad_token() { let app = spawn_app().await; let client = create_test_server_client_no_redirect(); @@ -153,7 +153,7 @@ async fn load_request_fails_with_bad_token() { assert!(response.status().is_server_error()); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn load_request_succeeds() { let app = spawn_app().await; let client = create_test_server_client_no_redirect(); @@ -178,7 +178,7 @@ async fn load_request_succeeds() { ); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn uninstall_request_succeeds() { let app = spawn_app().await; let client = create_test_server_client_no_redirect(); @@ -212,7 +212,7 @@ async fn uninstall_request_succeeds() { assert_eq!(response.status().as_u16(), 200); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn uninstall_request_fails_with_non_owner() { let app = spawn_app().await; let client = create_test_server_client_no_redirect(); diff --git a/apps/server/tests/e2e/helpers.rs b/apps/server/tests/e2e/helpers.rs index d59e79d..dc1cb07 100644 --- a/apps/server/tests/e2e/helpers.rs +++ b/apps/server/tests/e2e/helpers.rs @@ -1,5 +1,4 @@ use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; -use once_cell::sync::Lazy; use reqwest::Client; use secrecy::{ExposeSecret, Secret}; use sqlx::{Connection, Executor, PgConnection, PgPool}; @@ -15,11 +14,11 @@ use time::{Duration, OffsetDateTime}; use uuid::Uuid; use wiremock::MockServer; -static TRACING: Lazy<()> = Lazy::new(|| { +pub fn init_test_tracing() { let default_filter_level = "trace".into(); let subscriber_name = "test".into(); init_tracing(subscriber_name, default_filter_level); -}); +} pub struct TestApp { pub address: String, @@ -36,7 +35,7 @@ pub struct TestApp { } pub async fn spawn_app() -> TestApp { - Lazy::force(&TRACING); + init_test_tracing(); let bigcommerce_server = MockServer::start().await; @@ -74,6 +73,12 @@ pub async fn spawn_app() -> TestApp { } } +impl Drop for TestApp { + fn drop(&mut self) { + tokio::task::block_in_place(opentelemetry::global::shutdown_tracer_provider); + } +} + async fn configure_database(config: &Database) -> PgPool { let mut connection = PgConnection::connect_with(&config.without_db()) .await diff --git a/apps/server/tests/e2e/main.rs b/apps/server/tests/e2e/main.rs index 15d0f45..e99293d 100644 --- a/apps/server/tests/e2e/main.rs +++ b/apps/server/tests/e2e/main.rs @@ -3,9 +3,9 @@ pub mod widget; pub mod helpers; pub mod mocks; -mod pay; +pub mod pay; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn health_check() { let app = helpers::spawn_app().await; diff --git a/apps/server/tests/e2e/pay.rs b/apps/server/tests/e2e/pay.rs index deeafc8..075b6ba 100644 --- a/apps/server/tests/e2e/pay.rs +++ b/apps/server/tests/e2e/pay.rs @@ -1,7 +1,7 @@ use crate::helpers; use crate::helpers::create_test_server_client_no_redirect; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn pay_check() { let app = helpers::spawn_app().await; let response = create_test_server_client_no_redirect() diff --git a/apps/server/tests/e2e/widget/analytics.rs b/apps/server/tests/e2e/widget/analytics.rs index b76844c..2b4e28a 100644 --- a/apps/server/tests/e2e/widget/analytics.rs +++ b/apps/server/tests/e2e/widget/analytics.rs @@ -1,6 +1,6 @@ use crate::helpers::spawn_app; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_event_without_store_does_not_create_record() { let app = spawn_app().await; @@ -41,7 +41,7 @@ async fn insert_event_without_store_does_not_create_record() { ); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_event_after_store_created_creates_record() { let app = spawn_app().await; app.insert_test_store().await; @@ -92,7 +92,7 @@ async fn insert_event_after_store_created_creates_record() { ); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_event_using_universal_creates_record() { let app = spawn_app().await; app.insert_test_store().await; @@ -140,7 +140,7 @@ async fn insert_event_using_universal_creates_record() { ); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn submit_general_feedback_without_required_or_invalid_fields_does_not_create_record() { let app = spawn_app().await; @@ -181,7 +181,7 @@ async fn submit_general_feedback_without_required_or_invalid_fields_does_not_cre assert_eq!(app.get_form_feedback_submissions().await.count(), 0); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn submit_general_feedback_should_create_record() { let app = spawn_app().await; @@ -204,7 +204,7 @@ async fn submit_general_feedback_should_create_record() { assert_eq!(app.get_form_feedback_submissions().await.count(), 1); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn submit_universal_configurator_event_should_create_record() { let app = spawn_app().await; diff --git a/apps/server/tests/e2e/widget/configuration.rs b/apps/server/tests/e2e/widget/configuration.rs index 7da2edb..2313bb0 100644 --- a/apps/server/tests/e2e/widget/configuration.rs +++ b/apps/server/tests/e2e/widget/configuration.rs @@ -2,7 +2,7 @@ use swu_app::data::WidgetConfiguration; use crate::helpers::{get_widget_configuration, spawn_app}; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn save_widget_configuration_fails_with_invalid_config() { let app = spawn_app().await; @@ -22,7 +22,7 @@ async fn save_widget_configuration_fails_with_invalid_config() { assert!(response.status().is_client_error()); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn save_widget_configuration_fails_when_store_not_defined() { let app = spawn_app().await; @@ -38,7 +38,7 @@ async fn save_widget_configuration_fails_when_store_not_defined() { assert!(response.status().is_server_error()); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn read_widget_configuration_fails_with_no_store() { let app = spawn_app().await; @@ -53,7 +53,7 @@ async fn read_widget_configuration_fails_with_no_store() { assert!(response.status().is_server_error()); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn save_and_read_widget_configuration() { let app = spawn_app().await; diff --git a/apps/server/tests/e2e/widget/publish.rs b/apps/server/tests/e2e/widget/publish.rs index 7b4849d..88837f8 100644 --- a/apps/server/tests/e2e/widget/publish.rs +++ b/apps/server/tests/e2e/widget/publish.rs @@ -8,7 +8,7 @@ use crate::{ }, }; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn widget_publish_request_fails_without_token_or_with_invalid_token() { let app = spawn_app().await; @@ -34,7 +34,7 @@ async fn widget_publish_request_fails_without_token_or_with_invalid_token() { assert!(response.status().is_client_error()); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn widget_publish_request_succeeds() { let app = spawn_app().await; @@ -121,7 +121,7 @@ async fn widget_publish_request_succeeds() { } } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn widget_publish_request_fails_without_configuration_saved() { let app = spawn_app().await; @@ -145,7 +145,7 @@ async fn widget_publish_request_fails_without_configuration_saved() { assert!(response.status().is_server_error()); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn widget_publish_request_fails_without_bigcommerce_server_response() { let app = spawn_app().await; @@ -170,7 +170,7 @@ async fn widget_publish_request_fails_without_bigcommerce_server_response() { assert!(response.status().is_server_error()); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn get_published_status_fails_without_store() { let app = spawn_app().await; @@ -185,7 +185,7 @@ async fn get_published_status_fails_without_store() { assert!(response.status().is_server_error()); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn widget_preview_request_fails_without_store() { let app = spawn_app().await; @@ -200,7 +200,7 @@ async fn widget_preview_request_fails_without_store() { assert!(response.status().is_server_error()); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn widget_preview_request_succeeds() { let app = spawn_app().await; let client = create_test_server_client_no_redirect(); @@ -228,7 +228,7 @@ async fn widget_preview_request_succeeds() { ); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn widget_remove_request_fails_without_store() { let app = spawn_app().await; @@ -243,7 +243,7 @@ async fn widget_remove_request_fails_without_store() { assert!(response.status().is_server_error()); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn widget_remove_request_succeeds() { let app = spawn_app().await; @@ -283,7 +283,7 @@ async fn widget_remove_request_succeeds() { assert!(!response.published); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn widget_remove_request_with_feedback_succeeds() { let app = spawn_app().await; From 1d60d1eec0111e0a9bdb4ba1b0c74348400db3c4 Mon Sep 17 00:00:00 2001 From: Micah Thomas Date: Wed, 28 Feb 2024 19:44:44 -0700 Subject: [PATCH 8/9] chore: update github actions --- .github/workflows/exporter.yaml | 62 ++++++--------- .github/workflows/security-audit.yaml | 8 +- .github/workflows/server.yaml | 107 ++++++++++---------------- 3 files changed, 67 insertions(+), 110 deletions(-) diff --git a/.github/workflows/exporter.yaml b/.github/workflows/exporter.yaml index 354ee09..511afd8 100644 --- a/.github/workflows/exporter.yaml +++ b/.github/workflows/exporter.yaml @@ -6,7 +6,7 @@ on: - main pull_request: branches: - - '*' + - "*" env: CARGO_TERM_COLOR: always @@ -33,37 +33,26 @@ jobs: POSTGRES_HOST: localhost steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup .env run: ./scripts/create_env.sh - - name: Cache dependencies - id: cache-dependencies - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-exporter-${{ hashFiles('**/Cargo.lock') }} - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: - profile: minimal toolchain: stable - override: true - - name: Check Format - uses: actions-rs/cargo@v1 + - uses: Swatinem/rust-cache@v2 with: - command: fmt - args: --all -- --check + cache-targets: true + + - name: Check Format + run: cargo fmt --all -- --check - uses: taiki-e/install-action@nextest - name: Cache sqlx-cli - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-sqlx with: path: | @@ -71,16 +60,13 @@ jobs: ~/.cargo/bin/cargo-sqlx key: ${{ runner.os }}-sqlx-${{ env.SQLX_VERSION }} - name: Install sqlx-cli - uses: actions-rs/cargo@v1 if: steps.cache-sqlx.outputs.cache-hit == false - with: - command: install - args: | - sqlx-cli - --force - --version=${{ env.SQLX_VERSION }} - --features ${{ env.SQLX_FEATURES }} - --no-default-features + run: | + cargo install sqlx-cli \ + --force \ + --version=${{ env.SQLX_VERSION }} \ + --features ${{ env.SQLX_FEATURES }} \ + --no-default-features \ --locked - name: Migrate database @@ -89,11 +75,9 @@ jobs: ./scripts/init_db.sh - name: Lint - uses: actions-rs/clippy-check@v1 if: github.actor != 'dependabot[bot]' - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: -- -D warnings + run: | + cargo clippy -- -D warnings - name: Check sqlx offline query if: github.actor != 'dependabot[bot]' @@ -120,7 +104,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Exporter Google Sheet Credentials env: @@ -131,19 +115,19 @@ jobs: - name: Login id: auth - uses: google-github-actions/auth@v0 + uses: google-github-actions/auth@v2 with: token_format: access_token workload_identity_provider: ${{ secrets.GCP_IDENTITY_PROVIDER }} service_account: ${{ secrets.GCP_DEPLOY_SERVICE_ACCOUNT }} - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v0 + uses: google-github-actions/setup-gcloud@v2 with: install_components: beta - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Docker Image Metadata id: meta @@ -156,14 +140,14 @@ jobs: type=sha - name: Login to GAR - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: us-central1-docker.pkg.dev username: oauth2accesstoken password: ${{ steps.auth.outputs.access_token }} - name: Build production image - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: context: . provenance: false diff --git a/.github/workflows/security-audit.yaml b/.github/workflows/security-audit.yaml index 8e9e814..3f1d1fe 100644 --- a/.github/workflows/security-audit.yaml +++ b/.github/workflows/security-audit.yaml @@ -3,10 +3,10 @@ name: security on: push: paths: - - '**/Cargo.toml' - - '**/Cargo.lock' + - "**/Cargo.toml" + - "**/Cargo.lock" schedule: - - cron: '0 0 * * *' + - cron: "0 0 * * *" jobs: audit: @@ -14,6 +14,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - - uses: actions-rs/audit-check@v1 + - uses: rustsec/audit-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/server.yaml b/.github/workflows/server.yaml index 2d6a058..59c9e0c 100644 --- a/.github/workflows/server.yaml +++ b/.github/workflows/server.yaml @@ -6,7 +6,7 @@ on: - main pull_request: branches: - - '*' + - "*" env: CARGO_TERM_COLOR: always @@ -33,38 +33,27 @@ jobs: POSTGRES_HOST: localhost steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup .env run: ./scripts/create_env.sh - - name: Cache dependencies - id: cache-dependencies - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-server-${{ hashFiles('**/Cargo.lock') }} - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: - profile: minimal - components: rustfmt, clippy toolchain: stable - override: true + components: llvm-tools-preview - - name: Check Format - uses: actions-rs/cargo@v1 + - uses: Swatinem/rust-cache@v2 with: - command: fmt - args: --all -- --check + cache-targets: true + + - name: Check Format + run: cargo fmt --all -- --check - uses: taiki-e/install-action@nextest - name: Cache sqlx-cli - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-sqlx with: path: | @@ -72,16 +61,13 @@ jobs: ~/.cargo/bin/cargo-sqlx key: ${{ runner.os }}-sqlx-${{ env.SQLX_VERSION }} - name: Install sqlx-cli - uses: actions-rs/cargo@v1 if: steps.cache-sqlx.outputs.cache-hit == false - with: - command: install - args: | - sqlx-cli - --force - --version=${{ env.SQLX_VERSION }} - --features ${{ env.SQLX_FEATURES }} - --no-default-features + run: | + cargo install sqlx-cli \ + --force \ + --version=${{ env.SQLX_VERSION }} \ + --features ${{ env.SQLX_FEATURES }} \ + --no-default-features \ --locked - name: Migrate database @@ -90,11 +76,9 @@ jobs: ./scripts/init_db.sh - name: Lint - uses: actions-rs/clippy-check@v1 if: github.actor != 'dependabot[bot]' - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: -- -D warnings + run: | + cargo clippy -- -D warnings - name: Check sqlx offline query if: github.actor != 'dependabot[bot]' @@ -121,23 +105,23 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Login id: auth - uses: google-github-actions/auth@v0 + uses: google-github-actions/auth@v2 with: token_format: access_token workload_identity_provider: ${{ secrets.GCP_IDENTITY_PROVIDER }} service_account: ${{ secrets.GCP_DEPLOY_SERVICE_ACCOUNT }} - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v0 + uses: google-github-actions/setup-gcloud@v2 with: install_components: beta - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Docker Image Metadata id: meta @@ -150,14 +134,14 @@ jobs: type=sha - name: Login to GAR - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: us-central1-docker.pkg.dev username: oauth2accesstoken password: ${{ steps.auth.outputs.access_token }} - name: Build production image - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: context: . provenance: false @@ -195,7 +179,7 @@ jobs: --set-secrets=APP__BIGCOMMERCE__CLIENT_ID=APP__BIGCOMMERCE__CLIENT_ID:1 \ --set-secrets=APP__APPLICATION__JWT_SECRET=APP__APPLICATION__JWT_SECRET:1 \ --set-secrets=APP__LIQ_PAY__PUBLIC_KEY=APP__LIQ_PAY__PUBLIC_KEY:2 \ - --set-secrets=APP__LIQ_PAY__PRIVATE_KEY=APP__LIQ_PAY__PRIVATE_KEY:2 + --set-secrets=APP__LIQ_PAY__PRIVATE_KEY=APP__LIQ_PAY__PRIVATE_KEY:2 coverage: name: coverage @@ -219,33 +203,25 @@ jobs: POSTGRES_HOST: localhost steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup .env run: ./scripts/create_env.sh - - name: Cache dependencies - id: cache-dependencies - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-server-${{ hashFiles('**/Cargo.lock') }} - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: - profile: minimal toolchain: stable - override: true components: llvm-tools-preview + - uses: Swatinem/rust-cache@v2 + with: + cache-targets: true + - uses: taiki-e/install-action@cargo-llvm-cov - uses: taiki-e/install-action@nextest - name: Cache sqlx-cli - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-sqlx with: path: | @@ -253,16 +229,13 @@ jobs: ~/.cargo/bin/cargo-sqlx key: ${{ runner.os }}-sqlx-${{ env.SQLX_VERSION }} - name: Install sqlx-cli - uses: actions-rs/cargo@v1 if: steps.cache-sqlx.outputs.cache-hit == false - with: - command: install - args: | - sqlx-cli - --force - --version=${{ env.SQLX_VERSION }} - --features ${{ env.SQLX_FEATURES }} - --no-default-features + run: | + cargo install sqlx-cli \ + --force \ + --version=${{ env.SQLX_VERSION }} \ + --features ${{ env.SQLX_FEATURES }} \ + --no-default-features \ --locked - name: Migrate database @@ -276,7 +249,7 @@ jobs: cargo llvm-cov nextest --all-features --lcov --output-path lcov.info - name: Upload coverage to Coveralls - uses: coverallsapp/github-action@1.1.3 + uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} - path-to-lcov: apps/server/lcov.info + file: apps/server/lcov.info From 838ba662a8945926e8417478038a1746e55a69ff Mon Sep 17 00:00:00 2001 From: Micah Thomas Date: Wed, 28 Feb 2024 23:59:10 -0700 Subject: [PATCH 9/9] fix: jwt validation fix --- apps/server/src/authentication.rs | 3 ++- apps/server/src/bigcommerce/auth.rs | 7 ++++--- apps/server/src/bigcommerce/client.rs | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/server/src/authentication.rs b/apps/server/src/authentication.rs index e3b20e2..2c1c426 100644 --- a/apps/server/src/authentication.rs +++ b/apps/server/src/authentication.rs @@ -78,7 +78,8 @@ pub struct AuthorizedUser(pub String); #[tracing::instrument(name = "decode token")] pub fn decode_token(token: &str, secret: &Secret) -> Result { let key = DecodingKey::from_secret(secret.expose_secret().as_bytes()); - let validation = Validation::new(Algorithm::HS512); + let mut validation = Validation::new(Algorithm::HS512); + validation.validate_aud = false; let decoded = decode::(token, &key, &validation).map_err(Error::InvalidToken)?; trace!(?decoded, "token decoded"); diff --git a/apps/server/src/bigcommerce/auth.rs b/apps/server/src/bigcommerce/auth.rs index feee40f..21ef725 100644 --- a/apps/server/src/bigcommerce/auth.rs +++ b/apps/server/src/bigcommerce/auth.rs @@ -1,14 +1,15 @@ use secrecy::Secret; +use serde::{Deserialize, Serialize}; use super::store::APIToken; -#[derive(serde::Deserialize, serde::Serialize)] +#[derive(Deserialize, Serialize, Debug)] pub struct User { pub id: i32, pub email: String, } -#[derive(serde::Deserialize)] +#[derive(Deserialize)] pub struct OAuthResponse { pub access_token: Secret, pub scope: String, @@ -31,7 +32,7 @@ impl OAuthResponse { } } -#[derive(serde::Deserialize, serde::Serialize)] +#[derive(Deserialize, Serialize, Debug)] pub struct Claims { user: User, owner: User, diff --git a/apps/server/src/bigcommerce/client.rs b/apps/server/src/bigcommerce/client.rs index e27a33e..d8845e3 100644 --- a/apps/server/src/bigcommerce/client.rs +++ b/apps/server/src/bigcommerce/client.rs @@ -179,7 +179,9 @@ impl HttpAPI { pub fn decode_jwt(&self, token: &str) -> Result { let key = DecodingKey::from_secret(self.client_secret.expose_secret().as_bytes()); - let validation = Validation::new(Algorithm::HS256); + let mut validation = Validation::new(Algorithm::HS256); + validation.set_audience(&[&self.client_id]); + let decoded = decode::(token, &key, &validation).map_err(Error::InvalidToken)?; Ok(decoded.claims)