diff options
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | Cargo.lock | 2036 | ||||
-rw-r--r-- | Cargo.toml | 26 | ||||
-rw-r--r-- | src/ipc.rs | 83 | ||||
-rw-r--r-- | src/main.rs | 12 | ||||
-rw-r--r-- | src/server/admin.rs | 93 | ||||
-rw-r--r-- | src/server/config.rs | 29 | ||||
-rw-r--r-- | src/server/login.rs | 208 | ||||
-rw-r--r-- | src/server/mod.rs | 160 | ||||
-rw-r--r-- | src/server/nginx_check.rs | 41 | ||||
-rw-r--r-- | src/server/panel.rs | 30 | ||||
-rw-r--r-- | src/server/store.rs | 330 | ||||
-rw-r--r-- | src/tui/accounts.rs | 169 | ||||
-rw-r--r-- | src/tui/mod.rs | 159 |
14 files changed, 3380 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f35ea30 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +/*.json +/*.sock +/config.toml diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..021b24f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2036 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "895ff42f72016617773af68fb90da2a9677d89c62338ec09162d4909d86fdd8f" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bincode" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95" +dependencies = [ + "bincode_derive", + "serde", +] + +[[package]] +name = "bincode_derive" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30759b3b99a1b802a7a3aa21c85c3ded5c28e1c83170d82d70f08bbf7f3e4c" +dependencies = [ + "virtue", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-targets 0.52.4", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + +[[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 = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cursive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5438eb16bdd8af51b31e74764fef5d0a9260227a5ec82ba75c9d11ce46595839" +dependencies = [ + "ahash", + "cfg-if", + "crossbeam-channel", + "crossterm", + "cursive_core", + "lazy_static", + "libc", + "log", + "signal-hook", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "cursive_core" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4db3b58161228d0dcb45c7968c5e74c3f03ad39e8983e58ad7d57061aa2cd94d" +dependencies = [ + "ahash", + "crossbeam-channel", + "enum-map", + "enumset", + "lazy_static", + "log", + "num", + "owning_ref", + "time", + "unicode-segmentation", + "unicode-width", + "xi-unicode", +] + +[[package]] +name = "darling" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dissociate" +version = "0.1.0" +dependencies = [ + "argon2", + "axum", + "axum-extra", + "bincode", + "cursive", + "eyre", + "maud", + "nanoid", + "ouroboros", + "oxide-auth", + "oxide-auth-axum", + "paste", + "rand", + "serde", + "serde_json", + "tap", + "tokio", + "toml", +] + +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enumset" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "226c0da7462c13fb57e5cc9e0dc8f0635e7d27f276a3a7fd30054647f669007d" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08b6c6ab82d70f08844964ba10c7babb716de2ecaeab9be5717918a5177d3af" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "h2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "2.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "js-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "maud" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df518b75016b4289cdddffa1b01f2122f4a49802c93191f3133f6dc2472ebcaa" +dependencies = [ + "itoa", + "maud_macros", +] + +[[package]] +name = "maud_macros" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa453238ec218da0af6b11fc5978d3b5c3a45ed97b722391a2a11f3306274e18" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand", +] + +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "ouroboros" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b7be5a8a3462b752f4be3ff2b2bf2f7f1d00834902e46be2a4d68b87b0573c" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b645dcde5f119c2c454a92d0dfa271a2a3b205da92e4292a68ead4bdbfde1f33" +dependencies = [ + "heck", + "itertools", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + +[[package]] +name = "owning_ref" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "oxide-auth" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06729d9f5ee0fcade59be20aef48231be773808ea3d2eda2fcac394f678cfd95" +dependencies = [ + "base64 0.13.1", + "chrono", + "hmac", + "once_cell", + "rand", + "rmp-serde", + "rust-argon2", + "serde", + "serde_derive", + "serde_json", + "sha2", + "subtle", + "url", +] + +[[package]] +name = "oxide-auth-axum" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3592c190977a91610618349bd947fec5bf10b2ca1cdc882f6623031de4cd34e1" +dependencies = [ + "axum", + "oxide-auth", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rmp" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "rust-argon2" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5885493fdf0be6cdff808d1533ce878d21cfa49c7086fa00c66355cd9141bfc" +dependencies = [ + "base64 0.21.7", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + +[[package]] +name = "socket2" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "2.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "virtue" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "winnow" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +dependencies = [ + "memchr", +] + +[[package]] +name = "xi-unicode" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" + +[[package]] +name = "yansi" +version = "1.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7f5317e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "dissociate" +version = "0.1.0" +edition = "2021" + +[dependencies] +argon2 = "0.5.3" +axum = { version = "0.7.4", features = ["macros"] } +axum-extra = { version = "0.9.2", features = ["cookie"] } +bincode = "2.0.0-rc.3" +cursive = { version = "0.20.0", default-features = false, features = [ + "crossterm-backend", +] } +eyre = "0.6.12" +maud = "0.26.0" +nanoid = "0.4.0" +ouroboros = "0.18.3" +oxide-auth = "0.5.4" +oxide-auth-axum = "0.4.0" +paste = "1.0.14" +rand = "0.8.5" +serde = { version = "1.0.197", features = ["derive"] } +serde_json = "1.0.114" +tap = "1.0.1" +tokio = { version = "1.36.0", features = ["rt-multi-thread", "macros", "fs"] } +toml = "0.8.10" diff --git a/src/ipc.rs b/src/ipc.rs new file mode 100644 index 0000000..f3e7d0a --- /dev/null +++ b/src/ipc.rs @@ -0,0 +1,83 @@ +use std::fmt::{Display, Formatter}; + +use bincode::{Decode, Encode}; +use paste::paste; + +pub trait RequestDefinition: Encode { + type Response: ResponseDefinition; + fn into_request(self) -> IPCRequest; +} + +pub trait ResponseDefinition: Sized + Decode { + type Error: std::error::Error; + fn from_response(response: IPCResponse) -> Option<Result<Self, Self::Error>>; + fn into_response(self) -> IPCResponse; +} + +macro_rules! define { + ($(@$name:ident { $($req_n:ident: $req_t:ty),* } => { $($resp_n:ident: $resp_t:ty)* } | $($error:ident),*)*) => { + $( + paste! { + #[derive(Encode, Decode)] + pub struct [<$name Request>] { $(pub $req_n: $req_t),* } + impl RequestDefinition for [<$name Request>] { + type Response = [<$name Response>]; + fn into_request(self) -> IPCRequest { + IPCRequest::$name(self) + } + } + + #[derive(Encode, Decode)] + pub struct [<$name Response>] { $(pub $resp_n: $resp_t),* } + impl ResponseDefinition for [<$name Response>] { + type Error = [<$name Error>]; + fn from_response(response: IPCResponse) -> Option<Result<Self, Self::Error>> { + match response { + IPCResponse::$name(inner) => Some(inner), + _ => None, + } + } + fn into_response(self) -> IPCResponse { + IPCResponse::$name(Ok(self)) + } + } + + #[derive(Encode, Decode, Debug)] + pub enum [<$name Error>] { + $($error,)* + } + + impl Display for [<$name Error>] { + #[allow(unused)] + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match *self { + $(Self::$error => f.write_str(stringify!($error)),)* + } + } + } + + impl std::error::Error for [<$name Error>] {} + } + )* + #[derive(Encode, Decode)] + pub enum IPCRequest { + $( + $name(paste!([<$name Request>])), + )* + } + #[derive(Encode, Decode)] + pub enum IPCResponse { + $( + $name(Result<paste!([<$name Response>]), paste!([<$name Error>])>), + )* + } + }; +} + +define! { + @ListAccounts {} => { names: Vec<String> } | + @GetAccount { name: String } => { scopes: Vec<String> } | NotFound + @DeleteAccount { name: String } => {} | + @CreateInvite {} => { link: String } | + @UpdateScopes { account: String, scopes: Vec<String> } => {} | NotFound +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..936f37c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,12 @@ +mod ipc; +mod server; +mod tui; + +#[tokio::main] +async fn main() -> eyre::Result<()> { + match std::env::args().skip(1).next().as_deref() { + Some("serve") | None => server::serve().await, + Some("ctl") => tui::run(), + _ => Err(eyre::eyre!("unknown command")), + } +} diff --git a/src/server/admin.rs b/src/server/admin.rs new file mode 100644 index 0000000..54bdf12 --- /dev/null +++ b/src/server/admin.rs @@ -0,0 +1,93 @@ +use std::{future::Future, path::Path}; + +use tap::Pipe; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::{UnixListener, UnixStream}, +}; + +use crate::ipc::*; + +use super::store::Store; + +pub fn serve( + bind: &Path, + web_base: String, + store: Store, +) -> eyre::Result<impl Future<Output = eyre::Result<()>>> { + if bind.exists() { + std::fs::remove_file(bind)?; + } + let listener = UnixListener::bind(bind)?; + + Ok(async move { + loop { + match listener.accept().await { + Ok((stream, _)) => tokio::spawn({ + let store = store.clone(); + let web_base = web_base.clone(); + async move { + if let Err(err) = handle(stream, store, &web_base).await { + eprintln!("error handling admin connection: {err:?}"); + } + } + }), + Err(err) => { + eprintln!("error accepting admin connection: {err:?}"); + continue; + } + }; + } + }) +} + +async fn handle(mut stream: UnixStream, store: Store, web_base: &str) -> eyre::Result<()> { + while let Ok(request_length) = stream.read_u16().await { + let mut request_buffer = vec![0u8; request_length as usize]; + stream.read_exact(&mut request_buffer).await?; + let (request, _): (IPCRequest, _) = + bincode::decode_from_slice(&request_buffer, bincode::config::standard())?; + let response = match request { + IPCRequest::ListAccounts(_) => ListAccountsResponse { + names: store.list_accounts().await, + } + .into_response(), + IPCRequest::GetAccount(GetAccountRequest { name }) => { + IPCResponse::GetAccount(match store.get_account(&name).await { + Some(account) => Ok(GetAccountResponse { + scopes: account.scopes.clone(), + }), + None => Err(GetAccountError::NotFound), + }) + } + IPCRequest::DeleteAccount(DeleteAccountRequest { name }) => { + store.delete_account(&name).await?; + DeleteAccountResponse {}.into_response() + } + IPCRequest::CreateInvite(_) => store + .create_invite() + .await? + .pipe(|invite| CreateInviteResponse { + link: format!("{web_base}/invite/{invite}"), + }) + .into_response(), + IPCRequest::UpdateScopes(UpdateScopesRequest { account, scopes }) => { + IPCResponse::UpdateScopes( + match store + .update_account(&account, |account| { + account.scopes = scopes; + }) + .await? + { + true => Ok(UpdateScopesResponse {}), + false => Err(UpdateScopesError::NotFound), + }, + ) + } + }; + let response_buffer = bincode::encode_to_vec(response, bincode::config::standard())?; + stream.write_u16(response_buffer.len() as u16).await?; + stream.write_all(&response_buffer).await?; + } + Ok(()) +} diff --git a/src/server/config.rs b/src/server/config.rs new file mode 100644 index 0000000..4563f34 --- /dev/null +++ b/src/server/config.rs @@ -0,0 +1,29 @@ +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; + +use eyre::Context; +use tap::Pipe; + +#[derive(serde::Deserialize)] +pub struct Config { + pub web_socket: SocketAddr, + pub web_base: String, + pub cookie_domain: Option<String>, + pub admin_socket: PathBuf, + pub data: PathBuf, +} + +impl Config { + pub fn load(path: &Path) -> eyre::Result<Self> { + let mut config: Config = path + .pipe(std::fs::read_to_string) + .wrap_err("reading config file")? + .as_str() + .pipe(toml::from_str) + .wrap_err("parsing config file")?; + if config.web_base.ends_with('/') { + config.web_base = config.web_base.trim_end_matches('/').to_string(); + } + Ok(config) + } +} diff --git a/src/server/login.rs b/src/server/login.rs new file mode 100644 index 0000000..f9cee70 --- /dev/null +++ b/src/server/login.rs @@ -0,0 +1,208 @@ +use std::time::Duration; + +use argon2::{Argon2, PasswordVerifier}; +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Redirect, Response}, + routing::get, + Form, Router, +}; +use axum_extra::extract::{ + cookie::{Cookie, SameSite}, + CookieJar, +}; +use maud::{html, PreEscaped}; +use tap::Tap; + +use crate::server::store::Store; + +use super::{render_html, ApiState, CookieDomain, MakeError, MakeErrorMessage}; + +pub fn bind(app: Router<ApiState>) -> Router<ApiState> { + app.route("/login", get(handle_login_get).post(handle_login_post)) + .route("/logout", get(logout)) + .route( + "/invite/:invite", + get(handle_invite_get).post(handle_invite_post), + ) +} + +fn make_form(status: StatusCode, error: Option<&str>, submit: &str) -> Response { + render_html( + html!(title {"login"}), + html! { + @if let Some(error) = error { + p { "error: " (PreEscaped(error)) } + } + form action="" method="post" { + label for="name" { "name:" } + input type="text" id="name" name="name" {} + label for="password" { "password:" } + input type="password" id="password" name="password" {} + input type="submit" value=(submit) {} + } + }, + ) + .tap_mut(|response| *response.status_mut() = status) +} + +#[axum::debug_handler] +async fn handle_login_get() -> Response { + make_form(StatusCode::OK, None, "login") +} + +#[derive(serde::Deserialize)] +struct FormData { + name: String, + password: String, +} + +#[axum::debug_handler(state = ApiState)] +async fn handle_login_post( + jar: CookieJar, + State(store): State<Store>, + State(CookieDomain(cookie_domain)): State<CookieDomain>, + Form(form): Form<FormData>, +) -> Result<Response, Response> { + // read lock scope + { + let account = store.get_account(&form.name).await.ok_or_else(|| { + make_form(StatusCode::FORBIDDEN, Some("invalid credentials"), "login") + })?; + Argon2::default() + .verify_password(form.password.as_bytes(), &account.password.parsed()) + .ok() + .ok_or_else(|| { + make_form(StatusCode::FORBIDDEN, Some("invalid credentials"), "login") + })?; + } + let token = store + .create_token(&form.name) + .await + .make_error()? + .error_message( + StatusCode::INTERNAL_SERVER_ERROR, + "account disappeared while generating token", + )?; + + let mut cookie = Cookie::new("dissociate-token", token); + cookie.set_http_only(true); + cookie.set_max_age(Duration::from_secs(60 * 60 * 24 * 29).try_into().ok()); + cookie.set_path("/"); + cookie.set_same_site(SameSite::Strict); + cookie.set_secure(true); + if let Some(cookie_domain) = cookie_domain { + cookie.set_domain(cookie_domain); + } + + Ok((jar.add(cookie), Redirect::to("/")).into_response()) +} + +#[axum::debug_handler] +async fn logout(jar: CookieJar) -> Response { + ( + jar.remove(Cookie::from("dissociate-token")), + Redirect::to("/login"), + ) + .into_response() +} + +fn invite_error() -> Response { + render_html( + html!(), + html! { + h1 { "unknown invite" } + a href="/" { "return home" } + }, + ) + .tap_mut(|response| *response.status_mut() = StatusCode::NOT_FOUND) +} + +#[axum::debug_handler] +async fn handle_invite_get(Path(invite): Path<String>, State(store): State<Store>) -> Response { + if !store.check_invite(&invite).await { + return invite_error(); + } + make_form(StatusCode::OK, None, "create account") +} + +const NAME_ERROR: &str = "invalid name +requirements: +must not be empty +must not exceed 32 characters +must not contain control characters"; +#[axum::debug_handler(state = ApiState)] +async fn handle_invite_post( + jar: CookieJar, + Path(invite): Path<String>, + State(store): State<Store>, + State(CookieDomain(cookie_domain)): State<CookieDomain>, + Form(form): Form<FormData>, +) -> Result<Response, Response> { + if form.name.to_ascii_lowercase() == "empty" || form.password.to_ascii_lowercase() == "empty" { + return Err(make_form( + StatusCode::BAD_REQUEST, + Some("are you jokester?"), + "create account", + )); + } + if form.name.is_empty() || form.name.len() > 32 || form.name.chars().any(|ch| ch.is_control()) { + return Err(make_form( + StatusCode::BAD_REQUEST, + Some(&NAME_ERROR.replace('\n', "<br>")), + "create account", + )); + } + if form.password.is_empty() { + return Err(make_form( + StatusCode::BAD_REQUEST, + Some("password cannot be empty"), + "create account", + )); + } + + if store.get_account(&form.name).await.is_some() { + return Err(make_form( + StatusCode::CONFLICT, + Some("name taken"), + "create account", + )); + } + + if !store.use_invite(&invite).await.make_error()? { + return Err(invite_error()); + } + + if !store + .create_account(&form.name, &form.password) + .await + .make_error()? + { + return Err(make_form( + StatusCode::CONFLICT, + Some("name taken"), + "create account", + )); + } + let token = store + .create_token(&form.name) + .await + .make_error()? + .error_message( + StatusCode::INTERNAL_SERVER_ERROR, + "account disappeared while generating token", + )?; + + let mut cookie = Cookie::new("dissociate-token", token); + cookie.set_http_only(true); + cookie.set_max_age(Duration::from_secs(60 * 60 * 24 * 29).try_into().ok()); + cookie.set_path("/"); + cookie.set_same_site(SameSite::Strict); + cookie.set_secure(true); + if let Some(cookie_domain) = cookie_domain { + cookie.set_domain(cookie_domain); + } + + Ok((jar.add(cookie), Redirect::to("/")).into_response()) +} diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..d9b3beb --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,160 @@ +mod admin; +mod config; +mod login; +mod nginx_check; +mod panel; +mod store; + +use std::{future::IntoFuture, path::PathBuf}; + +use axum::{ + body::Body, + extract::FromRef, + http::{header::CONTENT_TYPE, StatusCode}, + response::{IntoResponse, Redirect, Response}, + routing::get, + Router, +}; +use axum_extra::extract::CookieJar; +use eyre::Context; +use maud::{html, PreEscaped}; +use tap::Pipe; +use tokio::{net::TcpListener, select}; + +use crate::server::store::Store; + +use self::config::Config; + +pub async fn serve() -> eyre::Result<()> { + let config_path = std::env::args() + .skip(2) + .next() + .map(PathBuf::from) + .unwrap_or_else(|| std::env::current_dir().unwrap().join("dissociate.toml")); + let config = Config::load(&config_path)?; + + let store = Store::load(config.data).await?; + println!("admin listening on {:?}", config.admin_socket); + let admin_serve = admin::serve(&config.admin_socket, config.web_base.clone(), store.clone())?; + + let listener = TcpListener::bind(config.web_socket).await?; + + let app = Router::new() + .pipe(login::bind) + .pipe(nginx_check::bind) + .pipe(panel::bind) + .with_state(ApiState { + store, + cookie_domain: CookieDomain(config.cookie_domain), + web_base: WebBase(config.web_base), + }) + .fallback(get(|| async { + render_html( + html!(title { "not found" }), + html! { + h1 { "404 not found" } + p {"sowwy :("} + }, + ) + })); + + println!("web listening on {:?}", config.web_socket); + let web_serve = axum::serve(listener, app).into_future(); + + select! { + res = admin_serve => res.wrap_err("in admin"), + res = web_serve => res.wrap_err("in web"), + } +} + +#[derive(Clone, FromRef)] +struct ApiState { + pub store: Store, + pub cookie_domain: CookieDomain, + pub web_base: WebBase, +} + +#[derive(Clone)] +struct CookieDomain(Option<String>); + +#[derive(Clone)] +struct WebBase(String); + +fn render_html(head: PreEscaped<impl AsRef<str>>, body: PreEscaped<impl AsRef<str>>) -> Response { + let html = html! { + (PreEscaped("<!doctype html>")) + html { + head {(head)} + body {(body)} + } + } + .into_string(); + Response::builder() + .header(CONTENT_TYPE, "text/html; charset=utf-8") + .body(Body::new(html)) + .unwrap() +} + +trait MakeErrorMessage<T> { + fn error_message(self, status: StatusCode, message: impl ToString) -> Result<T, Response>; +} + +impl<T, E: ToString> MakeErrorMessage<T> for Result<T, E> { + fn error_message(self, status: StatusCode, message: impl ToString) -> Result<T, Response> { + self.map_err(|err| { + render_html( + html!(title { "error" }), + html! { + h1 { (status.canonical_reason().unwrap_or_else(|| status.as_str())) } + pre { (err.to_string()) ": " (message.to_string()) } + }, + ) + }) + } +} + +impl<T> MakeErrorMessage<T> for Option<T> { + fn error_message(self, status: StatusCode, message: impl ToString) -> Result<T, Response> { + self.ok_or_else(|| { + render_html( + html!(title { "error" }), + html! { + h1 { (status.canonical_reason().unwrap_or_else(|| status.as_str())) } + pre { (message.to_string()) } + }, + ) + }) + } +} + +trait MakeError<T> { + fn make_error(self) -> Result<T, Response>; +} + +impl<T> MakeError<T> for std::io::Result<T> { + fn make_error(self) -> Result<T, Response> { + self.error_message(StatusCode::INTERNAL_SERVER_ERROR, "internal io error") + } +} + +trait Nevermind<T> { + fn prompt_login(self) -> Result<T, Response>; + fn prompt_logout(self) -> Result<T, Response>; +} + +impl<T> Nevermind<T> for Option<T> { + fn prompt_login(self) -> Result<T, Response> { + self.ok_or_else(|| Redirect::to("/login").into_response()) + } + + fn prompt_logout(self) -> Result<T, Response> { + self.ok_or_else(|| Redirect::to("/login").into_response()) + } +} + +async fn account_auth(jar: &CookieJar, store: &Store) -> Option<String> { + let cookie = jar.get("dissociate-token")?; + let token = cookie.value(); + let name = store.check_token(token).await?; + Some(name) +} diff --git a/src/server/nginx_check.rs b/src/server/nginx_check.rs new file mode 100644 index 0000000..7b67f26 --- /dev/null +++ b/src/server/nginx_check.rs @@ -0,0 +1,41 @@ +// for ngx_http_auth_request_module authentication +// make sure you have cookie_domain set properly +// depends on https://git.mia.jetzt/sysconf/tree/patches/nginx_auth_redirect.patch + +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Redirect, Response}, + routing::get, + Router, +}; +use axum_extra::extract::CookieJar; + +use crate::server::{account_auth, store::Store}; + +use super::{ApiState, WebBase}; + +pub fn bind(app: Router<ApiState>) -> Router<ApiState> { + app.route("/nginx_check/:scope", get(nginx_check)) +} + +#[axum::debug_handler(state = ApiState)] +async fn nginx_check( + jar: CookieJar, + Path(scope): Path<String>, + State(store): State<Store>, + State(WebBase(web_base)): State<WebBase>, +) -> Response { + let nevermind = || Redirect::to(&format!("{web_base}/logout")).into_response(); + let Some(name) = account_auth(&jar, &store).await else { + return nevermind(); + }; + let Some(account) = store.get_account(&name).await else { + return nevermind(); + }; + if account.scopes.contains(&scope) { + StatusCode::OK.into_response() + } else { + StatusCode::FORBIDDEN.into_response() + } +} diff --git a/src/server/panel.rs b/src/server/panel.rs new file mode 100644 index 0000000..addb0d8 --- /dev/null +++ b/src/server/panel.rs @@ -0,0 +1,30 @@ +use axum::{extract::State, response::Response, routing::get, Router}; +use axum_extra::extract::CookieJar; +use maud::html; +use tap::Pipe; + +use crate::server::{store::Store, Nevermind}; + +use super::{account_auth, render_html, ApiState}; + +pub fn bind(app: Router<ApiState>) -> Router<ApiState> { + app.route("/", get(get_panel)) +} + +#[axum::debug_handler(state = ApiState)] +async fn get_panel(jar: CookieJar, State(store): State<Store>) -> Result<Response, Response> { + Ok(account_auth(&jar, &store) + .await + .prompt_login()? + .pipe(render_normal_panel)) +} + +fn render_normal_panel(name: String) -> Response { + render_html( + html!(title { "dissociate" }), + html! { + p { "currently logged in as " (name) } + a href="/logout" { button { "log out" } } + }, + ) +} diff --git a/src/server/store.rs b/src/server/store.rs new file mode 100644 index 0000000..98c1bcc --- /dev/null +++ b/src/server/store.rs @@ -0,0 +1,330 @@ +use std::{ + collections::HashMap, + path::PathBuf, + sync::Arc, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use argon2::{ + password_hash::{rand_core::OsRng, Encoding, PasswordHashString, SaltString}, + Argon2, PasswordHash, PasswordHasher, +}; +use nanoid::nanoid; +use serde::{de::Unexpected, Deserialize, Serialize}; +use serde_json::{Map, Value}; +use tap::{Pipe, Tap}; +use tokio::sync::{RwLock, RwLockReadGuard}; + +#[derive(Clone)] +pub struct Store(Arc<RwLock<StoreInner>>); + +impl Store { + pub async fn load(path: PathBuf) -> eyre::Result<Self> { + let mut inner = StoreInner::new(path.clone()); + if path.try_exists()? { + let mut map: Map<String, Value> = + { std::fs::File::open(&path)?.pipe(serde_json::from_reader)? }; + let accounts: Vec<Account> = + serde_json::from_value(map.remove("accounts").unwrap_or_default())?; + let apps: Vec<App> = serde_json::from_value(map.remove("apps").unwrap_or_default())?; + for mut account in accounts { + account.tokens = account + .tokens + .into_iter() + .filter(|token| token.expires > SystemTime::now()) + .collect(); + for token in &account.tokens { + inner.token_map.insert( + token.value.clone(), + (account.name.clone(), token.expires.clone()), + ); + } + inner.accounts.insert(account.name.clone(), account); + } + for app in apps { + inner.apps.insert(app.name.clone(), app); + } + inner.invites = serde_json::from_value(map.remove("invites").unwrap_or_default())?; + inner.invites = inner + .invites + .into_iter() + .filter(|invite| invite.expires > SystemTime::now()) + .collect(); + } else { + Self::save(&mut inner).await?; + } + Ok(Store(Arc::new(RwLock::new(inner)))) + } + + async fn save(inner: &mut StoreInner) -> std::io::Result<()> { + let mut map: Map<String, Value> = Map::new(); + map.insert( + "accounts".into(), + serde_json::to_value(inner.accounts.values().collect::<Vec<_>>()).unwrap(), + ); + map.insert( + "apps".into(), + serde_json::to_value(inner.apps.values().collect::<Vec<_>>()).unwrap(), + ); + map.insert( + "invites".into(), + serde_json::to_value(&inner.invites).unwrap(), + ); + let data = serde_json::to_vec_pretty(&map).unwrap(); + let mut temp = inner.path.clone(); + temp.set_file_name( + temp.file_name() + .unwrap_or_default() + .to_os_string() + .tap_mut(|name| name.push(".tmp")), + ); + tokio::fs::write(&temp, data).await?; + tokio::fs::rename(temp, &inner.path).await?; + Ok(()) + } + + pub async fn get_account(&self, name: &str) -> Option<RwLockReadGuard<'_, Account>> { + let guard = self.0.read().await; + RwLockReadGuard::try_map(guard, |guard| guard.accounts.get(name)).ok() + } + + pub async fn create_account(&self, name: &str, password: &str) -> std::io::Result<bool> { + let hash = Argon2::default() + .hash_password(password.as_bytes(), &SaltString::generate(&mut OsRng)) + .unwrap() + .pipe(|hash| hash.serialize()) + .pipe(OwnedPasswordHash::from); + let mut guard = self.0.write().await; + if guard.accounts.get(name).is_some() { + return Ok(false); + } + guard.accounts.insert( + name.to_string(), + Account { + name: name.to_string(), + password: hash, + tokens: Default::default(), + scopes: Default::default(), + }, + ); + Self::save(&mut guard).await?; + Ok(true) + } + + pub async fn delete_account(&self, name: &str) -> std::io::Result<()> { + let mut guard = self.0.write().await; + if let Some(account) = guard.accounts.remove(name) { + for token in account.tokens { + guard.token_map.remove(&token.value); + } + Self::save(&mut guard).await + } else { + Ok(()) + } + } + + pub async fn update_account( + &self, + name: &str, + with: impl FnOnce(&mut Account), + ) -> std::io::Result<bool> { + let mut guard = self.0.write().await; + if let Some(account) = guard.accounts.get_mut(name) { + with(account); + Self::save(&mut guard).await.map(|_| true) + } else { + Ok(false) + } + } + + pub async fn list_accounts(&self) -> Vec<String> { + self.0 + .read() + .await + .accounts + .values() + .map(|account| account.name.clone()) + .collect() + } + + pub async fn create_token(&self, name: &str) -> std::io::Result<Option<String>> { + let mut guard = self.0.write().await; + let token = nanoid!(32); + if let Some(account) = guard.accounts.get_mut(name) { + let expires = SystemTime::now() + Duration::from_secs(60 * 60 * 24 * 30); + account.tokens.push(ExpiringValue { + value: token.clone(), + expires: expires.clone(), + }); + guard + .token_map + .insert(token.clone(), (name.to_string(), expires)); + Self::save(&mut guard).await?; + Ok(Some(token)) + } else { + Ok(None) + } + } + + pub async fn check_token(&self, token: &str) -> Option<String> { + let guard = self.0.read().await; + let Some((name, expires)) = guard.token_map.get(token) else { + return None; + }; + if *expires < SystemTime::now() { + return None; + } + Some(name.clone()) + } + + pub async fn create_invite(&self) -> std::io::Result<String> { + let mut guard = self.0.write().await; + let invite = nanoid!(32); + let expires = SystemTime::now() + Duration::from_secs(60 * 60 * 24 * 14); + guard.invites.push(ExpiringValue { + value: invite.clone(), + expires, + }); + Self::save(&mut guard).await?; + Ok(invite) + } + + pub async fn check_invite(&self, invite: &str) -> bool { + let guard = self.0.read().await; + let now = SystemTime::now(); + guard + .invites + .iter() + .any(|check| check.expires > now && check.value == invite) + } + + pub async fn use_invite(&self, invite: &str) -> std::io::Result<bool> { + let mut guard = self.0.write().await; + let now = SystemTime::now(); + if let Some((index, _)) = guard + .invites + .iter() + .enumerate() + .find(|(_, check)| check.expires > now && check.value == invite) + { + guard.invites.swap_remove(index); + Self::save(&mut guard).await?; + Ok(true) + } else { + Ok(false) + } + } +} + +pub struct StoreInner { + path: PathBuf, + accounts: HashMap<String, Account>, + apps: HashMap<String, App>, + invites: Vec<ExpiringValue>, + token_map: HashMap<String, (String, SystemTime)>, +} + +impl StoreInner { + fn new(at: PathBuf) -> Self { + Self { + path: at, + accounts: Default::default(), + apps: Default::default(), + invites: Default::default(), + token_map: Default::default(), + } + } +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct Account { + pub name: String, + pub password: OwnedPasswordHash, + pub tokens: Vec<ExpiringValue>, + pub scopes: Vec<String>, +} + +#[ouroboros::self_referencing] +pub struct OwnedPasswordHash { + owned: PasswordHashString, + #[borrows(owned)] + #[not_covariant] + parsed: PasswordHash<'this>, +} + +impl OwnedPasswordHash { + pub fn from(inner: PasswordHashString) -> Self { + OwnedPasswordHash::new(inner, |inner| inner.password_hash()) + } + + pub fn parsed(&self) -> PasswordHash { + self.with_parsed(|x| x.clone()) + } +} + +impl Serialize for OwnedPasswordHash { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + self.with_owned(|owned| owned.as_str().serialize(serializer)) + } +} + +impl<'de> Deserialize<'de> for OwnedPasswordHash { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + Ok(String::deserialize(deserializer)? + .pipe(|hash| { + PasswordHashString::parse(&hash, Encoding::B64).map_err(|_| { + <D::Error as serde::de::Error>::invalid_value( + Unexpected::Str(&hash), + &"valid password hash", + ) + }) + })? + .pipe(OwnedPasswordHash::from)) + } +} + +pub struct ExpiringValue { + pub value: String, + pub expires: SystemTime, +} + +impl Serialize for ExpiringValue { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + <(&str, u64)>::serialize( + &( + &self.value, + (self.expires.duration_since(SystemTime::UNIX_EPOCH)) + .unwrap() + .as_secs(), + ), + serializer, + ) + } +} + +impl<'de> Deserialize<'de> for ExpiringValue { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + <(String, u64)>::deserialize(deserializer).map(|(token, unix)| ExpiringValue { + value: token, + expires: UNIX_EPOCH + Duration::from_secs(unix), + }) + } +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct App { + name: String, + secret: String, +} diff --git a/src/tui/accounts.rs b/src/tui/accounts.rs new file mode 100644 index 0000000..a43028c --- /dev/null +++ b/src/tui/accounts.rs @@ -0,0 +1,169 @@ +use std::{cell::RefCell, rc::Rc}; + +use cursive::{ + view::Nameable, + views::{Button, Dialog, LinearLayout, SelectView, TextArea}, + Cursive, +}; + +use crate::ipc::{ + DeleteAccountRequest, GetAccountRequest, ListAccountsRequest, UpdateScopesRequest, +}; + +use super::{CursiveIpc, ToTextView, ViewExt}; + +pub fn show(siv: &mut Cursive) { + if let Some(accounts) = siv.ipc(ListAccountsRequest {}) { + siv.add_layer( + SelectView::new() + .autojump() + .with_all_str(accounts.names) + .on_submit(|siv, name: &str| { + manage_account(siv, name); + }) + .float("accounts"), + ); + } +} + +fn manage_account(siv: &mut Cursive, name: &str) { + let name = name.to_string(); + if let Some(_) = siv.ipc(GetAccountRequest { name: name.clone() }) { + siv.add_layer( + LinearLayout::vertical() + .child({ + let name = name.clone(); + enum SelectOption { + Back, + Scopes, + Delete, + } + SelectView::new() + .item("back", SelectOption::Back) + .item("scopes", SelectOption::Scopes) + .item("delete", SelectOption::Delete) + .on_submit(move |siv, opt| match opt { + SelectOption::Back => { + siv.pop_layer(); + } + SelectOption::Scopes => edit_scopes(siv, &name), + SelectOption::Delete => delete_account(siv, &name), + }) + }) + .float(&name), + ) + } +} + +fn edit_scopes(siv: &mut Cursive, name: &str) { + let name = name.to_string(); + let Some(account) = siv.ipc(GetAccountRequest { name: name.clone() }) else { + siv.pop_layer(); + return; // it was just there! + }; + let scopes = Rc::new(RefCell::new(account.scopes)); + enum SelectOption { + Scope(String), + Back, + Add, + } + siv.add_layer( + SelectView::new() + .autojump() + .item("back", SelectOption::Back) + .item("add", SelectOption::Add) + .with_all( + scopes + .clone() + .borrow() + .iter() + .map(|scope| (scope.to_string(), SelectOption::Scope(scope.clone()))), + ) + .on_submit(move |siv, opt| { + match opt { + SelectOption::Scope(scope) => { + let mut scopes = scopes.borrow_mut(); + let index = scopes + .iter() + .enumerate() + .find(|(_, check)| *check == scope) + .unwrap() + .0; + scopes.swap_remove(index); + ipc_update_scopes(siv, &name, &scopes); + siv.pop_layer(); + edit_scopes(siv, &name); + } + SelectOption::Back => { + siv.pop_layer(); + } + SelectOption::Add => add_scope(siv, name.clone(), scopes.clone()), + }; + }) + .float("edit scopes"), + ); +} + +fn add_scope(siv: &mut Cursive, name: String, scopes: Rc<RefCell<Vec<String>>>) { + siv.add_layer( + LinearLayout::vertical() + .child(TextArea::new().with_name("scope")) + .child(Button::new("add", { + move |siv| { + let text_area = siv.find_name::<TextArea>("scope").unwrap(); + let scope = text_area.get_content(); + if scope.is_empty() { + siv.pop_layer(); + return; + } + let mut scopes = scopes.borrow_mut(); + scopes.push(scope.to_string()); + ipc_update_scopes(siv, &name, &scopes); + siv.pop_layer(); + siv.pop_layer(); + edit_scopes(siv, &name); + } + })) + .float("add scope"), + ) +} + +fn ipc_update_scopes(siv: &mut Cursive, name: &str, scopes: &Vec<String>) { + siv.ipc(UpdateScopesRequest { + account: name.to_string(), + scopes: scopes.clone(), + }); +} + +fn delete_account(siv: &mut Cursive, name: &str) { + let name = name.to_string(); + siv.add_layer( + LinearLayout::vertical() + .child(format!("delete {name}?").text_view()) + .child(TextArea::new().with_name("confirm")) + .child(Button::new("submit", move |siv| { + if let Some(confirm) = siv.find_name::<TextArea>("confirm") { + if confirm.get_content() == "yes" { + if siv + .ipc(DeleteAccountRequest { name: name.clone() }) + .is_some() + { + siv.add_layer(Dialog::around("success".text_view()).button( + "ok", + |siv| { + // all the way back to the root + siv.pop_layer(); + siv.pop_layer(); + siv.pop_layer(); + siv.pop_layer(); + }, + )) + } + } else { + siv.pop_layer(); + } + } + })) + .float("are you sure"), + ); +} diff --git a/src/tui/mod.rs b/src/tui/mod.rs new file mode 100644 index 0000000..7c748c1 --- /dev/null +++ b/src/tui/mod.rs @@ -0,0 +1,159 @@ +mod accounts; + +use std::{ + io::{Read, Write}, + os::unix::net::UnixStream, +}; + +use cursive::{ + event::Key, + theme::{BorderStyle, Palette, Theme}, + utils::markup::StyledString, + view::Margins, + views::{Dialog, PaddedView, Panel, ScrollView, SelectView, TextView}, + Cursive, CursiveExt, +}; +use eyre::ContextCompat; + +use crate::ipc::{CreateInviteRequest, IPCResponse, RequestDefinition, ResponseDefinition}; + +pub fn run() -> eyre::Result<()> { + let Some(socket_path) = std::env::args().skip(2).next() else { + eyre::bail!("must specify socket path"); + }; + let stream = UnixStream::connect(socket_path)?; + let ipc = Ipc(stream); + + let mut siv = Cursive::new(); + siv.set_user_data(ipc); + + siv.set_theme(Theme { + shadow: false, + borders: BorderStyle::Simple, + palette: Palette::terminal_default(), + }); + + enum SelectItem { + Accounts, + Invite, + } + siv.add_layer( + SelectView::new() + .item("accounts", SelectItem::Accounts) + .item("invite", SelectItem::Invite) + .on_submit(move |siv, item| match item { + SelectItem::Accounts => accounts::show(siv), + SelectItem::Invite => invite(siv), + }) + .float("dissociate"), + ); + + siv.add_global_callback(Key::Esc, |siv| { + if siv.pop_layer().is_none() || siv.screen().is_empty() { + siv.quit(); + } + }); + + siv.run(); + + Ok(()) +} + +fn invite(siv: &mut Cursive) { + if let Some(invite) = siv.ipc(CreateInviteRequest {}) { + siv.add_layer( + Dialog::around(invite.link.text_view().hpad()) + .button("ok", |siv| { + siv.pop_layer(); + }) + .title("invite") + .pad(), + ); + } +} + +struct Ipc(UnixStream); + +impl Ipc { + fn exec<T: RequestDefinition>( + &mut self, + request: T, + ) -> eyre::Result<Result<T::Response, <T::Response as ResponseDefinition>::Error>> { + let request_buffer = + bincode::encode_to_vec(request.into_request(), bincode::config::standard())?; + self.0 + .write_all(&(request_buffer.len() as u16).to_be_bytes())?; + self.0.write_all(&request_buffer)?; + let mut response_len_buffer = [0u8; 2]; + self.0.read_exact(&mut response_len_buffer)?; + let response_len = u16::from_be_bytes(response_len_buffer) as usize; + let mut response_buffer = vec![0u8; response_len]; + self.0.read_exact(&mut response_buffer)?; + let (response, _): (IPCResponse, _) = + bincode::decode_from_slice(&response_buffer, bincode::config::standard())?; + T::Response::from_response(response).wrap_err("got wrong response type") + } +} + +trait CursiveIpc { + fn ipc<T: RequestDefinition>(&mut self, request: T) -> Option<T::Response>; +} + +impl CursiveIpc for Cursive { + fn ipc<T: RequestDefinition>(&mut self, request: T) -> Option<T::Response> { + match self.user_data::<Ipc>().unwrap().exec(request) { + Ok(Ok(resp)) => Some(resp), + Err(err) => { + self.add_layer( + Dialog::new() + .content(format!("ipc error: {err:?}").text_view()) + .button("quit", |siv| siv.quit()), + ); + None + } + Ok(Err(err)) => { + self.add_layer( + Dialog::new() + .content(format!("error: {err}").text_view()) + .button("ok", |siv| { + siv.pop_layer(); + }), + ); + None + } + } + } +} + +trait ViewExt: Sized { + fn float(self, title: &str) -> PaddedView<Panel<ScrollView<PaddedView<Self>>>>; + fn pad(self) -> PaddedView<Self>; + fn hpad(self) -> PaddedView<Self>; +} + +impl<V: Sized> ViewExt for V { + fn float(self, title: &str) -> PaddedView<Panel<ScrollView<PaddedView<Self>>>> { + PaddedView::new( + Margins::lrtb(1, 1, 1, 1), + Panel::new(ScrollView::new(PaddedView::new(Margins::lr(1, 1), self))).title(title), + ) + } + + fn pad(self) -> PaddedView<Self> { + PaddedView::new(Margins::lrtb(1, 1, 1, 1), self) + } + + fn hpad(self) -> PaddedView<Self> { + PaddedView::new(Margins::lr(1, 1), self) + } +} + +trait ToTextView { + fn text_view(self) -> TextView; +} + +impl<S: Into<StyledString>> ToTextView for S { + fn text_view(self) -> TextView { + TextView::new(self) + } +} |