diff --git a/Cargo.lock b/Cargo.lock index 3905427b3..b12152143 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,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]] @@ -311,6 +311,26 @@ 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" @@ -488,7 +508,7 @@ dependencies = [ "anyhow", "hex", "reqwest", - "ring", + "ring 0.16.20", "serde", "serde_json", "serde_urlencoded", @@ -505,7 +525,7 @@ dependencies = [ "async-stream", "futures", "hex", - "ring", + "ring 0.16.20", "serde", "serde_json", "tokio", @@ -642,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" @@ -791,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" @@ -865,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", ] @@ -1060,7 +1094,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -1082,7 +1116,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core 0.20.3", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -1113,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" @@ -1392,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", ] @@ -1427,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", @@ -1458,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", @@ -1481,7 +1525,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -1548,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", @@ -2241,6 +2285,7 @@ checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg", "scopeguard", + "serde", ] [[package]] @@ -2884,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" @@ -2912,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" @@ -2962,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", ] @@ -3070,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", ] @@ -3233,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" @@ -3305,7 +3370,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.23", + "syn 2.0.48", "walkdir", ] @@ -3397,7 +3462,7 @@ 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", ] @@ -3424,8 +3489,8 @@ 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]] @@ -3434,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]] @@ -3513,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]] @@ -3597,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", @@ -3704,7 +3769,7 @@ dependencies = [ "darling 0.20.3", "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -3864,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", @@ -3951,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]] @@ -3990,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", @@ -4002,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", ] @@ -4032,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", @@ -4044,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", @@ -4083,7 +4150,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -4248,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" @@ -4285,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", @@ -4300,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", @@ -4465,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" @@ -4497,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" @@ -4509,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", @@ -4521,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]] @@ -4679,6 +4813,7 @@ dependencies = [ "anyhow", "atty", "axum 0.7.4", + "axum-login", "axum-server", "bitcoin", "clap", @@ -4692,6 +4827,7 @@ dependencies = [ "rust_decimal_macros", "serde", "serde_json", + "sha2", "time", "tokio", "tower", diff --git a/webapp/Cargo.toml b/webapp/Cargo.toml index 6ee0cee0a..5129b37fe 100644 --- a/webapp/Cargo.toml +++ b/webapp/Cargo.toml @@ -8,6 +8,7 @@ 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"] } @@ -21,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/frontend/lib/auth/auth_service.dart b/webapp/frontend/lib/auth/auth_service.dart new file mode 100644 index 000000000..3bbbe6b81 --- /dev/null +++ b/webapp/frontend/lib/auth/auth_service.dart @@ -0,0 +1,30 @@ +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 { + bool isLoggedIn = 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!"); + + isLoggedIn = true; + } + + Future signOut() async { + await HttpClientManager.instance.get(Uri(path: '/api/logout')); + isLoggedIn = false; + } +} 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..9fc4c0115 100644 --- a/webapp/frontend/lib/common/http_client.dart +++ b/webapp/frontend/lib/common/http_client.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:http/browser_client.dart'; import 'package:http/http.dart'; class HttpClientManager { @@ -7,7 +8,7 @@ class HttpClientManager { 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 +19,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); } @@ -28,7 +32,7 @@ class CustomHttpClient extends BaseClient { Future delete(Uri url, {Map? headers, Object? body, Encoding? encoding}) { if (_dev && url.host == '') { - url = Uri.parse('http://$_host:$_port${url.toString()}'); + url = Uri.parse('https://$_host:$_port${url.toString()}'); } return _inner.delete(url, headers: headers, body: body, encoding: encoding); } @@ -36,7 +40,7 @@ class CustomHttpClient extends BaseClient { @override Future put(Uri url, {Map? headers, Object? body, Encoding? encoding}) { if (_dev && url.host == '') { - url = Uri.parse('http://$_host:$_port${url.toString()}'); + url = Uri.parse('https://$_host:$_port${url.toString()}'); } return _inner.put(url, headers: headers, body: body, encoding: encoding); } @@ -44,7 +48,7 @@ class CustomHttpClient extends BaseClient { @override Future post(Uri url, {Map? headers, Object? body, Encoding? encoding}) { if (_dev && url.host == '') { - url = Uri.parse('http://$_host:$_port${url.toString()}'); + url = Uri.parse('https://$_host:$_port${url.toString()}'); } return _inner.post(url, headers: headers, body: body, encoding: encoding); } @@ -52,7 +56,7 @@ class CustomHttpClient extends BaseClient { @override Future get(Uri url, {Map? headers}) { if (_dev && url.host == '') { - url = Uri.parse('http://$_host:$_port${url.toString()}'); + url = Uri.parse('https://$_host:$_port${url.toString()}'); } return _inner.get(url, headers: headers); } diff --git a/webapp/frontend/lib/common/scaffold_with_nav.dart b/webapp/frontend/lib/common/scaffold_with_nav.dart index fbffbe6d7..3d012ec3b 100644 --- a/webapp/frontend/lib/common/scaffold_with_nav.dart +++ b/webapp/frontend/lib/common/scaffold_with_nav.dart @@ -1,6 +1,12 @@ +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/wallet/wallet_service.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; @@ -39,6 +45,11 @@ class _ScaffoldWithNestedNavigation extends State String version = "unknown"; Balance balance = Balance.zero(); + 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, @@ -60,10 +71,25 @@ class _ScaffoldWithNestedNavigation extends State context.read().getBalance().then((b) => setState(() => balance = b)); } + @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, @@ -170,28 +196,48 @@ class ScaffoldWithNavigationRail extends StatelessWidget { padding: const EdgeInsets.all(25), child: Row( children: [ - RichText( - text: TextSpan( - text: "Off-chain: ", - style: const TextStyle(fontSize: 16, color: Colors.black), - children: [ - TextSpan( - text: balance.offChain.formatted(), - style: const TextStyle(fontWeight: FontWeight.bold)), - const TextSpan(text: " sats"), - ]), + Row( + children: [ + RichText( + text: TextSpan( + text: "Off-chain: ", + style: const TextStyle(fontSize: 16, color: Colors.black), + children: [ + TextSpan( + text: balance.offChain.formatted(), + style: const TextStyle(fontWeight: FontWeight.bold)), + const TextSpan(text: " sats"), + ]), + ), + const SizedBox(width: 30), + RichText( + text: TextSpan( + text: "On-chain: ", + style: const TextStyle(fontSize: 16, color: Colors.black), + children: [ + TextSpan( + text: balance.onChain.formatted(), + style: const TextStyle(fontWeight: FontWeight.bold)), + const TextSpan(text: " sats"), + ]), + ), + ], ), - const SizedBox(width: 30), - RichText( - text: TextSpan( - text: "On-chain: ", - style: const TextStyle(fontSize: 16, color: Colors.black), - children: [ - 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 3a5e35e45..3c6785873 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'; @@ -15,7 +16,8 @@ void main() { var providers = [ Provider(create: (context) => const VersionService()), - Provider(create: (context) => const WalletService()) + Provider(create: (context) => const WalletService()), + Provider(create: (context) => AuthService()) ]; runApp(MultiProvider(providers: providers, child: const TenTenOneApp())); } diff --git a/webapp/frontend/lib/routes.dart b/webapp/frontend/lib/routes.dart index c729479f2..38e028a94 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) { + final isLoggedIn = 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/src/api.rs b/webapp/src/api.rs index fe64a0048..80278b3e8 100644 --- a/webapp/src/api.rs +++ b/webapp/src/api.rs @@ -5,7 +5,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 native::api::ContractSymbol; use native::api::Direction; use native::api::Fee; @@ -24,6 +27,18 @@ 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)) + .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 6761b038d..2d6b08e7d 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; @@ -37,6 +41,9 @@ pub struct Opts { /// Where to find the cert and key pem files #[clap(long)] cert_dir: Option, + + #[clap(long, default_value = "satoshi")] + password: String, } #[derive(Debug, Clone, Copy, clap::ValueEnum)] @@ -68,6 +75,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"), 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 148d0befa..12d97d40b 100644 --- a/webapp/src/main.rs +++ b/webapp/src/main.rs @@ -1,16 +1,13 @@ mod api; +mod auth; mod cli; mod logger; +mod session; mod subscribers; -use crate::api::get_balance; -use crate::api::get_onchain_payment_history; -use crate::api::get_positions; -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; @@ -22,8 +19,11 @@ 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; @@ -60,6 +60,7 @@ async fn main() -> Result<()> { 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; @@ -92,7 +93,30 @@ async fn main() -> Result<()> { let config = RustlsConfig::from_pem_file(cert_dir.join("cert.pem"), cert_dir.join("key.pem")).await?; - let app = router(Arc::new(rx), network); + 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(false) + .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)); @@ -101,19 +125,14 @@ async fn main() -> Result<()> { .serve(app.into_make_service()) .await?; + deletion_task.await??; + Ok(()) } -fn router(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("/main.dart.js", get(main_dart_handler)) .route("/flutter.js", get(flutter_js)) .route("/index.html", get(index_handler)) @@ -136,8 +155,7 @@ fn router(subscribers: Arc, network: Network) -> Router { tracing::error!("something went wrong : {error:#}") }, ), - ) - .with_state(subscribers); + ); if matches!( network, 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; + } + } +}