diff --git a/Cargo.lock b/Cargo.lock index bc7cbed..6c70fe2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -74,6 +89,22 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "async-compression" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "zstd", + "zstd-safe", +] + [[package]] name = "async-lock" version = "3.4.0" @@ -217,6 +248,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-login" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5260ed0ecc8ace8e7e61a7406672faba598c8a86b8f4742fcdde0ddc979a318f" +dependencies = [ + "async-trait", + "axum", + "form_urlencoded", + "serde", + "subtle", + "thiserror 1.0.69", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions", + "tracing", + "urlencoding", +] + [[package]] name = "axum-server" version = "0.7.1" @@ -311,6 +362,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -341,6 +413,8 @@ version = "1.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -490,6 +564,17 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -520,6 +605,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -623,6 +717,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "derive-where" version = "1.2.7" @@ -750,6 +854,16 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.11.1" @@ -1396,6 +1510,15 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -1718,6 +1841,7 @@ checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", + "serde", ] [[package]] @@ -1890,6 +2014,12 @@ dependencies = [ "zeroize", ] +[[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" @@ -2121,6 +2251,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[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.20" @@ -2397,6 +2533,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "rpassword" version = "7.3.1" @@ -2721,6 +2879,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -2795,8 +2962,11 @@ name = "sparse-server" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "axum", + "axum-login", "axum-server", + "cfg-if", "chrono", "codee", "console_error_panic_hook", @@ -2816,10 +2986,13 @@ dependencies = [ "sqlx", "structopt", "thiserror 1.0.69", + "time", "tokio", "tokio-stream", "tower 0.4.13", "tower-http 0.5.2", + "tower-sessions", + "tower-sessions-sqlx-store", "tracing", "tracing-subscriber", "wasm-bindgen", @@ -2894,6 +3067,7 @@ dependencies = [ "sha2", "smallvec", "thiserror 2.0.11", + "time", "tokio", "tokio-stream", "tracing", @@ -2978,6 +3152,7 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror 2.0.11", + "time", "tracing", "whoami", ] @@ -3016,6 +3191,7 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror 2.0.11", + "time", "tracing", "whoami", ] @@ -3040,6 +3216,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", + "time", "tracing", "url", ] @@ -3268,6 +3445,37 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -3304,6 +3512,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -3431,14 +3640,33 @@ 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", + "cookie", + "futures-util", + "http", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-http" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ + "async-compression", "bitflags 2.8.0", "bytes", + "futures-core", "futures-util", "http", "http-body", @@ -3493,6 +3721,71 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tower-sessions" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65856c81ee244e0f8a55ab0f7b769b72fbde387c235f0a73cd97c579818d05eb" +dependencies = [ + "async-trait", + "http", + "time", + "tokio", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions-core", + "tower-sessions-memory-store", + "tracing", +] + +[[package]] +name = "tower-sessions-core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb6abbfcaf6436ec5a772cd9f965401da12db793e404ae6134eac066fa5a04f3" +dependencies = [ + "async-trait", + "axum-core", + "base64", + "futures", + "http", + "parking_lot", + "rand", + "serde", + "serde_json", + "thiserror 1.0.69", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "tower-sessions-memory-store" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fad75660c8afbe74f4e7cbbe8e9090171a056b57370ea4d7d5e9eb3e4af3092" +dependencies = [ + "async-trait", + "time", + "tokio", + "tower-sessions-core", +] + +[[package]] +name = "tower-sessions-sqlx-store" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd38eba51214e99accab78f6b7c8e273e90a9cb57575e86b592c60074e182d7" +dependencies = [ + "async-trait", + "rmp-serde", + "sqlx", + "thiserror 1.0.69", + "time", + "tower-sessions-core", +] + [[package]] name = "tracing" version = "0.1.41" @@ -3680,6 +3973,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -4189,3 +4488,31 @@ dependencies = [ "quote", "syn 2.0.96", ] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 84875ec..6fe9ff3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ panic = "abort" [profile.wasm-release] inherits = "release" +strip = true opt-level = 'z' lto = true codegen-units = 1 diff --git a/packages.nix b/packages.nix index 0a5fd90..ede04c2 100644 --- a/packages.nix +++ b/packages.nix @@ -30,14 +30,6 @@ let src = craneLib.cleanCargoSource ./.; - commonArgs = buildEnvironment // { - inherit src; - strictDeps = true; - - nativeBuildInputs = buildTools.linux; - buildInputs = buildTools.all; - }; - fileSetForBeaconCrate = pkgs.lib.fileset.toSource { root = ./.; fileset = pkgs.lib.fileset.unions [ @@ -97,6 +89,14 @@ let ]; }; + commonArgs = buildEnvironment // { + inherit src; + strictDeps = true; + + nativeBuildInputs = buildTools.linux; + buildInputs = buildTools.all; + }; + freebsdArgs = commonArgs // { # Sigh... # For some reason, crane and cargo don't run the build script for FreeBSD @@ -249,8 +249,11 @@ let installPhase = '' mkdir -p $out/bin - cp target/x86_64-unknown-linux-musl/release/sparse-server $out/bin + cp target/x86_64-unknown-linux-gnu/release/sparse-server $out/bin ''; + doCheck = false; + + RUSTFLAGS = "-Ctarget-feature=+crt-static"; SPARSE_INSTALLER_LINUX = "${sparse-installer-linux}/bin/sparse-installer"; SPARSE_INSTALLER_FREEBSD = diff --git a/sparse-server/.cargo/config.toml b/sparse-server/.cargo/config.toml index bd3c77b..e126768 100644 --- a/sparse-server/.cargo/config.toml +++ b/sparse-server/.cargo/config.toml @@ -3,3 +3,10 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"] [target.x86_64-unknown-linux-gnu] rustflags = ["-C", "link-arg=-fuse-ld=mold"] + +[unstable] +build-std = ["std", "panic_abort", "core", "alloc"] +build-std-features = ["panic_immediate_abort"] + +[build] +rustflags = ["--cfg=has_std"] diff --git a/sparse-server/.sqlx/query-09801043d7da4a27d3388f289ef8bf040f1279bb1aee533f7ab45d375f6e0b70.json b/sparse-server/.sqlx/query-09801043d7da4a27d3388f289ef8bf040f1279bb1aee533f7ab45d375f6e0b70.json new file mode 100644 index 0000000..0794dc9 --- /dev/null +++ b/sparse-server/.sqlx/query-09801043d7da4a27d3388f289ef8bf040f1279bb1aee533f7ab45d375f6e0b70.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE users SET last_active = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "09801043d7da4a27d3388f289ef8bf040f1279bb1aee533f7ab45d375f6e0b70" +} diff --git a/sparse-server/.sqlx/query-36691252e9640a76c9381b00ab14931aaa45f8d1cd1de4697bcd726865719d70.json b/sparse-server/.sqlx/query-36691252e9640a76c9381b00ab14931aaa45f8d1cd1de4697bcd726865719d70.json new file mode 100644 index 0000000..0cb5a50 --- /dev/null +++ b/sparse-server/.sqlx/query-36691252e9640a76c9381b00ab14931aaa45f8d1cd1de4697bcd726865719d70.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO users (user_name, password_hash) VALUES (?, \"\")", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "36691252e9640a76c9381b00ab14931aaa45f8d1cd1de4697bcd726865719d70" +} diff --git a/sparse-server/.sqlx/query-ed123391fb7afe255dc30bb1006410e8537b03cc9ff00c248ccc7c34a4a8366c.json b/sparse-server/.sqlx/query-4eeb48b1e4f85bae416b9d91b663d25b9abb6ecb4a31700b95141937c2f8f1f9.json similarity index 68% rename from sparse-server/.sqlx/query-ed123391fb7afe255dc30bb1006410e8537b03cc9ff00c248ccc7c34a4a8366c.json rename to sparse-server/.sqlx/query-4eeb48b1e4f85bae416b9d91b663d25b9abb6ecb4a31700b95141937c2f8f1f9.json index 03cd149..09b87e6 100644 --- a/sparse-server/.sqlx/query-ed123391fb7afe255dc30bb1006410e8537b03cc9ff00c248ccc7c34a4a8366c.json +++ b/sparse-server/.sqlx/query-4eeb48b1e4f85bae416b9d91b663d25b9abb6ecb4a31700b95141937c2f8f1f9.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT user_id, user_name, (SELECT MAX(expires) FROM sessions s WHERE s.user_id = u.user_id) as last_active FROM users u", + "query": "SELECT user_id, user_name, last_active FROM users", "describe": { "columns": [ { @@ -28,5 +28,5 @@ true ] }, - "hash": "ed123391fb7afe255dc30bb1006410e8537b03cc9ff00c248ccc7c34a4a8366c" + "hash": "4eeb48b1e4f85bae416b9d91b663d25b9abb6ecb4a31700b95141937c2f8f1f9" } diff --git a/sparse-server/.sqlx/query-6bccf4d930b1603d7df48cdbc605dc9095185b0fdcc5bf3613966699a9e67577.json b/sparse-server/.sqlx/query-6bccf4d930b1603d7df48cdbc605dc9095185b0fdcc5bf3613966699a9e67577.json new file mode 100644 index 0000000..7169283 --- /dev/null +++ b/sparse-server/.sqlx/query-6bccf4d930b1603d7df48cdbc605dc9095185b0fdcc5bf3613966699a9e67577.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE users SET password_hash = ? WHERE user_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "6bccf4d930b1603d7df48cdbc605dc9095185b0fdcc5bf3613966699a9e67577" +} diff --git a/sparse-server/.sqlx/query-7ca12d1edd84924ca65f597196eb618e4a313caf315a90aceaaaa253ff25947b.json b/sparse-server/.sqlx/query-7ca12d1edd84924ca65f597196eb618e4a313caf315a90aceaaaa253ff25947b.json new file mode 100644 index 0000000..cf21b27 --- /dev/null +++ b/sparse-server/.sqlx/query-7ca12d1edd84924ca65f597196eb618e4a313caf315a90aceaaaa253ff25947b.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT * FROM users WHERE user_id = ?", + "describe": { + "columns": [ + { + "name": "user_id", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "user_name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "password_hash", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "last_active", + "ordinal": 3, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + true + ] + }, + "hash": "7ca12d1edd84924ca65f597196eb618e4a313caf315a90aceaaaa253ff25947b" +} diff --git a/sparse-server/.sqlx/query-a1833e1eebd2373430b370b6fb3f2bfba2c7451759b741f4e7f5a71a49d76417.json b/sparse-server/.sqlx/query-a1833e1eebd2373430b370b6fb3f2bfba2c7451759b741f4e7f5a71a49d76417.json deleted file mode 100644 index cbb1804..0000000 --- a/sparse-server/.sqlx/query-a1833e1eebd2373430b370b6fb3f2bfba2c7451759b741f4e7f5a71a49d76417.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO users (user_name, password_salt, password_hash) VALUES (?, \"\", \"\")", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "a1833e1eebd2373430b370b6fb3f2bfba2c7451759b741f4e7f5a71a49d76417" -} diff --git a/sparse-server/.sqlx/query-bcaa134040954a687027e3b9d3cc2d9e9c2ade4c04eee8abea4db9d15db70fce.json b/sparse-server/.sqlx/query-bcaa134040954a687027e3b9d3cc2d9e9c2ade4c04eee8abea4db9d15db70fce.json deleted file mode 100644 index c56b85a..0000000 --- a/sparse-server/.sqlx/query-bcaa134040954a687027e3b9d3cc2d9e9c2ade4c04eee8abea4db9d15db70fce.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE users SET password_hash = ?, password_salt = ? WHERE user_id = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "bcaa134040954a687027e3b9d3cc2d9e9c2ade4c04eee8abea4db9d15db70fce" -} diff --git a/sparse-server/.sqlx/query-e0951ca9b4ff37ca9d9c8c4ea1ab618ad0dc8cdff118b6d801b568592762a29f.json b/sparse-server/.sqlx/query-e0951ca9b4ff37ca9d9c8c4ea1ab618ad0dc8cdff118b6d801b568592762a29f.json new file mode 100644 index 0000000..7004b97 --- /dev/null +++ b/sparse-server/.sqlx/query-e0951ca9b4ff37ca9d9c8c4ea1ab618ad0dc8cdff118b6d801b568592762a29f.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT * FROM users WHERE user_name = ?", + "describe": { + "columns": [ + { + "name": "user_id", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "user_name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "password_hash", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "last_active", + "ordinal": 3, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + true + ] + }, + "hash": "e0951ca9b4ff37ca9d9c8c4ea1ab618ad0dc8cdff118b6d801b568592762a29f" +} diff --git a/sparse-server/Cargo.toml b/sparse-server/Cargo.toml index d4bf27d..f1950c6 100644 --- a/sparse-server/Cargo.toml +++ b/sparse-server/Cargo.toml @@ -9,13 +9,13 @@ crate-type = ["cdylib", "rlib"] [dependencies] leptos = { version = "^0.7", features = ["nightly"] } leptos_router = { version = "^0.7", features = ["nightly"] } -axum = { version = "^0.7", optional = true } +axum = { version = "^0.7", features = ["ws"], optional = true } console_error_panic_hook = "0.1" leptos_axum = { version = "^0.7", optional = true } leptos_meta = { version = "^0.7" } -tokio = { version = "1", features = ["rt-multi-thread"], optional = true } +tokio = { version = "1", features = ["rt-multi-thread", "signal"], optional = true } tower = { version = "0.4", optional = true } -tower-http = { version = "0.5", features = ["fs"], optional = true } +tower-http = { version = "0.5", features = ["fs", "compression-br", "compression-deflate", "compression-gzip", "compression-zstd"], optional = true } wasm-bindgen = "0.2" thiserror = "1" http = "1" @@ -37,14 +37,24 @@ pbkdf2 = { version = "0.12", features = ["simple", "sha2"], optional = true } sha2 = { version = "0.10", optional = true } hex = { version = "0.4", optional = true } serde = "1.0" +axum-login = { version = "0.16.0", optional = true } +async-trait = "0.1.85" +cfg-if = "1.0.0" +tower-sessions = { version = "0.13.0", optional = true } +tower-sessions-sqlx-store = { version = "0.14.0", features = ["sqlite"], optional = true } +time = { version = "0.3.37", optional = true } [features] hydrate = ["leptos/hydrate", "chrono/wasmbind"] ssr = [ "dep:axum", + "dep:axum-login", "dep:tokio", + "dep:time", "dep:tower", "dep:tower-http", + "dep:tower-sessions", + "dep:tower-sessions-sqlx-store", "dep:leptos_axum", "dep:axum-server", "dep:tracing-subscriber", @@ -60,8 +70,7 @@ ssr = [ "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", - "leptos-use/ssr", - "axum/ws" + "leptos-use/ssr" ] [package.metadata.leptos] diff --git a/sparse-server/migrations/20250128020256_simplify_password.sql b/sparse-server/migrations/20250128020256_simplify_password.sql new file mode 100644 index 0000000..26663e1 --- /dev/null +++ b/sparse-server/migrations/20250128020256_simplify_password.sql @@ -0,0 +1,4 @@ +ALTER TABLE users DROP COLUMN password_salt;-- Add migration script here +ALTER TABLE users ADD COLUMN last_active int; + +DROP TABLE sessions; diff --git a/sparse-server/src/app.rs b/sparse-server/src/app.rs index e276e92..ddcdc74 100644 --- a/sparse-server/src/app.rs +++ b/sparse-server/src/app.rs @@ -2,8 +2,9 @@ use leptos::prelude::*; use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title}; use leptos_router::{ components::{A, Route, Router, Routes}, - StaticSegment, + path }; +use serde::{Serialize, Deserialize}; #[server] pub async fn test_retrieve() -> Result { @@ -20,6 +21,58 @@ pub async fn test_retrieve() -> Result { Ok(since_the_epoch) } +#[derive(Clone, Serialize, Deserialize)] +pub struct User { + user_id: i64, + user_name: String, +} + +#[server] +async fn me() -> Result, ServerFnError> { + let session: crate::db::user::AuthSession = leptos_axum::extract().await?; + + Ok(session.user.map(|user| User { + user_id: user.user_id, + user_name: user.user_name + })) +} + +#[server] +async fn login(username: String, password: String, next: Option) -> Result<(), ServerFnError> { + use leptos::server_fn::error::NoCustomError; + + let mut session: crate::db::user::AuthSession = leptos_axum::extract().await?; + + let user = match session.authenticate((username, password).clone()).await { + Ok(Some(user)) => user, + Ok(None) => return Err(ServerFnError::::ServerError("Invalid credentials".to_string())), + Err(e) => return Err(server_fn::server_fn_error!(e).into()) + }; + + if let Err(e) = session.login(&user).await { + return Err(server_fn::server_fn_error!(e).into()); + } + + if let Some(target) = next { + leptos_axum::redirect(&target); + } + + Ok(()) +} + +#[server] +async fn logout() -> Result<(), ServerFnError> { + let mut session: crate::db::user::AuthSession = leptos_axum::extract().await?; + + match session.logout().await { + Ok(_) => { + leptos_axum::redirect("/login"); + Ok(()) + } + Err(e) => Err(server_fn::server_fn_error!(e).into()) + } +} + pub fn shell(options: LeptosOptions) -> impl IntoView { view! { @@ -42,34 +95,71 @@ pub fn shell(options: LeptosOptions) -> impl IntoView { pub fn App() -> impl IntoView { provide_meta_context(); + let user = Resource::new(|| (), |_| async { me().await }); + view! { // sets the document title - + <Title text="Sparse Control"/> - // content for this welcome page <Router> <nav> <h1>"Sparse control"</h1> <A href="/">"Home"</A> + <Suspense fallback=|| ()> <A href="/beacons">"Beacon management"</A> <A href="/users">"Users"</A> + {move || user + .get() + .map(|err| err.ok()) + .flatten() + .flatten() + .map(|_| view! { + <a + href="#" + on:click=move |_| { + leptos::task::spawn_local(async move { + let _ = logout().await; + user.refetch(); + }); + } + > + "Log out" + </a> + })} + {move || user + .get() + .map(|err| err.ok()) + .flatten() + .flatten() + .is_none() + .then(|| view! { + <A href="/login">"Log in"</A> + })} + </Suspense> </nav> + <aside class="beacons"> </aside> + <Routes fallback=|| "Page not found.".into_view()> - <Route path=StaticSegment("") view=HomePage/> - <Route path=StaticSegment("/users") view=crate::users::UserView/> + <Route path=path!("users") view=crate::users::UserView /> + <Route path=path!("login") view=move || view! { <LoginPage /> } /> + <Route path=path!("") view=HomePage/> </Routes> </Router> } } +#[component] +fn LoginPage() -> impl IntoView { + +} + /// Renders the home page of your application. #[component] fn HomePage() -> impl IntoView { - use leptos_use::{UseWebSocketReturn, use_websocket}; // Creates a reactive value to update the button let count = RwSignal::new(0); @@ -91,23 +181,31 @@ fn HomePage() -> impl IntoView { let pending = request_time.pending(); let text_input = RwSignal::new("".to_owned()); + #[cfg_attr(feature = "ssr", allow(unused_variables))] let (messages, set_messages) = signal(Vec::<String>::new()); + cfg_if::cfg_if! { + if #[cfg(feature = "hydrate")] { + use leptos_use::{UseWebSocketReturn, use_websocket}; - let UseWebSocketReturn { send, message, .. } = use_websocket::<String, String, codee::string::FromToStringCodec>("/ws"); + let UseWebSocketReturn { send, message, .. } = use_websocket::<String, String, codee::string::FromToStringCodec>("/ws"); - Effect::new(move |_| { - message.with(move |message| { - if let Some(m) = message { - leptos::logging::log!("got update: {}", m); - set_messages.update(|messages: &mut Vec<_>| messages.push(format!("msg: {}", m))); - } - }) - }); + Effect::new(move |_| { + message.with(move |message| { + if let Some(m) = message { + leptos::logging::log!("got update: {}", m); + set_messages.update(|messages: &mut Vec<_>| messages.push(format!("msg: {}", m))); + } + }) + }); - let send_message = move |_| { - send(&text_input.get()); - text_input.set("".to_string()); - }; + let send_message = move |_| { + send(&text_input.get()); + text_input.set("".to_string()); + }; + } else { + let send_message = move |_| {}; + } + } view! { <main class="main"> diff --git a/sparse-server/src/cli.rs b/sparse-server/src/cli.rs index a7313c1..8923f63 100644 --- a/sparse-server/src/cli.rs +++ b/sparse-server/src/cli.rs @@ -1,4 +1,5 @@ -use std::path::PathBuf; +use std::{net::SocketAddrV4, path::PathBuf}; + use structopt::StructOpt; pub mod user; @@ -25,7 +26,15 @@ pub struct Options { #[structopt()] pub enum Command { /// Run the web and API server - Serve {}, + Serve { + /// Address to bind to for the management interface + #[structopt(default_value = "127.0.0.1:3000")] + management_address: SocketAddrV4, + + /// Public address to bind to for the beacons to call back to + #[structopt(default_value = "127.0.0.1:5000")] + bind_address: SocketAddrV4, + }, /// Extract the public key and print it to standard out ExtractPubKey {}, diff --git a/sparse-server/src/cli/user.rs b/sparse-server/src/cli/user.rs index 16def65..5f39aca 100644 --- a/sparse-server/src/cli/user.rs +++ b/sparse-server/src/cli/user.rs @@ -1,7 +1,7 @@ use std::process::ExitCode; use futures_util::StreamExt; -use sqlx::{Database, query, sqlite::SqlitePool}; +use sqlx::{query, sqlite::SqlitePool}; use crate::cli::UserCommand as UC; diff --git a/sparse-server/src/db.rs b/sparse-server/src/db.rs index 4173097..dce40e6 100644 --- a/sparse-server/src/db.rs +++ b/sparse-server/src/db.rs @@ -1,15 +1,2 @@ #[cfg(feature = "ssr")] pub mod user; - -pub struct User { - pub user_id: i16, - pub user_name: String, - pub password_salt: String, - pub password_hash: String, -} - -pub struct Sessions { - pub session_id: String, - pub user_id: i16, - pub expires: chrono::DateTime<chrono::offset::Local>, -} diff --git a/sparse-server/src/db/user.rs b/sparse-server/src/db/user.rs index d3d5d62..a0fe834 100644 --- a/sparse-server/src/db/user.rs +++ b/sparse-server/src/db/user.rs @@ -1,29 +1,77 @@ -use pbkdf2::{pbkdf2_hmac_array, password_hash::{rand_core::OsRng, SaltString}}; -use sha2::Sha256; +#[derive(Clone)] +pub struct User { + pub user_id: i64, + pub user_name: String, + password_hash: String, + pub last_active: Option<i64> +} + +use async_trait::async_trait; +use pbkdf2::{Pbkdf2, password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, rand_core::OsRng, SaltString}}; +use axum_login::{AuthUser, AuthnBackend, UserId}; +use sqlx::SqlitePool; use crate::error::Error; -const PASSWORD_ITERATIONS: u32 = 100_000; +impl std::fmt::Debug for User { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("User") + .field("user_id", &self.user_id) + .field("user_name", &self.user_name) + .field("password_hash", &"[redacted]") + .finish() + } +} + +impl AuthUser for User { + type Id = i64; + + fn id(&self) -> Self::Id { + self.user_id + } + + fn session_auth_hash(&self) -> &[u8] { + self.password_hash.as_bytes() + } +} + +async fn hash_password(pass: &[u8]) -> Result<String, Error> { + Ok(tokio::task::spawn_blocking({ + let pass = pass.to_owned(); + let salt = SaltString::generate(&mut OsRng); + + move || Pbkdf2.hash_password( + &*pass, + &salt, + ).map(|hash| hash.to_string()) + }).await??) +} + +async fn verify_password(pass: &str, hash: &str) -> Result<bool, Error> { + Ok(tokio::task::spawn_blocking({ + let pass = pass.to_owned(); + let hash = hash.to_owned(); + + move || + PasswordHash::new(&*hash) + .map(|parsed| Pbkdf2.verify_password( + &pass.as_bytes(), + &parsed + ).is_ok()) + }).await??) +} pub async fn reset_password<'a, E>(pool: E, id: i16, password: String) -> Result<(), crate::error::Error> where E: sqlx::SqliteExecutor<'a> { - let salt = SaltString::generate(&mut OsRng); - - let key = pbkdf2_hmac_array::<Sha256, 20>( - password.as_bytes(), - salt.as_str().as_bytes(), - PASSWORD_ITERATIONS - ); - - let salt_string = hex::encode(salt.as_str().as_bytes()); - let password_string = hex::encode(&key[..]); + let password_string = hash_password( + password.as_bytes() + ).await?; sqlx::query!( - "UPDATE users SET password_hash = ?, password_salt = ? WHERE user_id = ?", + "UPDATE users SET password_hash = ? WHERE user_id = ?", password_string, - salt_string, id ) .execute(pool) @@ -52,7 +100,7 @@ where tracing::info!("Creating new user {}", name); let new_id = sqlx::query!( - r#"INSERT INTO users (user_name, password_salt, password_hash) VALUES (?, "", "")"#, + r#"INSERT INTO users (user_name, password_hash) VALUES (?, "")"#, name ) .execute(&mut *tx) @@ -65,3 +113,68 @@ where Ok(()) } + +#[derive(Clone)] +pub struct Backend(SqlitePool); + +impl Backend { + pub fn new(db: SqlitePool) -> Self { + Self(db) + } +} + +#[async_trait] +impl AuthnBackend for Backend { + type User = User; + type Credentials = (String, String); + type Error = Error; + + async fn authenticate( + &self, + creds: Self::Credentials + ) -> Result<Option<Self::User>, Self::Error> { + let user: Option<Self::User> = sqlx::query_as!( + User, + "SELECT * FROM users WHERE user_name = ?", + creds.0 + ) + .fetch_optional(&self.0) + .await?; + + let Some(user) = user else { return Ok(None); }; + + let good_hash = verify_password( + &user.password_hash, + &creds.1 + ).await?; + + if good_hash { + let now = chrono::Utc::now().timestamp(); + + sqlx::query!( + "UPDATE users SET last_active = ?", + now + ) + .execute(&self.0) + .await?; + + Ok(Some(user)) + } else { + Ok(None) + } + } + + async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> { + let user: Option<Self::User> = sqlx::query_as!( + User, + "SELECT * FROM users WHERE user_id = ?", + user_id + ) + .fetch_optional(&self.0) + .await?; + + Ok(user) + } +} + +pub type AuthSession = axum_login::AuthSession<Backend>; diff --git a/sparse-server/src/error.rs b/sparse-server/src/error.rs index 9e5d010..12cb6fe 100644 --- a/sparse-server/src/error.rs +++ b/sparse-server/src/error.rs @@ -1,13 +1,21 @@ #[derive(Debug)] pub enum Error { + Generic(String), UserCreate(String), #[cfg(feature = "ssr")] Sqlx(sqlx::Error), + #[cfg(feature = "ssr")] + TokioJoin(tokio::task::JoinError), + #[cfg(feature = "ssr")] + Pbkdf2(pbkdf2::password_hash::errors::Error), } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Error::Generic(err) => { + write!(f, "generic error: {err}") + } Error::UserCreate(err) => { write!(f, "user create error: {err}") } @@ -15,6 +23,14 @@ impl std::fmt::Display for Error { Error::Sqlx(err) => { write!(f, "sqlx error: {err:?}") } + #[cfg(feature = "ssr")] + Error::TokioJoin(err) => { + write!(f, "tokio join error: {err:?}") + } + #[cfg(feature = "ssr")] + Error::Pbkdf2(err) => { + write!(f, "password hash error: {err:?}") + } } } } @@ -24,14 +40,38 @@ impl std::error::Error for Error { match self { #[cfg(feature = "ssr")] Error::Sqlx(err) => Some(err), + #[cfg(feature = "ssr")] + Error::TokioJoin(err) => Some(err), _ => None, } } } +impl std::str::FromStr for Error { + type Err = Self; + + fn from_str(err: &str) -> Result<Self, Self::Err> { + Ok(Self::Generic(err.to_string())) + } +} + #[cfg(feature = "ssr")] impl From<sqlx::Error> for Error { fn from(err: sqlx::Error) -> Self { Self::Sqlx(err) } } + +#[cfg(feature = "ssr")] +impl From<tokio::task::JoinError> for Error { + fn from(err: tokio::task::JoinError) -> Self { + Self::TokioJoin(err) + } +} + +#[cfg(feature = "ssr")] +impl From<pbkdf2::password_hash::errors::Error> for Error { + fn from(err: pbkdf2::password_hash::errors::Error) -> Self { + Self::Pbkdf2(err) + } +} diff --git a/sparse-server/src/main.rs b/sparse-server/src/main.rs index 8bcd358..078aad3 100644 --- a/sparse-server/src/main.rs +++ b/sparse-server/src/main.rs @@ -1,10 +1,15 @@ #[cfg(feature = "ssr")] pub(crate) mod beacons { + #[allow(dead_code)] pub const LINUX_BEACON: &'static [u8] = include_bytes!(std::env!("SPARSE_BEACON_LINUX")); + #[allow(dead_code)] pub const FREEBSD_BEACON: &'static [u8] = include_bytes!(std::env!("SPARSE_BEACON_FREEBSD")); + #[allow(dead_code)] pub const WINDOWS_BEACON: &'static [u8] = include_bytes!(std::env!("SPARSE_BEACON_WINDOWS")); + #[allow(dead_code)] pub const LINUX_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_LINUX")); + #[allow(dead_code)] pub const FREEBSD_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_FREEBSD")); } @@ -78,9 +83,9 @@ async fn main() -> anyhow::Result<std::process::ExitCode> { tracing::info!("Done running database migrations!"); match options.command.clone() { - Some(cli::Command::Serve { }) => { + Some(cli::Command::Serve { management_address, bind_address }) => { tracing::info!("Performing requested action, acting as web server"); - webserver::serve_web(options, pool).await + webserver::serve_web(management_address, bind_address, pool).await } Some(cli::Command::ExtractPubKey { }) => { Ok(ExitCode::SUCCESS) @@ -89,8 +94,13 @@ async fn main() -> anyhow::Result<std::process::ExitCode> { cli::user::handle_user_command(command, pool).await } None => { + use std::net::{Ipv4Addr, SocketAddrV4}; + tracing::info!("Performing default action of acting as web server"); - webserver::serve_web(options, pool).await + + let default_management_ip = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 3000); + let default_beacon_ip = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 5000); + webserver::serve_web(default_management_ip, default_beacon_ip, pool).await } } } diff --git a/sparse-server/src/users.rs b/sparse-server/src/users.rs index 5606430..35ff0a9 100644 --- a/sparse-server/src/users.rs +++ b/sparse-server/src/users.rs @@ -70,6 +70,7 @@ pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl Into let UseIntervalReturn { counter, .. } = use_interval(1000); let (time_ago, set_time_ago) = signal(user.last_active.map(|active| format_delta(Utc::now() - active))); + #[cfg(feature = "hydrate")] Effect::watch( move || counter.get(), move |_, _, _| { @@ -194,7 +195,7 @@ async fn list_users() -> Result<Vec<PubUser>, ServerFnError> { let users = sqlx::query_as!( DbUser, - "SELECT user_id, user_name, (SELECT MAX(expires) FROM sessions s WHERE s.user_id = u.user_id) as last_active FROM users u" + "SELECT user_id, user_name, last_active FROM users" ) .fetch(&pool) .map(|user| user.map(|u| PubUser { diff --git a/sparse-server/src/webserver.rs b/sparse-server/src/webserver.rs index 242df5f..87c0bdf 100644 --- a/sparse-server/src/webserver.rs +++ b/sparse-server/src/webserver.rs @@ -1,12 +1,14 @@ -use std::process::ExitCode; +use std::{net::SocketAddrV4, process::ExitCode}; use sqlx::sqlite::SqlitePool; use axum::Router; -use leptos::logging::log; use leptos::prelude::*; use leptos_axum::{generate_route_list, LeptosRoutes}; -use sparse_server::app::*; +use tokio::{signal, task::AbortHandle}; +use tower_sessions::{Expiry, SessionManagerLayer, session_store::ExpiredDeletion}; +use tower_sessions_sqlx_store::SqliteStore; +use sparse_server::app::*; pub async fn websocket(ws: axum::extract::ws::WebSocketUpgrade) -> axum::response::Response { tracing::info!("Handling websocket request to /ws"); @@ -14,7 +16,6 @@ pub async fn websocket(ws: axum::extract::ws::WebSocketUpgrade) -> axum::respons } async fn handle_websocket(mut socket: axum::extract::ws::WebSocket) { - use futures_util::StreamExt; use tracing::info; let mut count = 0; @@ -43,13 +44,34 @@ async fn handle_websocket(mut socket: axum::extract::ws::WebSocket) { } } -pub async fn serve_web(options: crate::cli::Options, db: SqlitePool) -> anyhow::Result<ExitCode> { +pub async fn serve_web(management_address: SocketAddrV4, _bind_address: SocketAddrV4, db: SqlitePool) -> anyhow::Result<ExitCode> { let conf = get_configuration(None).unwrap(); - let addr = conf.leptos_options.site_addr; let leptos_options = conf.leptos_options; - // Generate the list of routes in your Leptos App let routes = generate_route_list(App); + let session_store = SqliteStore::new(db.clone()); + session_store.migrate().await?; + + let deletion_task = tokio::task::spawn( + session_store + .clone() + .continuously_delete_expired(tokio::time::Duration::from_secs(60)) + ); + + let session_layer = SessionManagerLayer::new(session_store) + .with_secure(false) + .with_expiry(Expiry::OnInactivity(time::Duration::minutes(20))); + + + let backend = crate::db::user::Backend::new(db.clone()); + let auth_layer = axum_login::AuthManagerLayerBuilder::new(backend, session_layer).build(); + + let compression_layer = tower_http::compression::CompressionLayer::new() + .gzip(true) + .deflate(true) + .br(true) + .zstd(true); + let app = Router::new() .route("/ws", axum::routing::any(websocket)) .leptos_routes_with_context( @@ -61,13 +83,50 @@ pub async fn serve_web(options: crate::cli::Options, db: SqlitePool) -> anyhow:: move || shell(leptos_options.clone()) }) .fallback(leptos_axum::file_and_error_handler(shell)) - .with_state(leptos_options); + .with_state(leptos_options) + .layer(auth_layer) + .layer(compression_layer); // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` - tracing::info!("listening on http://{}", &addr); - let listener = tokio::net::TcpListener::bind(&addr).await?; - axum::serve(listener, app.into_make_service()).await?; + let management_listener = tokio::net::TcpListener::bind(&management_address).await?; + tracing::info!("management interface listening on http://{}", &management_address); + + axum::serve(management_listener, app.into_make_service()) + .with_graceful_shutdown(shutdown_signal(deletion_task.abort_handle())) + .await?; + + deletion_task.await??; Ok(ExitCode::SUCCESS) } + +async fn shutdown_signal(deletion_task_abort_handle: AbortHandle) { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => { + tracing::info!("Received Ctrl-C"); + deletion_task_abort_handle.abort() + }, + _ = terminate => { + tracing::info!("Received terminate command"); + deletion_task_abort_handle.abort() + }, + } +}