From 2dcb5f91b9639ad6ade3d8ed4907b2f21b7e1644 Mon Sep 17 00:00:00 2001 From: Mahmoud Harmouch Date: Fri, 22 Nov 2024 09:19:51 +0200 Subject: [PATCH 1/2] feat: impl stripe backend api --- .env.example | 2 + .gitignore | 3 + Cargo.lock | 444 +++++++++++++++++++++++--- Cargo.toml | 31 +- src/components/hero.rs | 3 +- src/lib.rs | 2 + src/main.rs | 22 +- src/pages/login.rs | 4 +- src/pay.rs | 18 ++ src/router.rs | 1 - src/server/subscription/controller.rs | 91 +++++- src/server/subscription/request.rs | 5 + 12 files changed, 546 insertions(+), 80 deletions(-) create mode 100644 src/pay.rs diff --git a/.env.example b/.env.example index 5fa5a6d..4536089 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,5 @@ MONGODB_DB_NAME=aibooks JWT_SECRET= GEMINI_API_KEY= UNSPLASH_API_KEY= +STRIPE_SECRET_KEY= +WEBSITE_URL=https://opensass.org \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6022e72..c3f9b80 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ **/*.rs.bk .env + +# Sus files +**/*.br diff --git a/Cargo.lock b/Cargo.lock index 718ae36..9d24266 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -45,6 +45,7 @@ version = "0.1.0" dependencies = [ "anyhow", "argon2", + "async-stripe", "axum", "axum-extra", "bson", @@ -56,19 +57,21 @@ dependencies = [ "dotenv", "futures-util", "gems", - "getrandom", + "getrandom 0.2.15", "gloo-storage 0.3.0", "http-api-isahc-client", "jsonwebtoken", "mongodb", - "rand", - "rand_core", + "rand 0.8.5", + "rand_core 0.6.4", "regex", "serde", + "serde_json", "time", "tokio", + "tower-http 0.6.1", "unsplash-api", - "uuid", + "uuid 1.11.0", "web-sys", ] @@ -152,6 +155,31 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-stripe" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58d670cf4d47a1b8ffef54286a5625382e360a34ee76902fd93ad8c7032a0c30" +dependencies = [ + "chrono", + "futures-util", + "hex", + "hmac", + "http-types", + "hyper 0.14.31", + "hyper-rustls 0.24.2", + "serde", + "serde_json", + "serde_path_to_error", + "serde_qs 0.10.1", + "sha2", + "smart-default", + "smol_str", + "thiserror", + "tokio", + "uuid 0.8.2", +] + [[package]] name = "async-task" version = "4.7.1" @@ -395,12 +423,12 @@ dependencies = [ "indexmap 2.6.0", "js-sys", "once_cell", - "rand", + "rand 0.8.5", "serde", "serde_bytes", "serde_json", "time", - "uuid", + "uuid 1.11.0", ] [[package]] @@ -928,7 +956,7 @@ dependencies = [ "tokio-stream", "tokio-util", "tower 0.4.13", - "tower-http", + "tower-http 0.5.2", "tower-layer", "tracing", "tracing-futures", @@ -1485,9 +1513,7 @@ dependencies = [ [[package]] name = "gems" -version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff15c7c4ac2b96ce67842335998c44e9e57ace31c527b7bd01098d443dda7d2e" +version = "0.0.9" dependencies = [ "base64 0.22.1", "futures-util", @@ -1516,6 +1542,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -1525,7 +1562,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1769,6 +1806,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.6.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.4.1" @@ -1840,7 +1896,7 @@ dependencies = [ "idna 0.4.0", "ipnet", "once_cell", - "rand", + "rand 0.8.5", "thiserror", "tinyvec", "tokio", @@ -1861,7 +1917,7 @@ dependencies = [ "lru-cache", "once_cell", "parking_lot", - "rand", + "rand 0.8.5", "resolv-conf", "smallvec", "thiserror", @@ -1980,6 +2036,27 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" +[[package]] +name = "http-types" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +dependencies = [ + "anyhow", + "async-channel 1.9.0", + "base64 0.13.1", + "futures-lite 1.13.0", + "http 0.2.12", + "infer", + "pin-project-lite", + "rand 0.7.3", + "serde", + "serde_json", + "serde_qs 0.8.5", + "serde_urlencoded", + "url", +] + [[package]] name = "httparse" version = "1.9.5" @@ -2002,7 +2079,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -2025,6 +2102,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.6", "http 1.1.0", "http-body 1.0.1", "httparse", @@ -2033,19 +2111,56 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.31", + "log", + "rustls 0.21.12", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.5.0", + "hyper-util", + "rustls 0.23.16", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tower-service", ] [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", - "hyper 0.14.31", + "http-body-util", + "hyper 1.5.0", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", ] [[package]] @@ -2055,13 +2170,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", + "futures-channel", "futures-util", "http 1.1.0", "http-body 1.0.1", "hyper 1.5.0", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -2264,6 +2382,12 @@ dependencies = [ "serde", ] +[[package]] +name = "infer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" + [[package]] name = "instant" version = "0.1.13" @@ -2561,7 +2685,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -2591,10 +2715,10 @@ dependencies = [ "once_cell", "pbkdf2", "percent-encoding", - "rand", + "rand 0.8.5", "rustc_version_runtime", - "rustls", - "rustls-pemfile", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", "serde", "serde_bytes", "serde_with", @@ -2606,10 +2730,10 @@ dependencies = [ "take_mut", "thiserror", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tokio-util", "typed-builder", - "uuid", + "uuid 1.11.0", "webpki-roots", ] @@ -2812,7 +2936,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -2971,6 +3095,19 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -2978,8 +3115,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -2989,7 +3136,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -2998,7 +3154,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -3041,20 +3206,23 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.31", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.0", + "hyper-rustls 0.27.3", "hyper-tls", + "hyper-util", "ipnet", "js-sys", "log", @@ -3063,11 +3231,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pemfile 2.2.0", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "system-configuration", "tokio", "tokio-native-tls", @@ -3078,7 +3246,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "winreg", + "windows-registry", ] [[package]] @@ -3099,7 +3267,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "spin", "untrusted", @@ -3158,10 +3326,35 @@ checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring", - "rustls-webpki", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.23.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -3171,6 +3364,21 @@ dependencies = [ "base64 0.21.7", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -3181,6 +3389,17 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.18" @@ -3351,9 +3570,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "indexmap 2.6.0", "itoa", @@ -3372,6 +3591,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + +[[package]] +name = "serde_qs" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cac3f1e2ca2fe333923a1ae72caca910b98ed0630bb35ef6f8c8517d6e81afa" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_qs" version = "0.12.0" @@ -3457,7 +3698,7 @@ dependencies = [ "send_wrapper", "serde", "serde_json", - "serde_qs", + "serde_qs 0.12.0", "server_fn_macro_default", "thiserror", "tower 0.4.13", @@ -3630,6 +3871,26 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smart-default" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "smol_str" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.5.7" @@ -3717,6 +3978,9 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -3731,20 +3995,20 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -3912,7 +4176,18 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls", + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.16", + "rustls-pki-types", "tokio", ] @@ -4012,6 +4287,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "http 1.1.0", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -4121,7 +4410,7 @@ dependencies = [ "http 1.1.0", "httparse", "log", - "rand", + "rand 0.8.5", "sha1", "thiserror", "utf-8", @@ -4247,13 +4536,22 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.15", +] + [[package]] name = "uuid" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ - "getrandom", + "getrandom 0.2.15", "serde", ] @@ -4290,6 +4588,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -4429,6 +4733,36 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -4680,6 +5014,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerovec" version = "0.10.4" diff --git a/Cargo.toml b/Cargo.toml index 2e8fa97..b9cdafc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,19 +26,38 @@ anyhow = "1.0.93" time = "0.3.36" regex = "1.11.1" getrandom = { version = "0.2.15", features = ["js"] } -gems = { version = "0.0.7", optional = true } +gems = { version = "0.0.9", optional = true } http-api-isahc-client = { version = "0.2.2", optional = true } axum = { version = "0.7.7", optional = true } unsplash-api = { version = "0.1.0", optional = true } gloo-storage = "0.3.0" - -# Debug -dioxus-logger = "0.5.1" +tower-http = { version = "0.6.1", features = ["cors"], optional = true } dioxus-free-icons = { version = "0.8.6", features = ["font-awesome-regular", "font-awesome-brands", "font-awesome-solid"] } web-sys = { version = "0.3.72", features = ["Selection", "Window"] } dioxus-web = { version = "0.5.6", features = ["hydrate"], optional = true } +async-stripe = { version = "0.39.1", default-feature = false, features = ["runtime-tokio-hyper-rustls", "billing"], optional = true } + +# Debug +dioxus-logger = "0.5.1" +serde_json = "1.0.133" [features] default = [] -server = ["dioxus/axum", "axum", "unsplash-api", "http-api-isahc-client", "tokio", "mongodb", "jsonwebtoken", "argon2", "uuid", "rand", "axum-extra", "rand_core", "gems"] -web = ["dioxus/web", "dioxus-web"] \ No newline at end of file +server = [ + "dioxus/axum", + "axum", + "unsplash-api", + "tower-http", + "http-api-isahc-client", + "tokio", + "mongodb", + "jsonwebtoken", + "argon2", + "uuid", + "rand", + "axum-extra", + "rand_core", + "gems", + "async-stripe" +] +web = ["dioxus/web", "dioxus-web"] diff --git a/src/components/hero.rs b/src/components/hero.rs index e5d08a7..51044c3 100644 --- a/src/components/hero.rs +++ b/src/components/hero.rs @@ -27,7 +27,8 @@ pub fn Hero() -> Element { }, div { class: "flex justify-center space-x-4", - button { + Link { + to: "/login", class: "bg-gray-500 text-white py-2 px-4 rounded-lg shadow hover:bg-gray-600 focus:outline-none", "Get Started" } diff --git a/src/lib.rs b/src/lib.rs index 88402a2..237d3e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,8 @@ pub mod components; #[cfg(feature = "server")] pub mod db; pub(crate) mod pages; +#[cfg(feature = "server")] +pub mod pay; pub mod router; pub(crate) mod server; pub mod theme; diff --git a/src/main.rs b/src/main.rs index 738d19a..1fc3f90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,15 +16,17 @@ fn main() { { let config = dioxus_web::Config::new().hydrate(true); - #[cfg(feature = "web")] LaunchBuilder::new().with_cfg(config).launch(App); } #[cfg(feature = "server")] { use aibook::db::get_client; + use axum::http::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}; + use axum::http::Method; use axum::{Extension, Router}; use std::sync::Arc; + use tower_http::cors::{Any, CorsLayer}; #[derive(Clone)] #[allow(dead_code)] @@ -41,12 +43,26 @@ fn main() { client: client.clone(), }); + let cors = CorsLayer::new() + .allow_origin(Any) + // TODO + // .allow_origin("http://0.0.0.0:3000".parse::().unwrap()) + // .allow_origin( + // "https://generativelanguage.googleapis.com" + // .parse::() + // .unwrap(), + // ) + .allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE]) + // .allow_credentials(true) + .allow_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE]); + let app = Router::new() + .layer(cors) + .layer(Extension(state)) .serve_dioxus_application(ServeConfig::builder().build(), || { VirtualDom::new(App) }) - .await - .layer(Extension(state)); + .await; let addr = std::net::SocketAddr::from(([0, 0, 0, 0], 3000)); let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); diff --git a/src/pages/login.rs b/src/pages/login.rs index a0d9018..08c596f 100644 --- a/src/pages/login.rs +++ b/src/pages/login.rs @@ -26,8 +26,8 @@ pub fn Login() -> Element { let dark_mode = use_context::>(); let mut toasts_manager = use_context::>(); - let mut email = use_signal(|| "".to_string()); - let mut password = use_signal(|| "".to_string()); + let mut email = use_signal(|| "oss@wiseai.dev".to_string()); + let mut password = use_signal(|| "pass".to_string()); let mut error_message = use_signal(|| None::); let mut email_valid = use_signal(|| true); diff --git a/src/pay.rs b/src/pay.rs new file mode 100644 index 0000000..2c0b6e4 --- /dev/null +++ b/src/pay.rs @@ -0,0 +1,18 @@ +use std::env; +use stripe::Client; +use tokio::sync::{Mutex, OnceCell}; + +static PAY: OnceCell> = OnceCell::const_new(); + +async fn init_stripe() -> &'static Mutex { + PAY.get_or_init(|| async { + let client = + Client::new(env::var("STRIPE_SECRET_KEY").expect("STRIPE_SECRET_KEY must be set.")); + Mutex::new(client) + }) + .await +} + +pub async fn get_stripe() -> &'static Mutex { + init_stripe().await +} diff --git a/src/router.rs b/src/router.rs index 77dd795..742872f 100644 --- a/src/router.rs +++ b/src/router.rs @@ -16,7 +16,6 @@ pub enum Route { #[route("/")] Home {}, #[end_layout] - // TODO: file an issue cz of ordering layout and router macros #[layout(LoginNavBar)] #[route("/login")] Login {}, diff --git a/src/server/subscription/controller.rs b/src/server/subscription/controller.rs index 3c023d6..a534df8 100644 --- a/src/server/subscription/controller.rs +++ b/src/server/subscription/controller.rs @@ -6,37 +6,51 @@ use dioxus::prelude::*; #[cfg(feature = "server")] use crate::db::get_client; +#[cfg(feature = "server")] +use crate::pay::get_stripe; use crate::server::auth::model::User; use crate::server::book::model::Book; use crate::server::common::response::SuccessResponse; use crate::server::subscription::model::Subscription; +use crate::server::subscription::request::StripeCancelRequest; use crate::server::subscription::request::StripePaymentRequest; use crate::server::subscription::request::SubscriptionDetailRequest; use crate::server::subscription::request::UpdateSubscriptionRequest; use crate::server::subscription::response::SubscriptionDetailResponse; use chrono::prelude::*; +use std::str::FromStr; +#[cfg(feature = "server")] +use stripe::{ + CheckoutSessionMode, CreateCheckoutSession, CreateCheckoutSessionLineItems, + Subscription as StripeSubscription, SubscriptionId, +}; #[server] pub async fn get_subscription_detail( req: SubscriptionDetailRequest, ) -> Result, ServerFnError> { let client = get_client().await; + let stripe_client = get_stripe().await.lock().await; let db = client.database(&std::env::var("MONGODB_DB_NAME").expect("MONGODB_DB_NAME must be set.")); let sub_collection = db.collection::("subscriptions"); - let subscription = sub_collection + if let Some(subscription) = sub_collection .find_one(doc! { "user": req.user_id }) - .await?; + .await? + { + // TODO: Change to enum val. Right now only stripe is allowed, might add paypal for the pal in the future + if subscription.method == "stripe" { + let subscription_id = SubscriptionId::from_str(subscription.sub_id.as_str()) + .map_err(|_| ServerFnError::new("Invalid subscription ID"))?; + let stripe_sub = + StripeSubscription::retrieve(&stripe_client, &subscription_id, &[]).await?; - if let Some(sub) = subscription { - if sub.method == "stripe" { - let stripe_sub = fetch_stripe_subscription(&sub.sub_id).await?; return Ok(SuccessResponse { status: "success".into(), data: SubscriptionDetailResponse { - session: stripe_sub, - method: sub.method, + session: serde_json::to_string(&stripe_sub)?, + method: subscription.method, }, }); } @@ -47,10 +61,6 @@ pub async fn get_subscription_detail( )) } -async fn fetch_stripe_subscription(subscriber_id: &str) -> Result { - Ok("stripe_session".into()) -} - #[server] pub async fn update_subscription( req: UpdateSubscriptionRequest, @@ -88,13 +98,64 @@ pub async fn update_subscription( pub async fn stripe_payment( req: StripePaymentRequest, ) -> Result, ServerFnError> { - let session_url = create_stripe_session(&req.plan_id).await?; + let stripe_client = get_stripe().await.lock().await; + + let mut session = CreateCheckoutSession::new(); + let success_url = format!( + "{}/success", + std::env::var("WEBSITE_URL").expect("WEBSITE_URL must be set.") + ); + session.success_url = Some(&success_url); + let cancel_url = format!( + "{}/failed", + std::env::var("WEBSITE_URL").expect("WEBSITE_URL must be set.") + ); + session.cancel_url = Some(&cancel_url); + session.mode = Some(CheckoutSessionMode::Subscription); + session.line_items = vec![CreateCheckoutSessionLineItems { + price: Some(req.plan_id.clone()), + quantity: Some(1), + ..Default::default() + }] + .into(); + + let checkout_session = stripe::CheckoutSession::create(&stripe_client, session).await?; + Ok(SuccessResponse { status: "success".into(), - data: session_url, + data: checkout_session.url.unwrap_or_default(), }) } -async fn create_stripe_session(plan_id: &str) -> Result { - Ok("stripe_session_url".into()) +#[server] +pub async fn stripe_cancel( + req: StripeCancelRequest, +) -> Result, ServerFnError> { + let client = get_client().await; + let stripe_client = get_stripe().await.lock().await; + let db = + client.database(&std::env::var("MONGODB_DB_NAME").expect("MONGODB_DB_NAME must be set.")); + let sub_collection = db.collection::("subscriptions"); + let user_collection = db.collection::("users"); + + if let Some(subscription) = sub_collection.find_one(doc! { "subId": &req.id }).await? { + let subscription_id = SubscriptionId::from_str(req.id.as_str()) + .map_err(|_| ServerFnError::new("Invalid subscription ID"))?; + let user_id = subscription.user; + + StripeSubscription::delete(&stripe_client, &subscription_id).await?; + + user_collection + .update_one(doc! { "_id": user_id }, doc! { "$set": { "type": "free" } }) + .await?; + + sub_collection.delete_one(doc! { "subId": &req.id }).await?; + + return Ok(SuccessResponse { + status: "success".into(), + data: "Subscription canceled successfully".into(), + }); + } + + Err(ServerFnError::new("Subscription not found")) } diff --git a/src/server/subscription/request.rs b/src/server/subscription/request.rs index 488c066..baf8fd7 100644 --- a/src/server/subscription/request.rs +++ b/src/server/subscription/request.rs @@ -15,3 +15,8 @@ pub struct UpdateSubscriptionRequest { pub struct StripePaymentRequest { pub plan_id: String, } + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct StripeCancelRequest { + pub id: String, +} From 0e033c91317b36469aef9af2d17baa1d6f4b77fb Mon Sep 17 00:00:00 2001 From: Mahmoud Harmouch Date: Fri, 22 Nov 2024 21:09:26 +0200 Subject: [PATCH 2/2] feat: impl stripe frontend logic --- .env.example | 4 +- Cargo.lock | 2 + Cargo.toml | 14 +++--- src/components/pricing.rs | 65 ++++++++++++++++++++++++++- src/main.rs | 11 ++--- src/pages.rs | 1 + src/pages/success.rs | 52 +++++++++++++++++++++ src/router.rs | 3 ++ src/server/subscription/controller.rs | 2 +- 9 files changed, 138 insertions(+), 16 deletions(-) create mode 100644 src/pages/success.rs diff --git a/.env.example b/.env.example index 4536089..495067f 100644 --- a/.env.example +++ b/.env.example @@ -6,4 +6,6 @@ JWT_SECRET= GEMINI_API_KEY= UNSPLASH_API_KEY= STRIPE_SECRET_KEY= -WEBSITE_URL=https://opensass.org \ No newline at end of file +WEBSITE_URL=https://opensass.org +STRIPE_PRICE_ONE=price_1... +STRIPE_PRICE_TWO=price_1... \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9d24266..bdc13b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1514,6 +1514,8 @@ dependencies = [ [[package]] name = "gems" version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd389a097b342d8d3f510785c00f197e80b52fc92331a330e38b791a5a03b4dd" dependencies = [ "base64 0.22.1", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index b9cdafc..1460619 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,8 @@ serde = { version = "1.0.215", features = ["derive"] } dioxus = { version = "0.5", features = ["fullstack", "router", "html"] } mongodb = { version = "3.1.0", optional = true } -dotenv = { version = "0.15.0" } chrono = { version = "0.4.38", features = ["serde"] } bson = { version = "2.13.0", features = ["chrono-0_4"] } -futures-util = { version = "0.3.31" } jsonwebtoken = { version = "9.3.0", optional = true } argon2 = { version = "0.5.3", optional = true } tokio = { version = "1.41.1", optional = true } @@ -22,24 +20,26 @@ uuid = { version = "1.11.0", optional = true } rand = { version = "0.8.5", optional = true } axum-extra = { version = "0.9.4", features = ["cookie"], optional = true } rand_core = { version = "0.6.4", features = ["std"], optional = true } -anyhow = "1.0.93" -time = "0.3.36" -regex = "1.11.1" getrandom = { version = "0.2.15", features = ["js"] } gems = { version = "0.0.9", optional = true } http-api-isahc-client = { version = "0.2.2", optional = true } axum = { version = "0.7.7", optional = true } unsplash-api = { version = "0.1.0", optional = true } -gloo-storage = "0.3.0" tower-http = { version = "0.6.1", features = ["cors"], optional = true } dioxus-free-icons = { version = "0.8.6", features = ["font-awesome-regular", "font-awesome-brands", "font-awesome-solid"] } web-sys = { version = "0.3.72", features = ["Selection", "Window"] } dioxus-web = { version = "0.5.6", features = ["hydrate"], optional = true } async-stripe = { version = "0.39.1", default-feature = false, features = ["runtime-tokio-hyper-rustls", "billing"], optional = true } +futures-util = { version = "0.3.31" } +dotenv = { version = "0.15.0" } +serde_json = "1.0.133" +anyhow = "1.0.93" +time = "0.3.36" +regex = "1.11.1" +gloo-storage = "0.3.0" # Debug dioxus-logger = "0.5.1" -serde_json = "1.0.133" [features] default = [] diff --git a/src/components/pricing.rs b/src/components/pricing.rs index 4fffc64..aeb993b 100644 --- a/src/components/pricing.rs +++ b/src/components/pricing.rs @@ -1,6 +1,15 @@ use crate::components::common::header::Header; +use crate::components::toast::manager::ToastManager; +use crate::components::toast::manager::ToastType; +use crate::server::subscription::controller::start_stripe_payment; +use crate::server::subscription::request::StripePaymentRequest; use crate::theme::Theme; +use chrono::Duration; use dioxus::prelude::*; +use dioxus_logger::tracing; +use gloo_storage::SessionStorage; +use gloo_storage::Storage; +use std::env; #[derive(Props, Clone, PartialEq)] struct PricingOption { @@ -9,11 +18,15 @@ struct PricingOption { description: &'static str, features: Vec<&'static str>, highlight: bool, + plan_id: Option<&'static str>, } #[component] pub fn Pricing() -> Element { let dark_mode = use_context::>(); + let navigator = use_navigator(); + let mut toasts_manager = use_context::>(); + let pricing_options = vec![ PricingOption { title: "Free", @@ -25,6 +38,7 @@ pub fn Pricing() -> Element { "Google Gemini Pro (limited features)", ], highlight: false, + plan_id: None, }, PricingOption { title: "Monthly", @@ -37,6 +51,7 @@ pub fn Pricing() -> Element { "Priority customer support", ], highlight: true, + plan_id: Some(env::var("STRIPE_PRICE_ONE").expect("STRIPE_PRICE_ONE must be set.")), }, PricingOption { title: "Yearly", @@ -51,8 +66,49 @@ pub fn Pricing() -> Element { "Priority support", ], highlight: false, + plan_id: Some(env::var("STRIPE_PRICE_TWO").expect("STRIPE_PRICE_TWO must be set.")), }, ]; + let handle_plan_selection = move |plan: (Option<&'static str>, &'static str)| { + if let Some(plan_id) = plan.0 { + spawn({ + let plan_title = plan.1.to_string(); + async move { + match start_stripe_payment(StripePaymentRequest { + plan_id: plan_id.to_string(), + }) + .await + { + Ok(response) => { + SessionStorage::set("stripe", response.data.clone()) + .expect("Session storage failed"); + SessionStorage::set("method", "stripe") + .expect("Session storage failed"); + SessionStorage::set("plan", &plan_title) + .expect("Session storage failed"); + toasts_manager.set( + toasts_manager() + .add_toast( + "Info".into(), + "Stripe payment initiation success!".into(), + ToastType::Info, + Some(Duration::seconds(5)), + ) + .clone(), + ); + navigator.push(response.data); + } + Err(err) => { + tracing::error!("Stripe payment initiation failed: {:?}", err); + } + } + } + }); + } else { + navigator.push("/login"); + tracing::info!("Free plan selected."); + } + }; rsx! { section { @@ -95,8 +151,13 @@ pub fn Pricing() -> Element { } }, - button { class: format!("mt-6 w-full py-2 rounded-md font-semibold {}", - if option.highlight { "bg-blue-500 ttext-gray-700 hover:bg-blue-600" } else { "bg-gray-300 text-gray-700 hover:bg-gray-400" }), + button { + class: format!("mt-6 w-full py-2 rounded-md font-semibold {}", + if option.highlight { "bg-blue-500 text-white hover:bg-blue-600" } else { "bg-gray-300 text-gray-700 hover:bg-gray-400" }), + onclick: move |e: Event| { + e.stop_propagation(); + handle_plan_selection((option.plan_id, option.title)); + }, "Select Plan" } } diff --git a/src/main.rs b/src/main.rs index 1fc3f90..efd607d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,15 +5,12 @@ use aibook::router::Route; use aibook::theme::ThemeProvider; use dioxus::prelude::*; use dioxus_logger::tracing; -use dotenv::dotenv; fn main() { - dotenv().ok(); - dioxus_logger::init(tracing::Level::INFO).expect("failed to init logger"); - tracing::info!("starting app"); - #[cfg(feature = "web")] { + dioxus_logger::init(tracing::Level::INFO).expect("failed to init logger"); + tracing::info!("starting app"); let config = dioxus_web::Config::new().hydrate(true); LaunchBuilder::new().with_cfg(config).launch(App); @@ -25,9 +22,13 @@ fn main() { use axum::http::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}; use axum::http::Method; use axum::{Extension, Router}; + use dotenv::dotenv; use std::sync::Arc; use tower_http::cors::{Any, CorsLayer}; + dotenv().ok(); + dioxus_logger::init(tracing::Level::INFO).expect("failed to init logger"); + #[derive(Clone)] #[allow(dead_code)] pub struct AppState { diff --git a/src/pages.rs b/src/pages.rs index 44a6c8f..60e3176 100644 --- a/src/pages.rs +++ b/src/pages.rs @@ -3,3 +3,4 @@ pub(crate) mod dashboard; pub(crate) mod home; pub(crate) mod login; pub(crate) mod signup; +pub(crate) mod success; diff --git a/src/pages/success.rs b/src/pages/success.rs new file mode 100644 index 0000000..a29d7a8 --- /dev/null +++ b/src/pages/success.rs @@ -0,0 +1,52 @@ +use dioxus::prelude::*; + +#[component] +pub fn SuccessPage() -> Element { + rsx! { + section { + class: "min-h-screen flex flex-col justify-center items-center bg-gradient-to-br from-blue-500 via-indigo-600 to-purple-700 text-white", + + div { + class: "text-center space-y-8 justify-center ", + + div { + class: "flex justify-center items-center mx-auto w-24 h-24 bg-white rounded-full text-green-500 shadow-lg animate-bounce", + svg { + xmlns: "http://www.w3.org/2000/svg", + class: "h-12 w-12", + fill: "none", + view_box: "0 0 24 24", + stroke: "currentColor", + stroke_width: "2", + path { + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M5 13l4 4L19 7", + } + } + }, + + h1 { + class: "text-4xl font-bold tracking-tight", + "Payment Successful!" + }, + + p { + class: "text-lg font-light text-gray-200 max-w-md mx-auto", + "Thank you for your payment. Your subscription is now active, and you can enjoy all the premium features we offer. A confirmation email has been sent to your registered email address." + }, + + a { + href: "/dashboard", + class: "inline-block px-8 py-3 bg-green-500 text-white font-semibold text-lg rounded-md shadow-md hover:bg-green-600 transition duration-300 ease-in-out", + "Go to Dashboard" + } + }, + + footer { + class: "mt-10 text-gray-300 text-sm", + "© 2024 AIBook. All rights reserved." + } + } + } +} diff --git a/src/router.rs b/src/router.rs index 742872f..f6a998e 100644 --- a/src/router.rs +++ b/src/router.rs @@ -8,6 +8,7 @@ use crate::pages::dashboard::Dashboard; use crate::pages::home::Home; use crate::pages::login::Login; use crate::pages::signup::Register; +use crate::pages::success::SuccessPage; use dioxus::prelude::*; #[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)] @@ -28,4 +29,6 @@ pub enum Route { EditBook { id: String }, #[route("/dashboard")] Dashboard {}, + #[route("/success")] + SuccessPage {}, } diff --git a/src/server/subscription/controller.rs b/src/server/subscription/controller.rs index a534df8..c0ac851 100644 --- a/src/server/subscription/controller.rs +++ b/src/server/subscription/controller.rs @@ -95,7 +95,7 @@ pub async fn update_subscription( } #[server] -pub async fn stripe_payment( +pub async fn start_stripe_payment( req: StripePaymentRequest, ) -> Result, ServerFnError> { let stripe_client = get_stripe().await.lock().await;