diff --git a/Cargo.lock b/Cargo.lock index e8a5ef982..6a43f1304 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,6 +117,12 @@ dependencies = [ "backtrace", ] +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "arrayvec" version = "0.7.2" @@ -153,13 +159,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.71" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -305,6 +311,49 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-login" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fff85d5ba21f2063b3e8e47ba7192f7d2b73760e3d413db8e7c9b9bea3095b2" +dependencies = [ + "async-trait", + "axum 0.7.4", + "form_urlencoded", + "ring 0.17.7", + "serde", + "thiserror", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions", + "tracing", + "urlencoding", +] + +[[package]] +name = "axum-server" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad46c3ec4e12f4a4b6835e173ba21c25e484c9d02b49770bf006ce5367c036" +dependencies = [ + "arc-swap", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.1.0", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "tokio", + "tokio-rustls", + "tower", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.67" @@ -459,7 +508,7 @@ dependencies = [ "anyhow", "hex", "reqwest", - "ring", + "ring 0.16.20", "serde", "serde_json", "serde_urlencoded", @@ -476,7 +525,7 @@ dependencies = [ "async-stream", "futures", "hex", - "ring", + "ring 0.16.20", "serde", "serde_json", "tokio", @@ -613,9 +662,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] [[package]] name = "cfg-if" @@ -762,6 +814,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "cookie" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "coordinator" version = "1.7.4" @@ -836,9 +899,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -1031,7 +1094,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -1053,7 +1116,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core 0.20.3", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -1084,6 +1147,16 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "diesel" version = "2.0.4" @@ -1363,9 +1436,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -1398,9 +1471,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -1429,9 +1502,9 @@ checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -1452,7 +1525,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -1519,9 +1592,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -2212,6 +2285,7 @@ checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg", "scopeguard", + "serde", ] [[package]] @@ -2855,9 +2929,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -2883,6 +2957,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2933,9 +3013,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.63" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -3041,9 +3121,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.29" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -3204,11 +3284,25 @@ dependencies = [ "libc", "once_cell", "spin 0.5.2", - "untrusted", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.48.0", +] + [[package]] name = "rkyv" version = "0.7.40" @@ -3276,7 +3370,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.23", + "syn 2.0.48", "walkdir", ] @@ -3368,19 +3462,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", - "ring", + "ring 0.16.20", "rustls-webpki 0.101.4", "sct", ] +[[package]] +name = "rustls-pemfile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e4980fa29e4c4b212ffb3db068a564cbf560e51d3944b7c88bd8bf5bec64f4" +dependencies = [ + "base64 0.21.0", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9d979b3ce68192e42760c7810125eb6cf2ea10efae545a156063e61f314e2a" + [[package]] name = "rustls-webpki" version = "0.100.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e98ff011474fa39949b7e5c0428f9b4937eda7da7848bbb947786b7be0b27dab" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -3389,8 +3499,8 @@ version = "0.101.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -3468,8 +3578,8 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -3552,29 +3662,29 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.168" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d614f89548720367ded108b3c843be93f3a341e22d5674ca0dd5cd57f34926af" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.168" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fe589678c688e44177da4f27152ee2d190757271dc7f1d5b6b9f68d869d641" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", @@ -3659,7 +3769,7 @@ dependencies = [ "darling 0.20.3", "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -3819,9 +3929,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.23" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -3906,22 +4016,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.48", ] [[package]] @@ -3945,11 +4055,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.20" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" dependencies = [ + "deranged", "itoa", + "powerfmt", "serde", "time-core", "time-macros", @@ -3957,15 +4069,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.8" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" dependencies = [ "time-core", ] @@ -3987,11 +4099,11 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.2" +version = "1.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" +checksum = "777d57dcc6bb4cf084e3212e1858447222aa451f21b5e2452497d9100da65b91" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", "mio", @@ -3999,7 +4111,7 @@ dependencies = [ "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", - "socket2 0.4.9", + "socket2 0.5.5", "tokio-macros", "tracing", "windows-sys 0.48.0", @@ -4038,7 +4150,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -4063,6 +4175,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -4193,6 +4315,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-cookies" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d" +dependencies = [ + "async-trait", + "axum-core 0.4.3", + "cookie", + "futures-util", + "http 1.0.0", + "parking_lot 0.12.1", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-http" version = "0.5.1" @@ -4230,13 +4369,57 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +[[package]] +name = "tower-sessions" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201aa0cccd55f565d89086d231328877fb897ba01163a224b6b917e2361a6db6" +dependencies = [ + "tower-sessions-core", + "tower-sessions-memory-store", +] + +[[package]] +name = "tower-sessions-core" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12564616434c7c811a229a223552b984f272e772d5dec4f71e61ca5bea7d3a3f" +dependencies = [ + "async-trait", + "axum-core 0.4.3", + "futures", + "http 1.0.0", + "parking_lot 0.12.1", + "serde", + "serde_json", + "thiserror", + "time", + "tokio", + "tower-cookies", + "tower-layer", + "tower-service", + "tracing", + "uuid", +] + +[[package]] +name = "tower-sessions-memory-store" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6800cb4ae79785b1bb413e48813306950902ef902f8ea93fce0a25032e2c254" +dependencies = [ + "async-trait", + "time", + "tokio", + "tower-sessions-core", +] + [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -4245,20 +4428,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.48", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -4410,6 +4593,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "ureq" version = "2.7.1" @@ -4442,9 +4631,9 @@ dependencies = [ [[package]] name = "urlencoding" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "utf-8" @@ -4454,9 +4643,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.3.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ "getrandom", "rand", @@ -4466,13 +4655,13 @@ dependencies = [ [[package]] name = "uuid-macro-internal" -version = "1.3.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b300a878652a387d2a0de915bdae8f1a548f0c6d45e072fe2688794b656cc9" +checksum = "7abb14ae1a50dad63eaa768a458ef43d298cd1bd44951677bd10b732a9ba2a2d" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.48", ] [[package]] @@ -4624,6 +4813,8 @@ dependencies = [ "anyhow", "atty", "axum 0.7.4", + "axum-login", + "axum-server", "bitcoin", "clap", "commons", @@ -4636,6 +4827,7 @@ dependencies = [ "rust_decimal_macros", "serde", "serde_json", + "sha2", "time", "tokio", "tower", diff --git a/justfile b/justfile index ce27836e1..3e3d966c4 100644 --- a/justfile +++ b/justfile @@ -85,6 +85,9 @@ web: web-release: cd webapp/frontend && flutter build web --release +run-web: + cd webapp/frontend && flutter run -d chrome --web-browser-flag "--disable-web-security" + # Build Rust library for iOS (debug mode) ios: cd mobile/native && CARGO_TARGET_DIR=../../target/ios_debug cargo lipo diff --git a/webapp/Cargo.toml b/webapp/Cargo.toml index 1b05ab8f2..7a4577bf2 100644 --- a/webapp/Cargo.toml +++ b/webapp/Cargo.toml @@ -8,6 +8,8 @@ edition = "2021" anyhow = "1" atty = "0.2.14" axum = { version = "0.7", features = ["tracing"] } +axum-login = "0.12.0" +axum-server = { version = "0.6", features = ["tls-rustls"] } bitcoin = "0.29.2" clap = { version = "4", features = ["derive"] } commons = { path = "../crates/commons" } @@ -20,6 +22,7 @@ rust_decimal = { version = "1", features = ["serde-with-float"] } rust_decimal_macros = "1" serde = "1.0.147" serde_json = "1" +sha2 = "0.10" time = "0.3" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tower = { version = "0.4", features = ["util"] } diff --git a/webapp/README.md b/webapp/README.md index 0d8421ff5..3d4a496dd 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -16,9 +16,9 @@ flutter build web ## Run the rust app ```bash -cargo run +cargo run -- --cert-dir certs --data-dir ../data ``` -The webinterface will be reachable under `http://localhost:3001` +The webinterface will be reachable under `https://localhost:3001` Note: if you can't see anything, you probably forgot to run `flutter build web` before diff --git a/webapp/certs/cert.pem b/webapp/certs/cert.pem new file mode 100644 index 000000000..56f30b966 --- /dev/null +++ b/webapp/certs/cert.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIUOzK208NVNruWNbI/0ev22Ln01dswDQYJKoZIhvcNAQEL +BQAwgYYxCzAJBgNVBAYTAlhYMRIwEAYDVQQIDAlTdGF0ZU5hbWUxETAPBgNVBAcM +CENpdHlOYW1lMRQwEgYDVQQKDAtDb21wYW55TmFtZTEbMBkGA1UECwwSQ29tcGFu +eVNlY3Rpb25OYW1lMR0wGwYDVQQDDBRDb21tb25OYW1lT3JIb3N0bmFtZTAeFw0y +NDAxMjIxMzA1MjBaFw0zNDAxMTkxMzA1MjBaMIGGMQswCQYDVQQGEwJYWDESMBAG +A1UECAwJU3RhdGVOYW1lMREwDwYDVQQHDAhDaXR5TmFtZTEUMBIGA1UECgwLQ29t +cGFueU5hbWUxGzAZBgNVBAsMEkNvbXBhbnlTZWN0aW9uTmFtZTEdMBsGA1UEAwwU +Q29tbW9uTmFtZU9ySG9zdG5hbWUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQDEQeFii40f/4BvccPIaEjW/HuC5V5hK7iEdQKaIg/Fo0R8040R6dIjq6yB +hzqQu55GzctjrsfvdyvmT4N4S0bK/gdeG4PPlkh/t5jWZmV7eI9XoedjT6UnrQYb +hh2sAuSoK4Snc8H40SlexLngRYHLK7Wu0xbip94NOGU9x3s8mvNB7v04aVeEPER/ +6q0GPjPxNHQi3pYD9v1PdRM5sjYch6zOBUsVAaam7jj2UgF38ERxXqd2JNKDZovx +wyYmmWxfjxx3LmH94d69XkwBR/QUZk9xKIAGaKyceA3G3Vu22bjQd6zR2Eefy337 +ZpvqHlqt82Zhj139TeR/cfJt6haaHZoedMI4QcaRacIFDZUq6snfftwP0QLQ2y12 +MN5mPI8bAFwIk2LyZdKXDSWxan61RT+twCJJ4S8KFryFU6MC6OScf+Eh9D5yhe12 +hdFftcbVcuwp5vv6FE2GgsMQQe3rC2C9w+/ag9rcIXn1tdnfBQt8YxEzFkFPtoOP +2q1LURSwy5LYwN6y/vv5B7vi6NWDONZuz+1ZMBTfSCMrI9NfQsHnP1dMmxbVa/Dx +JB4opHb5mXWBwrob77X2UaB+gHquWGf4spLa7TJ5Gcd+p6bcFtidWg9aFkK8VV11 +X9GUQBD/wT8R0+gE1rbnBHzAM28x09B+oO70KskRM5ITXOTvjwIDAQABo1MwUTAd +BgNVHQ4EFgQURPD83wu33omr4giNGe17BaztDPIwHwYDVR0jBBgwFoAURPD83wu3 +3omr4giNGe17BaztDPIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEAT/c/AJ+i86wlrin+nPoyAS9TpkN2YJ+6knlNApg17KfYZ0mmEHEI8pve+8jJ +wiEXtT0fkCfy6CtuNuwGF72L68bJU5DsaGCLviP53vmCRkm9KbQ/BwqyiJQQ9aiU +V/Ue1wvGhJy262XkPC+xvjbT6FISaq093OpIOBZYPBERSTCDdvygFlBmjB74CFKq ++RvBgZbOYmkYyRwGeiI54wLw1I1NGP+yM10vEgrHvsPV/GtM9kOvSnS7GAnq6+oH +ji64KZzDEqrMdKRbfapGGTIK6OOax1RAnvNjAWteLwlRBOvf4t8E/rylpdPFprBz +mfnaok/8LXWaM8ax1CA1mtqET3cz2jzT2rG9+uI+NU4xmnykWQisNRZEe+3vT+O+ +dRRXdyRoL2sSL8PLQeMA8NVdglpiQILB4KnZr/8m0qcne/IAy6yqQT9YGaCn/Kdb +Y5gI1jvnUOqTJU0oEft6hynC+k/f7EWqy6fvFNPFKgnLzl3ahOQzIfoxUUyCzErA +5X61TJj1PEDyLNVuNqb1O3sfDCPKLGDvySO5jPc6ZOlRJp1ajSFWTIUjn5Keizd1 +jrDNgUbW5KhEVjcx9mrzFMWYZgimGRIMFHfv96+WleWDdDeAdFAPRvnvcUX++VyG +NPb3/LNEu9DMBb6Hhzhousibzcqpuykvy9Xi4pRBjXjwXf4= +-----END CERTIFICATE----- diff --git a/webapp/certs/key.pem b/webapp/certs/key.pem new file mode 100644 index 000000000..8544100dd --- /dev/null +++ b/webapp/certs/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +SNUpDciiMSMdODrlzWzX9El2ftWq +-----END PRIVATE KEY----- diff --git a/webapp/frontend/lib/auth/auth_service.dart b/webapp/frontend/lib/auth/auth_service.dart new file mode 100644 index 000000000..77d16411b --- /dev/null +++ b/webapp/frontend/lib/auth/auth_service.dart @@ -0,0 +1,34 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:get_10101/common/http_client.dart'; +import 'package:get_10101/logger/logger.dart'; + +class AuthService { + Future isLoggedIn() async { + try { + final response = await HttpClientManager.instance.get(Uri(path: '/api/version')); + return response.statusCode == 200; + } catch (error) { + return false; + } + } + + Future signIn(String password) async { + final response = await HttpClientManager.instance.post(Uri(path: '/api/login'), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: jsonEncode({'password': password})); + + if (response.statusCode != 200) { + throw FlutterError("Failed to login"); + } + + logger.i("Successfully logged in!"); + } + + Future signOut() async { + await HttpClientManager.instance.get(Uri(path: '/api/logout')); + } +} diff --git a/webapp/frontend/lib/auth/login_screen.dart b/webapp/frontend/lib/auth/login_screen.dart new file mode 100644 index 000000000..97b513d35 --- /dev/null +++ b/webapp/frontend/lib/auth/login_screen.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:get_10101/auth/auth_service.dart'; +import 'package:get_10101/common/snack_bar.dart'; +import 'package:get_10101/common/text_input_field.dart'; +import 'package:get_10101/trade/trade_screen.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +class LoginScreen extends StatefulWidget { + static const route = "/login"; + + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + String _password = ""; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset('assets/10101_logo_icon.png', width: 350, height: 350), + SizedBox( + width: 500, + height: 150, + child: Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Colors.grey[100], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + TextInputField( + value: "", + label: "Password", + obscureText: true, + onSubmitted: (value) => value.isNotEmpty ? signIn(context, value) : (), + onChanged: (value) => setState(() => _password = value), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: _password.isEmpty ? null : () => signIn(context, _password), + child: Container( + padding: const EdgeInsets.all(10), + child: const Text( + "Sign in", + style: TextStyle(fontSize: 16), + ))) + ]), + )), + ], + ); + } +} + +void signIn(BuildContext context, String password) { + final authService = context.read(); + authService + .signIn(password) + .then((value) => GoRouter.of(context).go(TradeScreen.route)) + .catchError((error) { + final messenger = ScaffoldMessenger.of(context); + showSnackBar(messenger, error?.toString() ?? "Failed to login!"); + }); +} diff --git a/webapp/frontend/lib/common/http_client.dart b/webapp/frontend/lib/common/http_client.dart index 89b5caaf1..ceb477a68 100644 --- a/webapp/frontend/lib/common/http_client.dart +++ b/webapp/frontend/lib/common/http_client.dart @@ -1,13 +1,15 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:http/browser_client.dart'; import 'package:http/http.dart'; class HttpClientManager { - static final CustomHttpClient _httpClient = CustomHttpClient(Client(), true); + static final CustomHttpClient _httpClient = CustomHttpClient(Client(), kDebugMode); static CustomHttpClient get instance => _httpClient; } -class CustomHttpClient extends BaseClient { +class CustomHttpClient extends BrowserClient { // TODO: this should come from the settings // if this is true, we assume the website is running in dev mode and need to add _host:_port to be able to do http calls @@ -18,8 +20,11 @@ class CustomHttpClient extends BaseClient { final Client _inner; - CustomHttpClient(this._inner, this._dev); + CustomHttpClient(this._inner, this._dev) { + super.withCredentials = true; + } + @override Future send(BaseRequest request) { return _inner.send(request); } diff --git a/webapp/frontend/lib/common/scaffold_with_nav.dart b/webapp/frontend/lib/common/scaffold_with_nav.dart index a7c26d408..025a1e48f 100644 --- a/webapp/frontend/lib/common/scaffold_with_nav.dart +++ b/webapp/frontend/lib/common/scaffold_with_nav.dart @@ -1,5 +1,10 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:get_10101/auth/auth_service.dart'; +import 'package:get_10101/auth/login_screen.dart'; import 'package:get_10101/common/balance.dart'; +import 'package:get_10101/common/snack_bar.dart'; import 'package:get_10101/common/version_service.dart'; import 'package:get_10101/logger/logger.dart'; import 'package:get_10101/trade/orderbook_service.dart'; @@ -42,6 +47,11 @@ class _ScaffoldWithNestedNavigation extends State Balance balance = Balance.zero(); BestQuote? bestQuote; + Timer? _timeout; + + // sets the timeout until the user will get automatically logged out after inactivity. + final _inactivityTimout = const Duration(minutes: 5); + void _goBranch(int index) { widget.navigationShell.goBranch( index, @@ -66,10 +76,25 @@ class _ScaffoldWithNestedNavigation extends State })); } + @override + void dispose() { + super.dispose(); + _timeout?.cancel(); + } + @override Widget build(BuildContext context) { final navigationShell = widget.navigationShell; + final authService = context.read(); + + if (_timeout != null) _timeout!.cancel(); + _timeout = Timer(_inactivityTimout, () { + logger.i("Signing out due to inactivity"); + authService.signOut(); + GoRouter.of(context).go(LoginScreen.route); + }); + if (showNavigationDrawer) { return ScaffoldWithNavigationRail( body: navigationShell, @@ -179,33 +204,53 @@ class ScaffoldWithNavigationRail extends StatelessWidget { padding: const EdgeInsets.all(25), child: Row( children: [ - TopBarItem(label: 'Latest Bid: ', value: [ - TextSpan( - text: bestQuote?.bid?.toString(), - style: const TextStyle(fontWeight: FontWeight.bold), - ) - ]), - const SizedBox(width: 30), - TopBarItem(label: 'Latest Ask: ', value: [ - TextSpan( - text: bestQuote?.ask?.toString(), - style: const TextStyle(fontWeight: FontWeight.bold), - ) - ]), - const SizedBox(width: 30), - TopBarItem(label: 'Off-chain: ', value: [ - TextSpan( - text: balance.offChain.formatted(), - style: const TextStyle(fontWeight: FontWeight.bold)), - const TextSpan(text: " sats"), - ]), - const SizedBox(width: 30), - TopBarItem(label: 'On-chain: ', value: [ - TextSpan( - text: balance.onChain.formatted(), - style: const TextStyle(fontWeight: FontWeight.bold)), - const TextSpan(text: " sats"), - ]), + Row( + children: [ + TopBarItem(label: 'Latest Bid: ', value: [ + TextSpan( + text: bestQuote?.bid?.toString(), + style: const TextStyle(fontWeight: FontWeight.bold), + ) + ]), + const SizedBox(width: 30), + TopBarItem(label: 'Latest Ask: ', value: [ + TextSpan( + text: bestQuote?.ask?.toString(), + style: const TextStyle(fontWeight: FontWeight.bold), + ) + ]), + const SizedBox(width: 30), + TopBarItem(label: 'Off-chain: ', value: [ + TextSpan( + text: balance.offChain.formatted(), + style: const TextStyle(fontWeight: FontWeight.bold)), + const TextSpan(text: " sats"), + ]), + const SizedBox(width: 30), + TopBarItem(label: 'On-chain: ', value: [ + TextSpan( + text: balance.onChain.formatted(), + style: const TextStyle(fontWeight: FontWeight.bold)), + const TextSpan(text: " sats"), + ]), + ], + ), + Expanded( + child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + ElevatedButton( + onPressed: () { + context + .read() + .signOut() + .then((value) => GoRouter.of(context).go(LoginScreen.route)) + .catchError((error) { + final messenger = ScaffoldMessenger.of(context); + showSnackBar(messenger, error); + }); + }, + child: const Text("Sign out")) + ]), + ), ], ), ), diff --git a/webapp/frontend/lib/common/text_input_field.dart b/webapp/frontend/lib/common/text_input_field.dart index 24d84e7cd..6e6b318fe 100644 --- a/webapp/frontend/lib/common/text_input_field.dart +++ b/webapp/frontend/lib/common/text_input_field.dart @@ -7,11 +7,13 @@ class TextInputField extends StatelessWidget { this.label = '', this.hint = '', this.onChanged, + this.onSubmitted, required this.value, this.controller, this.validator, this.decoration, this.style, + this.obscureText = false, this.onTap}); final TextEditingController? controller; @@ -21,8 +23,10 @@ class TextInputField extends StatelessWidget { final String label; final String hint; final Function(String)? onChanged; + final Function(String)? onSubmitted; final Function()? onTap; final InputDecoration? decoration; + final bool obscureText; final String? Function(String?)? validator; @@ -33,6 +37,7 @@ class TextInputField extends StatelessWidget { enabled: enabled, controller: controller, initialValue: controller != null ? null : value, + obscureText: obscureText, decoration: decoration ?? InputDecoration( border: const OutlineInputBorder(), @@ -47,6 +52,7 @@ class TextInputField extends StatelessWidget { ), onChanged: (value) => {if (onChanged != null) onChanged!(value)}, onTap: onTap, + onFieldSubmitted: (value) => {if (onSubmitted != null) onSubmitted!(value)}, validator: (value) { if (validator != null) { return validator!(value); diff --git a/webapp/frontend/lib/main.dart b/webapp/frontend/lib/main.dart index 50f1fd335..ad4530243 100644 --- a/webapp/frontend/lib/main.dart +++ b/webapp/frontend/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:get_10101/auth/auth_service.dart'; import 'package:get_10101/common/version_service.dart'; import 'package:get_10101/logger/logger.dart'; import 'package:get_10101/routes.dart'; @@ -19,7 +20,9 @@ void main() { Provider(create: (context) => const VersionService()), Provider(create: (context) => const WalletService()), Provider(create: (context) => const QuoteService()), - Provider(create: (context) => const SettingsService()) + Provider(create: (context) => const SettingsService()), + Provider(create: (context) => const WalletService()), + Provider(create: (context) => AuthService()) ]; runApp(MultiProvider(providers: providers, child: const TenTenOneApp())); } @@ -35,11 +38,6 @@ class _TenTenOneAppState extends State { final GlobalKey scaffoldMessengerKey = GlobalKey(); - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { MaterialColor swatch = tenTenOnePurple; diff --git a/webapp/frontend/lib/routes.dart b/webapp/frontend/lib/routes.dart index c729479f2..020d84d40 100644 --- a/webapp/frontend/lib/routes.dart +++ b/webapp/frontend/lib/routes.dart @@ -1,16 +1,37 @@ import 'package:flutter/material.dart'; +import 'package:get_10101/auth/auth_service.dart'; +import 'package:get_10101/auth/login_screen.dart'; import 'package:get_10101/common/global_keys.dart'; import 'package:get_10101/common/scaffold_with_nav.dart'; import 'package:get_10101/settings/settings_screen.dart'; import 'package:get_10101/trade/trade_screen.dart'; import 'package:get_10101/wallet/wallet_screen.dart'; import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; final goRouter = GoRouter( + redirect: (context, state) async { + final isLoggedIn = await context.read().isLoggedIn(); + final isLoginRoute = state.matchedLocation == LoginScreen.route; + + if (!isLoggedIn && !isLoginRoute) { + return LoginScreen.route; + } else if (isLoggedIn && isLoginRoute) { + return TradeScreen.route; + } + + return null; + }, navigatorKey: rootNavigatorKey, initialLocation: TradeScreen.route, debugLogDiagnostics: true, routes: [ + GoRoute( + path: LoginScreen.route, + pageBuilder: (context, state) => NoTransitionPage( + child: routeChild(const LoginScreen()), + ), + ), StatefulShellRoute.indexedStack( builder: (context, state, navigationShell) { return ScaffoldWithNestedNavigation(navigationShell: navigationShell); diff --git a/webapp/frontend/lib/trade/order_and_position_table.dart b/webapp/frontend/lib/trade/order_and_position_table.dart index af8c56dac..655df7492 100644 --- a/webapp/frontend/lib/trade/order_and_position_table.dart +++ b/webapp/frontend/lib/trade/order_and_position_table.dart @@ -82,53 +82,51 @@ class OpenPositionTable extends StatelessWidget { } Widget buildTable(List positions) { - return Container( - child: Table( - border: TableBorder.symmetric(inside: const BorderSide(width: 2, color: Colors.black)), - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - columnWidths: const { - 0: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)), - 1: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)), - 2: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)), - 3: MinColumnWidth(FixedColumnWidth(150.0), FractionColumnWidth(0.1)), - 4: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)), - 5: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)), - 6: MinColumnWidth(FixedColumnWidth(200.0), FractionColumnWidth(0.2)), - }, - children: [ - TableRow( - decoration: BoxDecoration( - color: tenTenOnePurple.shade300, - border: Border.all( - width: 1, - ), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(10), topRight: Radius.circular(10)), + return Table( + border: TableBorder.symmetric(inside: const BorderSide(width: 2, color: Colors.black)), + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + columnWidths: const { + 0: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)), + 1: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)), + 2: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)), + 3: MinColumnWidth(FixedColumnWidth(150.0), FractionColumnWidth(0.1)), + 4: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)), + 5: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)), + 6: MinColumnWidth(FixedColumnWidth(200.0), FractionColumnWidth(0.2)), + }, + children: [ + TableRow( + decoration: BoxDecoration( + color: tenTenOnePurple.shade300, + border: Border.all( + width: 1, ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10), topRight: Radius.circular(10)), + ), + children: [ + buildHeaderCell('Quantity'), + buildHeaderCell('Entry Price'), + buildHeaderCell('Liquidation Price'), + buildHeaderCell('Margin'), + buildHeaderCell('Leverage'), + buildHeaderCell('Unrealized PnL'), + buildHeaderCell('Expiry'), + ], + ), + for (var position in positions) + TableRow( children: [ - buildHeaderCell('Quantity'), - buildHeaderCell('Entry Price'), - buildHeaderCell('Liquidation Price'), - buildHeaderCell('Margin'), - buildHeaderCell('Leverage'), - buildHeaderCell('Unrealized PnL'), - buildHeaderCell('Expiry'), + buildTableCell(position.quantity.toString()), + buildTableCell(position.averageEntryPrice.toString()), + buildTableCell(position.liquidationPrice.toString()), + buildTableCell(position.collateral.toString()), + buildTableCell(position.leverage.formatted()), + buildTableCell(position.pnlSats.toString()), + buildTableCell("${DateFormat('dd-MM-yyyy – HH:mm').format(position.expiry)} CET"), ], ), - for (var position in positions) - TableRow( - children: [ - buildTableCell(position.quantity.toString()), - buildTableCell(position.averageEntryPrice.toString()), - buildTableCell(position.liquidationPrice.toString()), - buildTableCell(position.collateral.toString()), - buildTableCell(position.leverage.formatted()), - buildTableCell(position.pnlSats.toString()), - buildTableCell("${DateFormat('dd-MM-yyyy – HH:mm').format(position.expiry)} CET"), - ], - ), - ], - ), + ], ); } diff --git a/webapp/frontend/lib/trade/orderbook_service.dart b/webapp/frontend/lib/trade/orderbook_service.dart index 6f1b74059..30b70d860 100644 --- a/webapp/frontend/lib/trade/orderbook_service.dart +++ b/webapp/frontend/lib/trade/orderbook_service.dart @@ -1,10 +1,7 @@ -import 'package:decimal/decimal.dart'; import 'package:get_10101/common/http_client.dart'; import 'package:get_10101/common/model.dart'; import 'dart:convert'; -import 'package:get_10101/logger/logger.dart'; - class BestQuote { Price? bid; Price? ask; diff --git a/webapp/frontend/lib/trade/trade_screen.dart b/webapp/frontend/lib/trade/trade_screen.dart index fd712dd8f..c17fe3056 100644 --- a/webapp/frontend/lib/trade/trade_screen.dart +++ b/webapp/frontend/lib/trade/trade_screen.dart @@ -191,12 +191,7 @@ class NewOrderWidget extends StatelessWidget { width: 300, child: TabBarView( controller: _tabController, - children: [ - NewOrderForm(isLong: true), - NewOrderForm( - isLong: false, - ) - ], + children: const [NewOrderForm(isLong: true), NewOrderForm(isLong: false)], ), ), ], diff --git a/webapp/frontend/lib/trade/trade_screen_order_form.dart b/webapp/frontend/lib/trade/trade_screen_order_form.dart index b28ff99bb..519cefdd7 100644 --- a/webapp/frontend/lib/trade/trade_screen_order_form.dart +++ b/webapp/frontend/lib/trade/trade_screen_order_form.dart @@ -5,7 +5,6 @@ import 'package:get_10101/common/amount_text_input_form_field.dart'; import 'package:get_10101/common/model.dart'; import 'package:get_10101/common/snack_bar.dart'; import 'package:get_10101/common/theme.dart'; -import 'package:get_10101/logger/logger.dart'; import 'package:get_10101/trade/new_order_service.dart'; import 'package:get_10101/trade/orderbook_service.dart'; import 'package:provider/provider.dart'; diff --git a/webapp/frontend/lib/wallet/wallet_service.dart b/webapp/frontend/lib/wallet/wallet_service.dart index 4578e678f..ebb112601 100644 --- a/webapp/frontend/lib/wallet/wallet_service.dart +++ b/webapp/frontend/lib/wallet/wallet_service.dart @@ -5,7 +5,6 @@ import 'package:get_10101/common/http_client.dart'; import 'package:get_10101/common/model.dart'; import 'package:get_10101/common/balance.dart'; import 'package:get_10101/common/payment.dart'; -import 'package:get_10101/logger/logger.dart'; class WalletService { const WalletService(); diff --git a/webapp/src/api.rs b/webapp/src/api.rs index 83e1fc544..022a70b35 100644 --- a/webapp/src/api.rs +++ b/webapp/src/api.rs @@ -6,7 +6,10 @@ use axum::extract::State; use axum::http::StatusCode; use axum::response::IntoResponse; use axum::response::Response; +use axum::routing::get; +use axum::routing::post; use axum::Json; +use axum::Router; use commons::Price; use native::api::ContractSymbol; use native::api::Direction; @@ -27,6 +30,21 @@ use std::sync::Arc; use time::OffsetDateTime; use uuid::Uuid; +pub fn router(subscribers: Arc) -> Router { + Router::new() + .route("/api/version", get(version)) + .route("/api/balance", get(get_balance)) + .route("/api/newaddress", get(get_unused_address)) + .route("/api/sendpayment", post(send_payment)) + .route("/api/history", get(get_onchain_payment_history)) + .route("/api/orders", post(post_new_order)) + .route("/api/positions", get(get_positions)) + .route("/api/quotes/:contract_symbol", get(get_best_quote)) + .route("/api/node", get(get_node_id)) + .route("/api/seed", get(get_seed_phrase)) + .with_state(subscribers) +} + pub struct AppError(anyhow::Error); impl IntoResponse for AppError { diff --git a/webapp/src/auth.rs b/webapp/src/auth.rs new file mode 100644 index 000000000..a8a37cdcf --- /dev/null +++ b/webapp/src/auth.rs @@ -0,0 +1,128 @@ +use axum::async_trait; +use axum::routing::get; +use axum::routing::post; +use axum::Router; +use axum_login::AuthUser; +use axum_login::AuthnBackend; +use axum_login::UserId; +use bitcoin::hashes::hex::ToHex; +use serde::Deserialize; +use sha2::digest::FixedOutput; +use sha2::Digest; +use sha2::Sha256; +use std::error::Error; +use std::fmt::Display; +use std::fmt::Formatter; + +#[derive(Clone)] +pub struct Backend { + pub(crate) hashed_password: String, +} + +#[derive(Clone, Debug)] +pub struct User { + password: String, +} + +#[derive(Clone, Deserialize)] +pub struct Credentials { + pub password: String, +} + +#[derive(std::fmt::Debug)] +pub struct BackendError(String); + +impl Display for BackendError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.to_string().fmt(f) + } +} + +impl Error for BackendError {} + +#[async_trait] +impl AuthnBackend for Backend { + type User = User; + type Credentials = Credentials; + type Error = BackendError; + + async fn authenticate( + &self, + creds: Self::Credentials, + ) -> Result, Self::Error> { + let mut hasher = Sha256::new(); + hasher.update(creds.password.as_bytes()); + let hashed_password = hasher.finalize_fixed().to_hex(); + + let user = match hashed_password == self.hashed_password { + true => Some(User { + password: self.hashed_password.clone(), + }), + false => None, + }; + + Ok(user) + } + + async fn get_user(&self, _user_id: &UserId) -> Result, Self::Error> { + Ok(Some(User { + password: self.hashed_password.clone(), + })) + } +} + +impl AuthUser for User { + type Id = u64; + + fn id(&self) -> Self::Id { + 0 + } + + fn session_auth_hash(&self) -> &[u8] { + self.password.as_bytes() + } +} + +pub fn router() -> Router { + Router::new() + .route("/api/login", post(post::login)) + .route("/api/logout", get(get::logout)) +} + +mod post { + use super::*; + use axum::http::StatusCode; + use axum::response::IntoResponse; + use axum::Json; + use axum_login::AuthSession; + + pub async fn login( + mut auth_session: AuthSession, + creds: Json, + ) -> impl IntoResponse { + let user = match auth_session.authenticate(creds.0).await { + Ok(Some(user)) => user, + Ok(None) => { + return StatusCode::UNAUTHORIZED.into_response(); + } + Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), + }; + + if auth_session.login(&user).await.is_err() { + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + + StatusCode::OK.into_response() + } +} + +mod get { + use crate::api::AppError; + use crate::auth::Backend; + use axum_login::AuthSession; + + pub async fn logout(mut auth_session: AuthSession) -> Result<(), AppError> { + auth_session.logout().await?; + Ok(()) + } +} diff --git a/webapp/src/cli.rs b/webapp/src/cli.rs index d9d958b20..7c48e1198 100644 --- a/webapp/src/cli.rs +++ b/webapp/src/cli.rs @@ -1,6 +1,10 @@ use anyhow::ensure; use anyhow::Result; +use bitcoin::hashes::hex::ToHex; use clap::Parser; +use sha2::digest::FixedOutput; +use sha2::Digest; +use sha2::Sha256; use std::env::current_dir; use std::path::PathBuf; use std::str::FromStr; @@ -33,6 +37,16 @@ pub struct Opts { default_value = "16f88cf7d21e6c0f46bcbc983a4e3b19726c6c98858cc31c83551a88fde171c0@http://127.0.0.1:8081" )] oracle: String, + + /// Where to find the cert and key pem files + #[clap(long)] + cert_dir: Option, + + #[clap(long, default_value = "satoshi")] + password: String, + + #[clap(long)] + pub secure: bool, } #[derive(Debug, Clone, Copy, clap::ValueEnum)] @@ -64,6 +78,12 @@ impl Opts { self.network.into() } + pub fn password(&self) -> String { + let mut hasher = Sha256::new(); + hasher.update(self.password.as_bytes()); + hasher.finalize_fixed().to_hex() + } + pub fn data_dir(&self) -> Result { let data_dir = match self.data_dir.clone() { None => current_dir()?.join("data"), @@ -74,6 +94,15 @@ impl Opts { Ok(data_dir) } + pub fn cert_dir(&self) -> Result { + let cert_dir = match self.cert_dir.clone() { + None => current_dir()?.join("webapp/certs"), + Some(path) => path, + }; + + Ok(cert_dir) + } + pub fn coordinator_pubkey(&self) -> Result { let coordinator: Vec<&str> = self.coordinator_endpoint.split('@').collect(); ensure!(coordinator.len() == 2, "invalid coordinator endpoint"); diff --git a/webapp/src/logger.rs b/webapp/src/logger.rs index a6a6da63f..2ce2611b4 100644 --- a/webapp/src/logger.rs +++ b/webapp/src/logger.rs @@ -24,6 +24,8 @@ pub fn init_tracing(level: LevelFilter, json_format: bool, tokio_console: bool) .add_directive("hyper=warn".parse()?) .add_directive("rustls=warn".parse()?) .add_directive("sled=warn".parse()?) + .add_directive("h2=warn".parse()?) + .add_directive("axum_login=warn".parse()?) .add_directive("bdk=warn".parse()?) // bdk is quite spamy on debug .add_directive("lightning_transaction_sync=warn".parse()?) .add_directive("lightning::ln::peer_handler=debug".parse()?) diff --git a/webapp/src/main.rs b/webapp/src/main.rs index 2decebe2a..8e25b07f3 100644 --- a/webapp/src/main.rs +++ b/webapp/src/main.rs @@ -1,19 +1,13 @@ mod api; +mod auth; mod cli; mod logger; +mod session; mod subscribers; -use crate::api::get_balance; -use crate::api::get_best_quote; -use crate::api::get_node_id; -use crate::api::get_onchain_payment_history; -use crate::api::get_positions; -use crate::api::get_seed_phrase; -use crate::api::get_unused_address; -use crate::api::post_new_order; -use crate::api::send_payment; -use crate::api::version; +use crate::auth::Backend; use crate::cli::Opts; +use crate::session::InMemorySessionStore; use crate::subscribers::AppSubscribers; use anyhow::Context; use anyhow::Result; @@ -25,8 +19,12 @@ use axum::response::Html; use axum::response::IntoResponse; use axum::response::Response; use axum::routing::get; -use axum::routing::post; use axum::Router; +use axum_login::login_required; +use axum_login::tower_sessions::Expiry; +use axum_login::tower_sessions::SessionManagerLayer; +use axum_login::AuthManagerLayerBuilder; +use axum_server::tls_rustls::RustlsConfig; use bitcoin::Network; use rust_embed::RustEmbed; use std::net::SocketAddr; @@ -54,13 +52,18 @@ async fn main() -> Result<()> { let data_dir = data_dir.clone().to_string_lossy().to_string(); tracing::info!("Data-dir: {data_dir:?}"); + let cert_dir = opts.cert_dir()?; + tracing::info!("Cert-dir: {cert_dir:?}"); + let coordinator_endpoint = opts.coordinator_endpoint()?; let coordinator_p2p_port = opts.coordinator_p2p_port()?; let coordinator_pubkey = opts.coordinator_pubkey()?; let oracle_endpoint = opts.oracle_endpoint()?; let oracle_pubkey = opts.oracle_pubkey()?; + let password = opts.password(); let coordinator_http_port = opts.coordinator_http_port; let esplora_endpoint = opts.esplora; + let secure = opts.secure; let config = native::config::api::Config { coordinator_pubkey, @@ -87,24 +90,58 @@ async fn main() -> Result<()> { let (rx, tx) = AppSubscribers::new().await; native::event::subscribe(tx); - serve(using_serve_dir(Arc::new(rx), network), 3001).await?; + // configure certificate and private key used by https + let config = + RustlsConfig::from_pem_file(cert_dir.join("cert.pem"), cert_dir.join("key.pem")).await?; + + let session_store = InMemorySessionStore::new(); + let deletion_task = tokio::task::spawn( + session_store + .clone() + .continuously_delete_expired(Duration::from_secs(60)), + ); + + let session_layer = SessionManagerLayer::new(session_store.clone()) + .with_secure(matches!(network, Network::Bitcoin)) + .with_expiry(Expiry::OnInactivity(time::Duration::hours(1))); + + let auth_layer = AuthManagerLayerBuilder::new( + Backend { + hashed_password: password, + }, + session_layer, + ) + .build(); + + let app = api::router(Arc::new(rx)) + .route_layer(login_required!(Backend)) + .merge(auth::router()) + .merge(router(network)) + .layer(auth_layer); + + // run https server + let addr = SocketAddr::from(([0, 0, 0, 0], 3001)); + tracing::debug!("listening on {}", addr); + match secure { + false => { + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app.into_make_service()).await + } + true => { + axum_server::bind_rustls(addr, config) + .serve(app.into_make_service()) + .await + } + }?; + + deletion_task.await??; Ok(()) } -fn using_serve_dir(subscribers: Arc, network: Network) -> Router { +fn router(network: Network) -> Router { let router = Router::new() .route("/", get(index_handler)) - .route("/api/version", get(version)) - .route("/api/balance", get(get_balance)) - .route("/api/newaddress", get(get_unused_address)) - .route("/api/sendpayment", post(send_payment)) - .route("/api/history", get(get_onchain_payment_history)) - .route("/api/orders", post(post_new_order)) - .route("/api/positions", get(get_positions)) - .route("/api/quotes/:contract_symbol", get(get_best_quote)) - .route("/api/node", get(get_node_id)) - .route("/api/seed", get(get_seed_phrase)) .route("/main.dart.js", get(main_dart_handler)) .route("/flutter.js", get(flutter_js)) .route("/index.html", get(index_handler)) @@ -127,16 +164,12 @@ fn using_serve_dir(subscribers: Arc, network: Network) -> Router tracing::error!("something went wrong : {error:#}") }, ), - ) - .with_state(subscribers); - - if matches!( - network, - Network::Regtest | Network::Signet | Network::Testnet - ) { - router.layer(CorsLayer::permissive()) - } else { + ); + + if matches!(network, Network::Bitcoin) { router + } else { + router.layer(CorsLayer::very_permissive()) } } @@ -192,11 +225,3 @@ where } } } - -async fn serve(app: Router, port: u16) -> Result<()> { - let addr = SocketAddr::from(([0, 0, 0, 0], port)); - let listener = tokio::net::TcpListener::bind(addr).await?; - tracing::debug!("listening on {}", listener.local_addr()?); - axum::serve(listener, app).await?; - Ok(()) -} diff --git a/webapp/src/session.rs b/webapp/src/session.rs new file mode 100644 index 000000000..c22d6b394 --- /dev/null +++ b/webapp/src/session.rs @@ -0,0 +1,70 @@ +use axum::async_trait; +use axum_login::tower_sessions::session::Id; +use axum_login::tower_sessions::session::Record; +use axum_login::tower_sessions::session_store; +use axum_login::tower_sessions::ExpiredDeletion; +use axum_login::tower_sessions::SessionStore; +use parking_lot::RwLock; +use std::collections::HashMap; +use std::sync::Arc; +use time::OffsetDateTime; + +#[derive(Debug, Clone)] +pub struct InMemorySessionStore { + sessions: Arc>>, +} + +#[async_trait] +impl SessionStore for InMemorySessionStore { + async fn save(&self, session_record: &Record) -> session_store::Result<()> { + self.sessions + .write() + .insert(session_record.id, session_record.clone()); + Ok(()) + } + + async fn load(&self, session_id: &Id) -> session_store::Result> { + Ok(self.sessions.read().get(session_id).cloned()) + } + + async fn delete(&self, session_id: &Id) -> session_store::Result<()> { + self.sessions.write().remove(session_id); + Ok(()) + } +} + +#[async_trait] +impl ExpiredDeletion for InMemorySessionStore { + async fn delete_expired(&self) -> session_store::Result<()> { + let mut expired_session_ids = vec![]; + let sessions = self.sessions.read(); + for session in sessions.iter() { + if OffsetDateTime::now_utc() >= session.1.expiry_date { + expired_session_ids.push(session.0); + } + } + for expired_session_id in expired_session_ids.iter() { + self.sessions.write().remove(expired_session_id); + } + Ok(()) + } +} + +impl InMemorySessionStore { + pub(crate) fn new() -> Self { + Self { + sessions: Arc::new(RwLock::new(HashMap::new())), + } + } + + pub(crate) async fn continuously_delete_expired( + self, + period: tokio::time::Duration, + ) -> session_store::Result<()> { + let mut interval = tokio::time::interval(period); + loop { + self.delete_expired().await?; + interval.tick().await; + } + } +}