diff --git a/Cargo.lock b/Cargo.lock index 252343c..dca31c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,48 +33,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm-siv" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "polyval", - "subtle", - "zeroize", -] - [[package]] name = "ahash" version = "0.7.8" @@ -206,42 +164,6 @@ version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" -[[package]] -name = "apache-avro" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aef82843a0ec9f8b19567445ad2421ceeb1d711514384bdd3d49fe37102ee13" -dependencies = [ - "bigdecimal", - "digest", - "libflate", - "log", - "num-bigint", - "quad-rand", - "rand 0.8.5", - "regex-lite", - "serde", - "serde_bytes", - "serde_json", - "strum", - "strum_macros", - "thiserror 1.0.69", - "typed-builder 0.19.1", - "uuid", -] - -[[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 = "arrayvec" version = "0.7.6" @@ -275,9 +197,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" +checksum = "06575e6a9673580f52661c92107baabffbf41e2141373441cbcdc47cb733003c" dependencies = [ "brotli", "flate2", @@ -465,12 +387,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "auto-future" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" - [[package]] name = "autocfg" version = "1.4.0" @@ -484,7 +400,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" dependencies = [ "axum-core", - "axum-macros", "base64 0.22.1", "bytes", "form_urlencoded", @@ -509,7 +424,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-tungstenite", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", "tracing", @@ -544,7 +459,6 @@ dependencies = [ "axum", "axum-core", "bytes", - "cookie", "futures-util", "http", "http-body", @@ -552,52 +466,11 @@ dependencies = [ "mime", "pin-project-lite", "serde", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", ] -[[package]] -name = "axum-macros" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "axum-test" -version = "17.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317c1f4ecc1e68e0ad5decb78478421055c963ce215e736ed97463fa609cd196" -dependencies = [ - "anyhow", - "assert-json-diff", - "auto-future", - "axum", - "bytes", - "bytesize", - "cookie", - "http", - "http-body-util", - "hyper", - "hyper-util", - "mime", - "pretty_assertions", - "reserve-port", - "rust-multipart-rfc7578_2", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "tokio", - "tower 0.5.2", - "url", -] - [[package]] name = "backon" version = "1.4.0" @@ -624,18 +497,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "backtrace_printer" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d28de81c708c843640982b66573df0f0168d87e42854b563971f326745aab7" -dependencies = [ - "btparse-stable", - "colored", - "regex", - "thiserror 1.0.69", -] - [[package]] name = "base16ct" version = "0.2.0" @@ -669,18 +530,6 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c103cbbedac994e292597ab79342dbd5b306a362045095db54917d92a9fdfd92" -[[package]] -name = "bb8" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89aabfae550a5c44b43ab941844ffcd2e993cb6900b342debf59e9ea74acdb8" -dependencies = [ - "async-trait", - "futures-util", - "parking_lot 0.12.3", - "tokio", -] - [[package]] name = "bigdecimal" version = "0.4.7" @@ -746,15 +595,6 @@ dependencies = [ "wyz", ] -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -868,28 +708,12 @@ dependencies = [ "serde", ] -[[package]] -name = "btparse-stable" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d75b8252ed252f881d1dc4482ae3c3854df6ee8183c1906bac50ff358f4f89f" - [[package]] name = "bumpalo" version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" -[[package]] -name = "byte-unit" -version = "4.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da78b32057b8fdfc352504708feeba7216dcd65a2c9ab02978cbd288d1279b6c" -dependencies = [ - "serde", - "utf8-width", -] - [[package]] name = "bytecheck" version = "0.6.12" @@ -933,12 +757,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bytesize" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2c12f985c78475a6b8d629afd0c360260ef34cfef52efccdcfd31972f81c2e" - [[package]] name = "cacache" version = "13.1.0" @@ -1036,26 +854,6 @@ dependencies = [ "phf_codegen", ] -[[package]] -name = "chumsky" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" -dependencies = [ - "hashbrown 0.14.5", - "stacker", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - [[package]] name = "clap" version = "4.5.31" @@ -1139,20 +937,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", -] - [[package]] name = "commoncrypto" version = "0.2.0" @@ -1248,15 +1032,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "convert_case" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "cookie" version = "0.18.1" @@ -1312,15 +1087,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -1354,39 +1120,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "cron" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8c3e73077b4b4a6ab1ea5047c37c57aee77657bc8ecd6f29b0af082d0b0c07" -dependencies = [ - "chrono", - "nom 7.1.3", - "once_cell", -] - -[[package]] -name = "cron" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740" -dependencies = [ - "chrono", - "once_cell", - "winnow 0.6.26", -] - -[[package]] -name = "cron_clock" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a8699d8ed16e3db689f8ae04d8dc3c6666a4ba7e724e5a157884b7cc385d16b" -dependencies = [ - "chrono", - "nom 7.1.3", - "once_cell", -] - [[package]] name = "crossbeam-channel" version = "0.5.14" @@ -1430,26 +1163,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "cruet" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113a9e83d8f614be76de8df1f25bf9d0ea6e85ea573710a3d3f3abe1438ae49c" -dependencies = [ - "once_cell", - "regex", -] - -[[package]] -name = "cruet" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6132609543972496bc97b1e01f1ce6586768870aeb4cabeb3385f4e05b5caead" -dependencies = [ - "once_cell", - "regex", -] - [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1469,7 +1182,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] @@ -1530,36 +1242,6 @@ dependencies = [ "syn 2.0.98", ] -[[package]] -name = "csv" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "csv-core" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" -dependencies = [ - "memchr", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1622,12 +1304,6 @@ dependencies = [ "syn 2.0.98", ] -[[package]] -name = "dary_heap" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04d2cd9c18b9f454ed67da600630b021a8a80bf33f8c95896ab33aaf1c26b728" - [[package]] name = "dashmap" version = "5.5.3" @@ -1725,12 +1401,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - [[package]] name = "digest" version = "0.10.7" @@ -1758,26 +1428,7 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" dependencies = [ - "dirs-sys 0.4.1", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys 0.5.0", -] - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", + "dirs-sys", ] [[package]] @@ -1788,33 +1439,10 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users 0.4.6", + "redox_users", "windows-sys 0.48.0", ] -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.0", - "windows-sys 0.59.0", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users 0.4.6", - "winapi", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -1873,27 +1501,6 @@ dependencies = [ "dtoa", ] -[[package]] -name = "duct" -version = "0.13.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ab5718d1224b63252cd0c6f74f6480f9ffeb117438a2e0f5cf6d9a4798929c" -dependencies = [ - "libc", - "once_cell", - "os_pipe", - "shared_child", -] - -[[package]] -name = "duct_sh" -version = "0.13.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6633cadba557545fbbe0299a2f9adc4bb2fc5fb238773f5e841e0c23d62146" -dependencies = [ - "duct", -] - [[package]] name = "dyn-clone" version = "1.0.18" @@ -1938,12 +1545,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ego-tree" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c6ba7d4eec39eaa9ab24d44a0e73a7949a1095a8b3f3abb11eddf27dbb56a53" - [[package]] name = "ego-tree" version = "0.10.0" @@ -1980,22 +1581,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "email-encoding" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea3d894bbbab314476b265f9b2d46bf24b123a36dd0e96b06a1b49545b9d9dcc" -dependencies = [ - "base64 0.22.1", - "memchr", -] - -[[package]] -name = "email_address" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" - [[package]] name = "encode_unicode" version = "1.0.0" @@ -2011,16 +1596,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "english-to-cron" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c16423ac933fee80f05a52b435a912d5b08edbbbfe936e0042ebb3accdf303da" -dependencies = [ - "lazy_static", - "regex", -] - [[package]] name = "enum-as-inner" version = "0.6.1" @@ -2049,16 +1624,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "etag" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b3d0661a2ccddc26cba0b834e9b717959ed6fdd76c7129ee159c170a875bf44" -dependencies = [ - "str-buf", - "xxhash-rust", -] - [[package]] name = "etcetera" version = "0.8.0" @@ -2121,16 +1686,6 @@ dependencies = [ "ascii_utils", ] -[[package]] -name = "faster-hex" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" -dependencies = [ - "heapless", - "serde", -] - [[package]] name = "fastrand" version = "2.3.0" @@ -2181,12 +1736,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "flagset" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" - [[package]] name = "flate2" version = "1.1.0" @@ -2244,15 +1793,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs-err" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" -dependencies = [ - "autocfg", -] - [[package]] name = "funty" version = "2.0.0" @@ -2408,16 +1948,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "gethostname" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "getopts" version = "0.2.21" @@ -2544,15 +2074,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -2567,10 +2088,6 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash 0.8.11", - "allocator-api2", -] [[package]] name = "hashbrown" @@ -2616,16 +2133,6 @@ dependencies = [ "http", ] -[[package]] -name = "heapless" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" -dependencies = [ - "hash32", - "stable_deref_trait", -] - [[package]] name = "heck" version = "0.4.1" @@ -2638,18 +2145,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" - [[package]] name = "hex" version = "0.3.2" @@ -2745,17 +2240,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "hostname" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" -dependencies = [ - "cfg-if", - "libc", - "windows 0.52.0", -] - [[package]] name = "html-escape" version = "0.2.13" @@ -3183,25 +2667,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "include_dir" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" -dependencies = [ - "include_dir_macros", -] - -[[package]] -name = "include_dir_macros" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" -dependencies = [ - "proc-macro2", - "quote", -] - [[package]] name = "indenter" version = "0.3.3" @@ -3247,15 +2712,6 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - [[package]] name = "insta" version = "1.42.1" @@ -3305,23 +2761,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "ipnetwork" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" -dependencies = [ - "serde", -] - -[[package]] -name = "is-terminal" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" -dependencies = [ - "hermit-abi 0.4.0", - "libc", - "windows-sys 0.59.0", -] +checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" [[package]] name = "is_terminal_polyfill" @@ -3586,65 +3028,12 @@ dependencies = [ "tokio", ] -[[package]] -name = "lettre" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d476fe7a4a798f392ce34947aa7d53d981127e37523c5251da3c927f7fa901f" -dependencies = [ - "async-trait", - "base64 0.22.1", - "chumsky", - "email-encoding", - "email_address", - "fastrand", - "futures-io", - "futures-util", - "hostname 0.4.0", - "httpdate", - "idna", - "mime", - "nom 8.0.0", - "percent-encoding", - "quoted_printable", - "rustls", - "socket2", - "tokio", - "tokio-rustls", - "url", - "webpki-roots", -] - [[package]] name = "libc" version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" -[[package]] -name = "libflate" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d9dfdc14ea4ef0900c1cddbc8dcd553fbaacd8a4a282cf4018ae9dd04fb21e" -dependencies = [ - "adler32", - "core2", - "crc32fast", - "dary_heap", - "libflate_lz77", -] - -[[package]] -name = "libflate_lz77" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d" -dependencies = [ - "core2", - "hashbrown 0.14.5", - "rle-decode-fast", -] - [[package]] name = "libm" version = "0.2.11" @@ -3775,7 +3164,7 @@ version = "1.0.0-alpha.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c12744d1279367caed41739ef094c325d53fb0ffcd4f9b84a368796f870252" dependencies = [ - "convert_case 0.6.0", + "convert_case", "proc-macro2", "quote", "syn 1.0.109", @@ -3815,91 +3204,6 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "loco-gen" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec8762a66ade27c5157288ca46db51a77bbd95cb2d7ad87bcaada9621e3a4d9" -dependencies = [ - "chrono", - "clap", - "colored", - "cruet 0.14.0", - "duct", - "include_dir", - "regex", - "rrgen", - "serde", - "serde_json", - "thiserror 1.0.69", - "tracing", -] - -[[package]] -name = "loco-rs" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19decee5529e36ffd3e22f83cf601c1fcb35a8932886fc33c19e8482bc1baf88" -dependencies = [ - "argon2", - "async-trait", - "axum", - "axum-extra", - "axum-test", - "backtrace_printer", - "bb8", - "byte-unit", - "bytes", - "cfg-if", - "chrono", - "clap", - "colored", - "cruet 0.13.3", - "duct", - "duct_sh", - "english-to-cron", - "fs-err", - "futures-util", - "heck 0.4.1", - "hyper", - "include_dir", - "ipnetwork", - "jsonwebtoken", - "lettre", - "loco-gen", - "mime", - "moka", - "opendal 0.50.2", - "rand 0.8.5", - "regex", - "reqwest", - "rusty-sidekiq", - "scraper 0.21.0", - "sea-orm", - "sea-orm-migration", - "semver", - "serde", - "serde_json", - "serde_variant", - "serde_yaml", - "sqlx", - "tera", - "thiserror 1.0.69", - "thousands", - "tokio", - "tokio-cron-scheduler", - "tokio-util", - "toml", - "tower 0.4.13", - "tower-http", - "tracing", - "tracing-appender", - "tracing-subscriber", - "ulid", - "uuid", - "validator", -] - [[package]] name = "log" version = "0.4.26" @@ -4065,12 +3369,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.7.4" @@ -4165,8 +3463,6 @@ dependencies = [ "httparse", "memchr", "mime", - "serde", - "serde_json", "spin", "version_check", ] @@ -4206,16 +3502,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nom" version = "8.0.0" @@ -4243,7 +3529,6 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", - "serde", ] [[package]] @@ -4269,17 +3554,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - [[package]] name = "num-integer" version = "0.1.46" @@ -4310,25 +3584,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.9", - "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 = "oauth2" version = "5.0.0" @@ -4364,40 +3619,6 @@ version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "opendal" -version = "0.50.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb28bb6c64e116ceaf8dd4e87099d3cfea4a58e85e62b104fef74c91afba0f44" -dependencies = [ - "anyhow", - "async-trait", - "backon", - "base64 0.22.1", - "bytes", - "chrono", - "flagset", - "futures", - "getrandom 0.2.15", - "http", - "log", - "md-5", - "once_cell", - "percent-encoding", - "quick-xml 0.36.2", - "reqwest", - "serde", - "serde_json", - "tokio", - "uuid", -] - [[package]] name = "opendal" version = "0.51.2" @@ -4524,16 +3745,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "os_pipe" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "ouroboros" version = "0.18.5" @@ -4718,17 +3929,6 @@ dependencies = [ "regex", ] -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "paste" version = "1.0.15" @@ -4954,18 +4154,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - [[package]] name = "portable-atomic" version = "1.11.0" @@ -4993,16 +4181,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" -[[package]] -name = "pretty_assertions" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" -dependencies = [ - "diff", - "yansi", -] - [[package]] name = "primeorder" version = "0.13.6" @@ -5071,15 +4249,6 @@ version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" -[[package]] -name = "psm" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58e5423e24c18cc840e1c98370b3993c6649cd1678b4d24318bcf0a083cbe88" -dependencies = [ - "cc", -] - [[package]] name = "ptr_meta" version = "0.1.4" @@ -5126,16 +4295,10 @@ dependencies = [ "tap", "thiserror 2.0.11", "tracing", - "typed-builder 0.20.0", + "typed-builder", "url", ] -[[package]] -name = "quad-rand" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40" - [[package]] name = "quick-error" version = "1.2.3" @@ -5221,7 +4384,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e361f53ac3215518de2a005d132dbfa798dca29b1d46bcadae676f9a419671b4" dependencies = [ "cfg_rust_features", - "nom 8.0.0", + "nom", "percent-encoding", "thiserror 1.0.69", "url", @@ -5236,12 +4399,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "quoted_printable" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" - [[package]] name = "radium" version = "0.7.0" @@ -5344,6 +4501,7 @@ dependencies = [ "bollard", "bytes", "chrono", + "clap", "color-eyre", "cookie", "dotenv", @@ -5351,26 +4509,27 @@ dependencies = [ "fastrand", "figment", "futures", + "futures-util", "html-escape", "http", "http-cache", "http-cache-reqwest", "http-cache-semantics", "insta", + "ipnetwork", "itertools 0.14.0", "jwt-authorizer", "lazy_static", "leaky-bucket", "librqbit-core", "lightningcss", - "loco-rs", "log", "maplit", "mockito", "moka", - "nom 8.0.0", + "nom", "once_cell", - "opendal 0.51.2", + "opendal", "openidconnect", "qbit-rs", "quirks_path", @@ -5381,13 +4540,14 @@ dependencies = [ "reqwest-tracing", "rss", "rstest", - "scraper 0.22.0", + "scraper", "sea-orm", "sea-orm-migration", "seaography", "secrecy", "serde", "serde_json", + "serde_variant", "serde_with", "serde_yaml", "serial_test", @@ -5396,36 +4556,16 @@ dependencies = [ "testcontainers-modules", "thiserror 2.0.11", "tokio", - "tower 0.5.2", + "tower", "tower-http", "tracing", + "tracing-appender", "tracing-subscriber", "url", "uuid", - "zino", "zune-image", ] -[[package]] -name = "redis" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa8455fa3621f6b41c514946de66ea0531f57ca017b2e6c7cc368035ea5b46df" -dependencies = [ - "async-trait", - "bytes", - "combine", - "futures-util", - "itoa", - "percent-encoding", - "pin-project-lite", - "ryu", - "sha1_smol", - "tokio", - "tokio-util", - "url", -] - [[package]] name = "redox_syscall" version = "0.2.16" @@ -5464,17 +4604,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "redox_users" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" -dependencies = [ - "getrandom 0.2.15", - "libredox", - "thiserror 2.0.11", -] - [[package]] name = "reflink-copy" version = "0.1.24" @@ -5519,12 +4648,6 @@ dependencies = [ "regex-syntax 0.8.5", ] -[[package]] -name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - [[package]] name = "regex-syntax" version = "0.6.29" @@ -5558,13 +4681,11 @@ version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ - "async-compression", "base64 0.22.1", "bytes", "cookie", "cookie_store", "encoding_rs", - "futures-channel", "futures-core", "futures-util", "h2", @@ -5599,7 +4720,7 @@ dependencies = [ "tokio-native-tls", "tokio-rustls", "tokio-util", - "tower 0.5.2", + "tower", "tower-service", "url", "wasm-bindgen", @@ -5663,23 +4784,13 @@ dependencies = [ "tracing", ] -[[package]] -name = "reserve-port" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "359fc315ed556eb0e42ce74e76f4b1cd807b50fa6307f3de4e51f92dbe86e2d5" -dependencies = [ - "lazy_static", - "thiserror 2.0.11", -] - [[package]] name = "resolv-conf" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" dependencies = [ - "hostname 0.3.1", + "hostname", "quick-error", ] @@ -5745,31 +4856,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "rle-decode-fast" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" - -[[package]] -name = "rrgen" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e27f5f254d89b0b5b76445442e5c935b63a566ee5a735c9d877ca2029b4ce9" -dependencies = [ - "cruet 0.13.3", - "fs-err", - "glob", - "heck 0.4.1", - "regex", - "serde", - "serde_json", - "serde_regex", - "serde_yaml", - "tera", - "thiserror 1.0.69", -] - [[package]] name = "rsa" version = "0.9.7" @@ -5832,22 +4918,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "rust-multipart-rfc7578_2" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc4bb9e7c9abe5fa5f30c2d8f8fefb9e0080a2c1e3c2e567318d2907054b35d3" -dependencies = [ - "bytes", - "futures-core", - "futures-util", - "http", - "mime", - "mime_guess", - "rand 0.9.0", - "thiserror 2.0.11", -] - [[package]] name = "rust_decimal" version = "1.36.0" @@ -5904,7 +4974,6 @@ version = "0.23.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" dependencies = [ - "log", "once_cell", "ring", "rustls-pki-types", @@ -5960,34 +5029,6 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" -[[package]] -name = "rusty-sidekiq" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15544f047600b602c7b11ff7ee0882f9034f9cbe2c205693edd5615e2a6c03ee" -dependencies = [ - "async-trait", - "bb8", - "chrono", - "convert_case 0.6.0", - "cron_clock", - "gethostname", - "hex 0.4.3", - "num_cpus", - "rand 0.8.5", - "redis", - "serde", - "serde_json", - "serial_test", - "sha2", - "slog-term", - "thiserror 1.0.69", - "tokio", - "tokio-util", - "tracing", - "tracing-subscriber", -] - [[package]] name = "ryu" version = "1.0.19" @@ -6033,23 +5074,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scraper" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0e749d29b2064585327af5038a5a8eb73aeebad4a3472e83531a436563f7208" -dependencies = [ - "ahash 0.8.11", - "cssparser 0.34.0", - "ego-tree 0.9.0", - "getopts", - "html5ever", - "indexmap 2.7.1", - "precomputed-hash", - "selectors", - "tendril", -] - [[package]] name = "scraper" version = "0.22.0" @@ -6057,7 +5081,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc3d051b884f40e309de6c149734eab57aa8cc1347992710dc80bcc1c2194c15" dependencies = [ "cssparser 0.34.0", - "ego-tree 0.10.0", + "ego-tree", "getopts", "html5ever", "precomputed-hash", @@ -6368,15 +5392,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_bytes" -version = "0.11.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" -dependencies = [ - "serde", -] - [[package]] name = "serde_derive" version = "1.0.218" @@ -6419,27 +5434,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_qs" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" -dependencies = [ - "percent-encoding", - "serde", - "thiserror 1.0.69", -] - -[[package]] -name = "serde_regex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" -dependencies = [ - "regex", - "serde", -] - [[package]] name = "serde_repr" version = "0.1.19" @@ -6580,12 +5574,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - [[package]] name = "sha2" version = "0.10.8" @@ -6606,16 +5594,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shared_child" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fa9338aed9a1df411814a5b2252f7cd206c55ae9bf2fa763f8de84603aa60c" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "shlex" version = "1.3.0" @@ -6705,25 +5683,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "slog" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" - -[[package]] -name = "slog-term" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6e022d0b998abfe5c3782c1f03551a596269450ccd677ea51c56f8b214610e8" -dependencies = [ - "is-terminal", - "slog", - "term", - "thread_local", - "time", -] - [[package]] name = "slug" version = "0.1.6" @@ -7004,19 +5963,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "stacker" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9156ebd5870ef293bfb43f91c7a74528d363ec0d424afe24160ed5a4343d08a" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.59.0", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -7029,12 +5975,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" -[[package]] -name = "str-buf" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceb97b7225c713c2fd4db0153cb6b3cab244eb37900c3f634ed4d43310d8c34" - [[package]] name = "string_cache" version = "0.8.8" @@ -7250,17 +6190,6 @@ dependencies = [ "unic-segment", ] -[[package]] -name = "term" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" -dependencies = [ - "dirs-next", - "rustversion", - "winapi", -] - [[package]] name = "testcontainers" version = "0.23.3" @@ -7344,12 +6273,6 @@ dependencies = [ "syn 2.0.98", ] -[[package]] -name = "thousands" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" - [[package]] name = "thread_local" version = "1.1.8" @@ -7368,9 +6291,7 @@ checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", - "libc", "num-conv", - "num_threads", "powerfmt", "serde", "time-core", @@ -7436,21 +6357,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "tokio-cron-scheduler" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2594dd7c2abbbafbb1c78d167fd10860dc7bd75f814cb051a1e0d3e796b9702" -dependencies = [ - "chrono", - "cron 0.12.1", - "num-derive", - "num-traits", - "tokio", - "tracing", - "uuid", -] - [[package]] name = "tokio-macros" version = "2.5.0" @@ -7565,18 +6471,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.7.3", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "tower-layer", - "tower-service", - "tracing", + "winnow", ] [[package]] @@ -7654,7 +6549,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" dependencies = [ "crossbeam-channel", - "parking_lot 0.12.3", "thiserror 1.0.69", "time", "tracing-subscriber", @@ -7721,14 +6615,12 @@ dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "parking_lot 0.12.3", "regex", "serde", "serde_json", "sharded-slab", "smallvec", "thread_local", - "time", "tracing", "tracing-core", "tracing-log", @@ -7758,33 +6650,13 @@ dependencies = [ "utf-8", ] -[[package]] -name = "typed-builder" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06fbd5b8de54c5f7c91f6fe4cebb949be2125d7758e630bb58b1d831dbce600" -dependencies = [ - "typed-builder-macro 0.19.1", -] - [[package]] name = "typed-builder" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e14ed59dc8b7b26cacb2a92bad2e8b1f098806063898ab42a3bd121d7d45e75" dependencies = [ - "typed-builder-macro 0.20.0", -] - -[[package]] -name = "typed-builder-macro" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9534daa9fd3ed0bd911d462a37f172228077e7abf18c18a5f67199d959205f8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", + "typed-builder-macro", ] [[package]] @@ -7924,16 +6796,6 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -7988,44 +6850,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "utoipa" -version = "5.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" -dependencies = [ - "indexmap 2.7.1", - "serde", - "serde_json", - "utoipa-gen", -] - -[[package]] -name = "utoipa-gen" -version = "5.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", - "ulid", - "url", - "uuid", -] - -[[package]] -name = "utoipa-rapidoc" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5f8f5abd341cce16bb4f09a8bafc087d4884a004f25fb980e538d51d6501dab" -dependencies = [ - "axum", - "serde", - "serde_json", - "utoipa", -] - [[package]] name = "uuid" version = "1.15.0" @@ -8033,40 +6857,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd8dcafa1ca14750d8d7a05aa05988c17aab20886e1f3ae33a40223c58d92ef7" dependencies = [ "getrandom 0.3.1", - "rand 0.9.0", "serde", ] -[[package]] -name = "validator" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0b4a29d8709210980a09379f27ee31549b73292c87ab9899beee1c0d3be6303" -dependencies = [ - "idna", - "once_cell", - "regex", - "serde", - "serde_derive", - "serde_json", - "url", - "validator_derive", -] - -[[package]] -name = "validator_derive" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac855a2ce6f843beb229757e6e570a42e837bcb15e5f449dd48d5747d41bf77" -dependencies = [ - "darling", - "once_cell", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.98", -] - [[package]] name = "valuable" version = "0.1.1" @@ -8306,16 +7099,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core 0.52.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.58.0" @@ -8649,15 +7432,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.6.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "0.7.3" @@ -8844,156 +7618,6 @@ dependencies = [ "syn 2.0.98", ] -[[package]] -name = "zino" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "265118c999bdb42b8dbdfeb3a5f3375128b90ab2f0758bc626887fffaacdefe6" -dependencies = [ - "cfg-if", - "serde_json", - "zino-axum", - "zino-core", - "zino-http", - "zino-openapi", - "zino-storage", -] - -[[package]] -name = "zino-axum" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5862b177fabf349df302aaa9cda10ed6bdd125b6e07deb60b10a35ddc525457c" -dependencies = [ - "axum", - "futures", - "http", - "tokio", - "tower 0.5.2", - "tower-http", - "tracing", - "utoipa-rapidoc", - "zino-core", - "zino-http", - "zino-openapi", -] - -[[package]] -name = "zino-channel" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee50afd3b09309f256260362a2e13a96da50a6e1e59c14635942cb9a36aff22d" -dependencies = [ - "serde", - "serde_json", - "zino-core", -] - -[[package]] -name = "zino-core" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7ce0b6105ded8ff1c3719b5c055e141427239fbc1443ecc460720ebbe00208c" -dependencies = [ - "aes-gcm-siv", - "ahash 0.8.11", - "apache-avro", - "argon2", - "base64 0.22.1", - "cfg-if", - "chrono", - "convert_case 0.7.1", - "cron 0.15.0", - "csv", - "dirs", - "faster-hex", - "hkdf", - "hmac", - "http", - "parking_lot 0.12.3", - "rand 0.9.0", - "regex", - "reqwest", - "reqwest-middleware", - "reqwest-tracing", - "rust_decimal", - "serde", - "serde_json", - "serde_qs", - "sha1", - "sha2", - "smallvec", - "sqlx", - "toml", - "tracing", - "tracing-appender", - "tracing-log", - "tracing-subscriber", - "url", - "uuid", -] - -[[package]] -name = "zino-http" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d87dedb04d606d64da351c92c14fa33ab04777c8a20ec32c4302298512776c" -dependencies = [ - "bytes", - "cfg-if", - "etag", - "futures", - "http", - "mime_guess", - "multer", - "percent-encoding", - "regex", - "reqwest", - "ryu", - "serde", - "serde_json", - "serde_qs", - "smallvec", - "toml", - "tracing", - "url", - "zino-channel", - "zino-core", - "zino-storage", -] - -[[package]] -name = "zino-openapi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c40508d23d8aab6753093ddd60eee9866fc86b03109e8d682d1af70aa20e55bc" -dependencies = [ - "ahash 0.8.11", - "convert_case 0.7.1", - "serde_json", - "toml", - "tracing", - "utoipa", - "zino-core", -] - -[[package]] -name = "zino-storage" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef47e6602c68087e9018cc1cfccd852468525b0ba59ac4fb439855f41992325" -dependencies = [ - "bytes", - "etag", - "md-5", - "mime_guess", - "multer", - "reqwest", - "toml", - "tracing", - "zino-core", -] - [[package]] name = "zstd" version = "0.13.3" diff --git a/apps/recorder/Cargo.toml b/apps/recorder/Cargo.toml index 6eaf564..3580d7b 100644 --- a/apps/recorder/Cargo.toml +++ b/apps/recorder/Cargo.toml @@ -22,8 +22,6 @@ testcontainers = [ ] [dependencies] -loco-rs = { version = "0.14" } -zino = { version = "0.33", features = ["axum"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1.42", features = ["macros", "fs", "rt-multi-thread"] } @@ -97,8 +95,17 @@ seaography = { version = "1.1" } quirks_path = "0.1.1" base64 = "0.22.1" tower = "0.5.2" -axum-extra = "0.10.0" -tower-http = "0.6.2" +axum-extra = "0.10" +tower-http = { version = "0.6", features = [ + "trace", + "catch-panic", + "timeout", + "add-extension", + "cors", + "fs", + "set-header", + "compression-full", +] } serde_yaml = "0.9.34" tera = "1.20.0" openidconnect = { version = "4", features = ["rustls-tls"] } @@ -119,10 +126,14 @@ secrecy = { version = "0.10.3", features = ["serde"] } http = "1.2.0" cookie = "0.18.1" async-stream = "0.3.6" +serde_variant = "0.1.3" +tracing-appender = "0.2.3" +clap = "4.5.31" +futures-util = "0.3.31" +ipnetwork = "0.21.1" [dev-dependencies] serial_test = "3" -loco-rs = { version = "0.14", features = ["testing"] } insta = { version = "1", features = ["redactions", "yaml", "filters"] } mockito = "1.6.1" rstest = "0.24.0" diff --git a/apps/recorder/config/production.yaml b/apps/recorder/config/production.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/apps/recorder/config/test.yaml b/apps/recorder/config/test.yaml deleted file mode 100644 index ca7528b..0000000 --- a/apps/recorder/config/test.yaml +++ /dev/null @@ -1,125 +0,0 @@ -# Loco configuration file documentation - -# Application logging configuration -logger: - # Enable or disable logging. - enable: true - # Enable pretty backtrace (sets RUST_BACKTRACE=1) - pretty_backtrace: true - # Log level, options: trace, debug, info, warn or error. - level: debug - # Define the logging format. options: compact, pretty or Json - format: compact - # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries - # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters. - # override_filter: trace - -# Web server configuration -server: - # Port on which the server will listen. the server binding is 0.0.0.0:{PORT} - port: 5001 - # The UI hostname or IP address that mailers will point to. - host: http://webui.konobangu.com - # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block - middlewares: - # Enable Etag cache header middleware - etag: - enable: true - # Allows to limit the payload size request. payload that bigger than this file will blocked the request. - limit_payload: - # Enable/Disable the middleware. - enable: true - # the limit size. can be b,kb,kib,mb,mib,gb,gib - body_limit: 5mb - # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. - logger: - # Enable/Disable the middleware. - enable: true - # when your code is panicked, the request still returns 500 status code. - catch_panic: - # Enable/Disable the middleware. - enable: true - # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. - timeout_request: - # Enable/Disable the middleware. - enable: false - # Duration time in milliseconds. - timeout: 5000 - cors: - enable: true - # Set the value of the [`Access-Control-Allow-Origin`][mdn] header - # allow_origins: - # - https://loco.rs - # Set the value of the [`Access-Control-Allow-Headers`][mdn] header - # allow_headers: - # - Content-Type - # Set the value of the [`Access-Control-Allow-Methods`][mdn] header - # allow_methods: - # - POST - # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds - # max_age: 3600 - -# Worker Configuration -workers: - # specifies the worker mode. Options: - # - BackgroundQueue - Workers operate asynchronously in the background, processing queued. - # - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed. - # - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities. - mode: BackgroundQueue - -# Mailer Configuration. -mailer: - # SMTP mailer configuration. - smtp: - # Enable/Disable smtp mailer. - enable: true - # SMTP server host. e.x localhost, smtp.gmail.com - host: '{{ get_env(name="MAILER_HOST", default="localhost") }}' - # SMTP server port - port: 1025 - # Use secure connection (SSL/TLS). - secure: false - # auth: - # user: - # password: - -# Database Configuration -database: - # Database connection URI - uri: '{{ get_env(name="DATABASE_URL", default="postgres://konobangu:konobangu@127.0.0.1:5432/konobangu") }}' - # When enabled, the sql query will be logged. - enable_logging: true - # Set the timeout duration when acquiring a connection. - connect_timeout: 500 - # Set the idle duration before closing a connection. - idle_timeout: 500 - # Minimum number of connections for a pool. - min_connections: 1 - # Maximum number of connections for a pool. - max_connections: 1 - # Run migration up when application loaded - auto_migrate: true - # Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode - dangerously_truncate: false - # Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode - dangerously_recreate: false - -# Redis Configuration -redis: - # Redis connection URI - uri: '{{ get_env(name="REDIS_URL", default="redis://127.0.0.1:6379") }}' - # Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode - dangerously_flush: false - -settings: - dal: - data_dir: ./temp - mikan: - http_client: - exponential_backoff_max_retries: 3 - leaky_bucket_max_tokens: 2 - leaky_bucket_initial_tokens: 0 - leaky_bucket_refill_tokens: 1 - leaky_bucket_refill_interval: 500 - user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0" - base_url: "https://mikanani.me/" diff --git a/apps/recorder/config/development.yaml b/apps/recorder/recorder.config.yaml similarity index 50% rename from apps/recorder/config/development.yaml rename to apps/recorder/recorder.config.yaml index ef30ae4..fe063df 100644 --- a/apps/recorder/config/development.yaml +++ b/apps/recorder/recorder.config.yaml @@ -26,12 +26,6 @@ server: # Enable Etag cache header middleware etag: enable: true - # Allows to limit the payload size request. payload that bigger than this file will blocked the request. - limit_payload: - # Enable/Disable the middleware. - enable: true - # the limit size. can be b,kb,kib,mb,mib,gb,gib - body_limit: 5mb # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. logger: # Enable/Disable the middleware. @@ -60,32 +54,6 @@ server: # - POST # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds # max_age: 3600 - fallback: - enable: false - -# Worker Configuration -workers: - # specifies the worker mode. Options: - # - BackgroundQueue - Workers operate asynchronously in the background, processing queued. - # - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed. - # - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities. - mode: BackgroundAsync - -# Mailer Configuration. -mailer: - # SMTP mailer configuration. - smtp: - # Enable/Disable smtp mailer. - enable: true - # SMTP server host. e.x localhost, smtp.gmail.com - host: '{{ get_env(name="MAILER_HOST", default="localhost") }}' - # SMTP server port - port: 1025 - # Use secure connection (SSL/TLS). - secure: false - # auth: - # user: - # password: # Database Configuration database: @@ -103,43 +71,31 @@ database: max_connections: 1 # Run migration up when application loaded auto_migrate: true - # Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode - dangerously_truncate: false - # Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode - dangerously_recreate: false -# Redis Configuration -redis: - # Redis connection URI - uri: '{{ get_env(name="REDIS_URL", default="redis://localhost:6379") }}' - # Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode - dangerously_flush: false +storage: + data_dir: '{{ get_env(name="STORAGE_DATA_DIR", default="./data") }}' -settings: - dal: - data_dir: '{{ get_env(name="DAL_DATA_DIR", default="./data") }}' +mikan: + base_url: "https://mikanani.me/" + http_client: + exponential_backoff_max_retries: 3 + leaky_bucket_max_tokens: 2 + leaky_bucket_initial_tokens: 0 + leaky_bucket_refill_tokens: 1 + leaky_bucket_refill_interval: 500 - mikan: - base_url: "https://mikanani.me/" - http_client: - exponential_backoff_max_retries: 3 - leaky_bucket_max_tokens: 2 - leaky_bucket_initial_tokens: 0 - leaky_bucket_refill_tokens: 1 - leaky_bucket_refill_interval: 500 +auth: + auth_type: '{{ get_env(name="AUTH_TYPE", default = "basic") }}' + basic_user: '{{ get_env(name="BASIC_USER", default = "konobangu") }}' + basic_password: '{{ get_env(name="BASIC_PASSWORD", default = "konobangu") }}' + oidc_issuer: '{{ get_env(name="OIDC_ISSUER", default = "") }}' + oidc_audience: '{{ get_env(name="OIDC_AUDIENCE", default = "") }}' + oidc_client_id: '{{ get_env(name="OIDC_CLIENT_ID", default = "") }}' + oidc_client_secret: '{{ get_env(name="OIDC_CLIENT_SECRET", default = "") }}' + oidc_extra_scopes: '{{ get_env(name="OIDC_EXTRA_SCOPES", default = "") }}' + oidc_extra_claim_key: '{{ get_env(name="OIDC_EXTRA_CLAIM_KEY", default = "") }}' + oidc_extra_claim_value: '{{ get_env(name="OIDC_EXTRA_CLAIM_VALUE", default = "") }}' - auth: - auth_type: '{{ get_env(name="AUTH_TYPE", default = "basic") }}' - basic_user: '{{ get_env(name="BASIC_USER", default = "konobangu") }}' - basic_password: '{{ get_env(name="BASIC_PASSWORD", default = "konobangu") }}' - oidc_issuer: '{{ get_env(name="OIDC_ISSUER", default = "") }}' - oidc_audience: '{{ get_env(name="OIDC_AUDIENCE", default = "") }}' - oidc_client_id: '{{ get_env(name="OIDC_CLIENT_ID", default = "") }}' - oidc_client_secret: '{{ get_env(name="OIDC_CLIENT_SECRET", default = "") }}' - oidc_extra_scopes: '{{ get_env(name="OIDC_EXTRA_SCOPES", default = "") }}' - oidc_extra_claim_key: '{{ get_env(name="OIDC_EXTRA_CLAIM_KEY", default = "") }}' - oidc_extra_claim_value: '{{ get_env(name="OIDC_EXTRA_CLAIM_VALUE", default = "") }}' - - graphql: - depth_limit: null - complexity_limit: null +graphql: + depth_limit: null + complexity_limit: null diff --git a/apps/recorder/src/app/builder.rs b/apps/recorder/src/app/builder.rs index d689ffb..4f2b871 100644 --- a/apps/recorder/src/app/builder.rs +++ b/apps/recorder/src/app/builder.rs @@ -1,153 +1,136 @@ -use std::{path::Path, sync::Arc}; +use std::sync::Arc; -use figment::Figment; -use itertools::Itertools; +use clap::{Parser, command}; -use super::{core::App, env::Enviornment}; -use crate::{ - app::{config::AppConfig, context::create_context, router::create_router}, - errors::RResult, -}; +use super::{AppContext, core::App, env::Environment}; +use crate::{app::config::AppConfig, errors::RResult}; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct MainCliArgs { + /// Explicit config file path + #[arg(short, long)] + config_file: Option, + + /// Explicit dotenv file path + #[arg(short, long)] + dotenv_file: Option, + + /// Explicit working dir + #[arg(short, long)] + working_dir: Option, + + /// Explicit environment + #[arg(short, long)] + environment: Option, +} pub struct AppBuilder { dotenv_file: Option, config_file: Option, working_dir: String, - enviornment: Enviornment, + enviornment: Environment, } impl AppBuilder { - pub async fn load_dotenv(&self) -> RResult<()> { - let try_dotenv_file_or_dirs = if self.dotenv_file.is_some() { - vec![self.dotenv_file.as_deref()] - } else { - vec![Some(&self.working_dir as &str)] - }; + pub async fn from_main_cli(environment: Option) -> RResult { + let args = MainCliArgs::parse(); - let priority_suffix = &AppConfig::priority_suffix(&self.enviornment); - let dotenv_prefix = AppConfig::dotenv_prefix(); - let try_filenames = priority_suffix - .iter() - .map(|ps| format!("{}{}", &dotenv_prefix, ps)) - .collect_vec(); - - for try_dotenv_file_or_dir in try_dotenv_file_or_dirs.into_iter().flatten() { - let try_dotenv_file_or_dir_path = Path::new(try_dotenv_file_or_dir); - if try_dotenv_file_or_dir_path.exists() { - if try_dotenv_file_or_dir_path.is_dir() { - for f in try_filenames.iter() { - let p = try_dotenv_file_or_dir_path.join(f); - if p.exists() && p.is_file() { - dotenv::from_path(p)?; - break; - } - } - } else if try_dotenv_file_or_dir_path.is_file() { - dotenv::from_path(try_dotenv_file_or_dir_path)?; - break; + let environment = environment.unwrap_or_else(|| { + args.environment.unwrap_or({ + if cfg!(test) { + Environment::Testing + } else if cfg!(debug_assertions) { + Environment::Development + } else { + Environment::Production } - } - } - - Ok(()) - } - - pub async fn build_config(&self) -> RResult { - let try_config_file_or_dirs = if self.config_file.is_some() { - vec![self.config_file.as_deref()] - } else { - vec![Some(&self.working_dir as &str)] - }; - - let allowed_extensions = &AppConfig::allowed_extension(); - let priority_suffix = &AppConfig::priority_suffix(&self.enviornment); - let convention_prefix = &AppConfig::config_prefix(); - - let try_filenames = priority_suffix - .iter() - .flat_map(|ps| { - allowed_extensions - .iter() - .map(move |ext| (format!("{}{}{}", convention_prefix, ps, ext), ext)) }) - .collect_vec(); + }); - let mut fig = Figment::from(AppConfig::default_provider()); + let mut builder = Self::default(); - for try_config_file_or_dir in try_config_file_or_dirs.into_iter().flatten() { - let try_config_file_or_dir_path = Path::new(try_config_file_or_dir); - if try_config_file_or_dir_path.exists() { - if try_config_file_or_dir_path.is_dir() { - for (f, ext) in try_filenames.iter() { - let p = try_config_file_or_dir_path.join(f); - if p.exists() && p.is_file() { - fig = AppConfig::merge_provider_from_file(fig, &p, ext)?; - break; - } - } - } else if let Some(ext) = try_config_file_or_dir_path - .extension() - .and_then(|s| s.to_str()) - && try_config_file_or_dir_path.is_file() - { - fig = - AppConfig::merge_provider_from_file(fig, try_config_file_or_dir_path, ext)?; - break; - } - } + if let Some(working_dir) = args.working_dir { + builder = builder.working_dir(working_dir); + } + if matches!( + &environment, + Environment::Testing | Environment::Development + ) { + builder = builder.working_dir_from_manifest_dir(); } - let app_config: AppConfig = fig.extract()?; + builder = builder + .config_file(args.config_file) + .dotenv_file(args.dotenv_file) + .environment(environment); - Ok(app_config) + Ok(builder) } pub async fn build(self) -> RResult { - let _app_name = env!("CARGO_CRATE_NAME"); + AppConfig::load_dotenv( + &self.enviornment, + &self.working_dir, + self.dotenv_file.as_deref(), + ) + .await?; - let _app_version = format!( - "{} ({})", - env!("CARGO_PKG_VERSION"), - option_env!("BUILD_SHA") - .or(option_env!("GITHUB_SHA")) - .unwrap_or("dev") + let config = AppConfig::load_config( + &self.enviornment, + &self.working_dir, + self.config_file.as_deref(), + ) + .await?; + + let app_context = Arc::new( + AppContext::new(self.enviornment.clone(), config, self.working_dir.clone()).await?, ); - self.load_dotenv().await?; - - let config = self.build_config().await?; - - let app_context = Arc::new(create_context(config).await?); - - let router = create_router(app_context.clone()).await?; - Ok(App { context: app_context, - router, builder: self, }) } - pub fn set_working_dir(self, working_dir: String) -> Self { + pub fn working_dir(self, working_dir: String) -> Self { let mut ret = self; ret.working_dir = working_dir; ret } - pub fn set_working_dir_to_manifest_dir(self) -> Self { - let manifest_dir = if cfg!(debug_assertions) { + pub fn environment(self, environment: Environment) -> Self { + let mut ret = self; + ret.enviornment = environment; + ret + } + + pub fn config_file(self, config_file: Option) -> Self { + let mut ret = self; + ret.config_file = config_file; + ret + } + + pub fn dotenv_file(self, dotenv_file: Option) -> Self { + let mut ret = self; + ret.dotenv_file = dotenv_file; + ret + } + + pub fn working_dir_from_manifest_dir(self) -> Self { + let manifest_dir = if cfg!(debug_assertions) || cfg!(test) { env!("CARGO_MANIFEST_DIR") } else { "./apps/recorder" }; - self.set_working_dir(manifest_dir.to_string()) + self.working_dir(manifest_dir.to_string()) } } impl Default for AppBuilder { fn default() -> Self { Self { - enviornment: Enviornment::Production, + enviornment: Environment::Production, dotenv_file: None, config_file: None, working_dir: String::from("."), diff --git a/apps/recorder/src/app/config/default_mixin.toml b/apps/recorder/src/app/config/default_mixin.toml index 9186339..f20456b 100644 --- a/apps/recorder/src/app/config/default_mixin.toml +++ b/apps/recorder/src/app/config/default_mixin.toml @@ -14,3 +14,5 @@ leaky_bucket_refill_interval = 500 [graphql] depth_limit = inf complexity_limit = inf + +[cache] diff --git a/apps/recorder/src/app/config/mod.rs b/apps/recorder/src/app/config/mod.rs index 444f28a..a996cea 100644 --- a/apps/recorder/src/app/config/mod.rs +++ b/apps/recorder/src/app/config/mod.rs @@ -7,21 +7,26 @@ use figment::{ use itertools::Itertools; use serde::{Deserialize, Serialize}; -use super::env::Enviornment; +use super::env::Environment; use crate::{ - auth::AuthConfig, errors::RResult, extract::mikan::AppMikanConfig, - graphql::config::GraphQLConfig, storage::StorageConfig, + auth::AuthConfig, cache::CacheConfig, database::DatabaseConfig, errors::RResult, + extract::mikan::MikanConfig, graphql::GraphQLConfig, logger::LoggerConfig, + storage::StorageConfig, web::WebServerConfig, }; const DEFAULT_CONFIG_MIXIN: &str = include_str!("./default_mixin.toml"); const CONFIG_ALLOWED_EXTENSIONS: &[&str] = &[".toml", ".json", ".yaml", ".yml"]; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppConfig { + pub server: WebServerConfig, + pub cache: CacheConfig, pub auth: AuthConfig, - pub dal: StorageConfig, - pub mikan: AppMikanConfig, + pub storage: StorageConfig, + pub mikan: MikanConfig, pub graphql: GraphQLConfig, + pub logger: LoggerConfig, + pub database: DatabaseConfig, } impl AppConfig { @@ -40,13 +45,13 @@ impl AppConfig { .collect_vec() } - pub fn priority_suffix(enviornment: &Enviornment) -> Vec { + pub fn priority_suffix(environment: &Environment) -> Vec { vec![ - format!(".{}.local", enviornment.full_name()), - format!(".{}.local", enviornment.short_name()), + format!(".{}.local", environment.full_name()), + format!(".{}.local", environment.short_name()), String::from(".local"), - enviornment.full_name().to_string(), - enviornment.short_name().to_string(), + environment.full_name().to_string(), + environment.short_name().to_string(), String::from(""), ] } @@ -75,4 +80,97 @@ impl AppConfig { _ => unreachable!("unsupported config extension"), }) } + + pub async fn load_dotenv( + environment: &Environment, + working_dir: &str, + dotenv_file: Option<&str>, + ) -> RResult<()> { + let try_dotenv_file_or_dirs = if dotenv_file.is_some() { + vec![dotenv_file] + } else { + vec![Some(working_dir)] + }; + + let priority_suffix = &AppConfig::priority_suffix(environment); + let dotenv_prefix = AppConfig::dotenv_prefix(); + let try_filenames = priority_suffix + .iter() + .map(|ps| format!("{}{}", &dotenv_prefix, ps)) + .collect_vec(); + + for try_dotenv_file_or_dir in try_dotenv_file_or_dirs.into_iter().flatten() { + let try_dotenv_file_or_dir_path = Path::new(try_dotenv_file_or_dir); + if try_dotenv_file_or_dir_path.exists() { + if try_dotenv_file_or_dir_path.is_dir() { + for f in try_filenames.iter() { + let p = try_dotenv_file_or_dir_path.join(f); + if p.exists() && p.is_file() { + dotenv::from_path(p)?; + break; + } + } + } else if try_dotenv_file_or_dir_path.is_file() { + dotenv::from_path(try_dotenv_file_or_dir_path)?; + break; + } + } + } + + Ok(()) + } + + pub async fn load_config( + environment: &Environment, + working_dir: &str, + config_file: Option<&str>, + ) -> RResult { + let try_config_file_or_dirs = if config_file.is_some() { + vec![config_file] + } else { + vec![Some(working_dir)] + }; + + let allowed_extensions = &AppConfig::allowed_extension(); + let priority_suffix = &AppConfig::priority_suffix(environment); + let convention_prefix = &AppConfig::config_prefix(); + + let try_filenames = priority_suffix + .iter() + .flat_map(|ps| { + allowed_extensions + .iter() + .map(move |ext| (format!("{}{}{}", convention_prefix, ps, ext), ext)) + }) + .collect_vec(); + + let mut fig = Figment::from(AppConfig::default_provider()); + + for try_config_file_or_dir in try_config_file_or_dirs.into_iter().flatten() { + let try_config_file_or_dir_path = Path::new(try_config_file_or_dir); + if try_config_file_or_dir_path.exists() { + if try_config_file_or_dir_path.is_dir() { + for (f, ext) in try_filenames.iter() { + let p = try_config_file_or_dir_path.join(f); + if p.exists() && p.is_file() { + fig = AppConfig::merge_provider_from_file(fig, &p, ext)?; + break; + } + } + } else if let Some(ext) = try_config_file_or_dir_path + .extension() + .and_then(|s| s.to_str()) + && try_config_file_or_dir_path.is_file() + { + fig = + AppConfig::merge_provider_from_file(fig, try_config_file_or_dir_path, ext)?; + break; + } + } + } + + let app_config: AppConfig = fig.extract()?; + + Ok(app_config) + } } diff --git a/apps/recorder/src/app/context.rs b/apps/recorder/src/app/context.rs index c84a3c9..584f5b2 100644 --- a/apps/recorder/src/app/context.rs +++ b/apps/recorder/src/app/context.rs @@ -1,13 +1,13 @@ -use sea_orm::DatabaseConnection; - -use super::config::AppConfig; +use super::{Environment, config::AppConfig}; use crate::{ - auth::AuthService, cache::CacheService, errors::RResult, extract::mikan::MikanClient, - graphql::GraphQLService, storage::StorageService, + auth::AuthService, cache::CacheService, database::DatabaseService, errors::RResult, + extract::mikan::MikanClient, graphql::GraphQLService, logger::LoggerService, + storage::StorageService, }; pub struct AppContext { - pub db: DatabaseConnection, + pub logger: LoggerService, + pub db: DatabaseService, pub config: AppConfig, pub cache: CacheService, pub mikan: MikanClient, @@ -15,8 +15,36 @@ pub struct AppContext { pub graphql: GraphQLService, pub storage: StorageService, pub working_dir: String, + pub environment: Environment, } -pub async fn create_context(_config: AppConfig) -> RResult { - todo!() +impl AppContext { + pub async fn new( + environment: Environment, + config: AppConfig, + working_dir: impl ToString, + ) -> RResult { + let config_cloned = config.clone(); + + let logger = LoggerService::from_config(config.logger).await?; + let cache = CacheService::from_config(config.cache).await?; + let db = DatabaseService::from_config(config.database).await?; + let storage = StorageService::from_config(config.storage).await?; + let auth = AuthService::from_conf(config.auth).await?; + let mikan = MikanClient::from_config(config.mikan).await?; + let graphql = GraphQLService::from_config_and_database(config.graphql, db.clone()).await?; + + Ok(AppContext { + config: config_cloned, + environment, + logger, + auth, + cache, + db, + storage, + mikan, + working_dir: working_dir.to_string(), + graphql, + }) + } } diff --git a/apps/recorder/src/app/core.rs b/apps/recorder/src/app/core.rs index f8237c0..55d13d2 100644 --- a/apps/recorder/src/app/core.rs +++ b/apps/recorder/src/app/core.rs @@ -1,15 +1,89 @@ -use std::sync::Arc; +use std::{net::SocketAddr, sync::Arc}; -use super::{builder::AppBuilder, context::AppContext, router::AppRouter}; +use axum::Router; +use futures::try_join; +use tokio::signal; + +use super::{builder::AppBuilder, context::AppContext}; +use crate::{ + errors::RResult, + web::{ + controller::{self, core::ControllerTrait}, + middleware::default_middleware_stack, + }, +}; pub struct App { pub context: Arc, pub builder: AppBuilder, - pub router: AppRouter, } impl App { pub fn builder() -> AppBuilder { AppBuilder::default() } + + pub async fn serve(&self) -> RResult<()> { + let context = &self.context; + let config = &context.config; + let listener = tokio::net::TcpListener::bind(&format!( + "{}:{}", + config.server.binding, config.server.port + )) + .await?; + + let mut router = Router::>::new(); + + let (graphqlc, oidcc) = try_join!( + controller::graphql::create(context.clone()), + controller::oidc::create(context.clone()), + )?; + + for c in [graphqlc, oidcc] { + router = c.apply_to(router); + } + + let middlewares = default_middleware_stack(context.clone()); + for mid in middlewares { + router = mid.apply(router)?; + tracing::info!(name = mid.name(), "+middleware"); + } + + let router = router + .with_state(context.clone()) + .into_make_service_with_connect_info::(); + + axum::serve(listener, router) + .with_graceful_shutdown(async move { + Self::shutdown_signal().await; + tracing::info!("shutting down..."); + }) + .await?; + + Ok(()) + } + + async fn shutdown_signal() { + 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 => {}, + () = terminate => {}, + } + } } diff --git a/apps/recorder/src/app/env.rs b/apps/recorder/src/app/env.rs index 9cab86f..9c68c47 100644 --- a/apps/recorder/src/app/env.rs +++ b/apps/recorder/src/app/env.rs @@ -1,10 +1,22 @@ -pub enum Enviornment { +use clap::ValueEnum; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, ValueEnum)] +#[serde(rename_all = "snake_case")] +#[value(rename_all = "snake_case")] +pub enum Environment { + #[serde(alias = "dev")] + #[value(alias = "dev")] Development, + #[serde(alias = "prod")] + #[value(alias = "prod")] Production, + #[serde(alias = "test")] + #[value(alias = "test")] Testing, } -impl Enviornment { +impl Environment { pub fn full_name(&self) -> &'static str { match &self { Self::Development => "development", diff --git a/apps/recorder/src/app/mod.rs b/apps/recorder/src/app/mod.rs index 8da570b..838b404 100644 --- a/apps/recorder/src/app/mod.rs +++ b/apps/recorder/src/app/mod.rs @@ -3,75 +3,10 @@ pub mod config; pub mod context; pub mod core; pub mod env; -pub mod router; pub use core::App; -use std::path::Path; -use async_trait::async_trait; +pub use builder::AppBuilder; +pub use config::AppConfig; pub use context::AppContext; -use loco_rs::{ - Result, - app::{AppContext as LocoAppContext, Hooks}, - boot::{BootResult, StartMode, create_app}, - config::Config, - controller::AppRoutes, - db::truncate_table, - environment::Environment, - prelude::*, - task::Tasks, -}; - -use crate::{migrations::Migrator, models::subscribers}; - -pub struct App1; - -#[async_trait] -impl Hooks for App1 { - fn app_version() -> String { - format!( - "{} ({})", - env!("CARGO_PKG_VERSION"), - option_env!("BUILD_SHA") - .or(option_env!("GITHUB_SHA")) - .unwrap_or("dev") - ) - } - - fn app_name() -> &'static str { - env!("CARGO_CRATE_NAME") - } - - async fn boot( - mode: StartMode, - environment: &Environment, - config: Config, - ) -> Result { - create_app::(mode, environment, config).await - } - - async fn initializers(_ctx: &LocoAppContext) -> Result>> { - let initializers: Vec> = vec![]; - - Ok(initializers) - } - - fn routes(_ctx: &LocoAppContext) -> AppRoutes { - AppRoutes::with_default_routes() - } - - fn register_tasks(_tasks: &mut Tasks) {} - - async fn truncate(ctx: &LocoAppContext) -> Result<()> { - truncate_table(&ctx.db, subscribers::Entity).await?; - Ok(()) - } - - async fn seed(_ctx: &LocoAppContext, _base: &Path) -> Result<()> { - Ok(()) - } - - async fn connect_workers(_ctx: &LocoAppContext, _queue: &Queue) -> Result<()> { - Ok(()) - } -} +pub use env::Environment; diff --git a/apps/recorder/src/app/router.rs b/apps/recorder/src/app/router.rs deleted file mode 100644 index 3a9f2b3..0000000 --- a/apps/recorder/src/app/router.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::sync::Arc; - -use axum::Router; -use futures::try_join; - -use crate::{ - app::AppContext, - controllers::{self, core::ControllerTrait}, - errors::RResult, -}; - -pub struct AppRouter { - pub root: Router>, -} - -pub async fn create_router(context: Arc) -> RResult { - let mut root_router = Router::>::new(); - - let (graphqlc, oidcc) = try_join!( - controllers::graphql::create(context.clone()), - controllers::oidc::create(context.clone()), - )?; - - for c in [graphqlc, oidcc] { - root_router = c.apply_to(root_router); - } - - root_router = root_router.with_state(context); - - Ok(AppRouter { root: root_router }) -} diff --git a/apps/recorder/src/auth/basic.rs b/apps/recorder/src/auth/basic.rs index a0d4bce..9081a49 100644 --- a/apps/recorder/src/auth/basic.rs +++ b/apps/recorder/src/auth/basic.rs @@ -77,7 +77,7 @@ impl AuthServiceTrait for BasicAuthService { { let subscriber_auth = crate::models::auth::Model::find_by_pid(ctx, SEED_SUBSCRIBER) .await - .map_err(AuthError::FindAuthRecordError)?; + .map_err(|_| AuthError::FindAuthRecordError)?; return Ok(AuthUserInfo { subscriber_auth, auth_type: AuthType::Basic, diff --git a/apps/recorder/src/auth/errors.rs b/apps/recorder/src/auth/errors.rs index 3713663..3050577 100644 --- a/apps/recorder/src/auth/errors.rs +++ b/apps/recorder/src/auth/errors.rs @@ -13,7 +13,7 @@ use openidconnect::{ use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::{errors::RError, fetch::HttpClientError, models::auth::AuthType}; +use crate::{fetch::HttpClientError, models::auth::AuthType}; #[derive(Debug, Error)] pub enum AuthError { @@ -23,7 +23,7 @@ pub enum AuthError { current: AuthType, }, #[error("Failed to find auth record")] - FindAuthRecordError(RError), + FindAuthRecordError, #[error("Invalid credentials")] BasicInvalidCredentials, #[error(transparent)] diff --git a/apps/recorder/src/auth/oidc.rs b/apps/recorder/src/auth/oidc.rs index 5dc070d..8528b2c 100644 --- a/apps/recorder/src/auth/oidc.rs +++ b/apps/recorder/src/auth/oidc.rs @@ -311,7 +311,7 @@ impl AuthServiceTrait for OidcAuthService { } r => r, } - .map_err(AuthError::FindAuthRecordError)?; + .map_err(|_| AuthError::FindAuthRecordError)?; Ok(AuthUserInfo { subscriber_auth, diff --git a/apps/recorder/src/bin/main.rs b/apps/recorder/src/bin/main.rs index 0061a24..7df982e 100644 --- a/apps/recorder/src/bin/main.rs +++ b/apps/recorder/src/bin/main.rs @@ -1,9 +1,15 @@ -use loco_rs::cli; -use recorder::{app::App1, migrations::Migrator}; +use color_eyre::{self, eyre}; +use recorder::app::AppBuilder; #[tokio::main] -async fn main() -> color_eyre::eyre::Result<()> { +async fn main() -> eyre::Result<()> { color_eyre::install()?; - cli::main::().await?; + + let builder = AppBuilder::from_main_cli(None).await?; + + let app = builder.build().await?; + + app.serve().await?; + Ok(()) } diff --git a/apps/recorder/src/cache/config.rs b/apps/recorder/src/cache/config.rs index 5d4e3d9..dd0cb93 100644 --- a/apps/recorder/src/cache/config.rs +++ b/apps/recorder/src/cache/config.rs @@ -1 +1,4 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct CacheConfig {} diff --git a/apps/recorder/src/cache/service.rs b/apps/recorder/src/cache/service.rs index dd4bf39..717299c 100644 --- a/apps/recorder/src/cache/service.rs +++ b/apps/recorder/src/cache/service.rs @@ -1 +1,10 @@ +use super::CacheConfig; +use crate::errors::RResult; + pub struct CacheService {} + +impl CacheService { + pub async fn from_config(_config: CacheConfig) -> RResult { + Ok(Self {}) + } +} diff --git a/apps/recorder/src/controllers/mod.rs b/apps/recorder/src/controllers/mod.rs deleted file mode 100644 index 2502f51..0000000 --- a/apps/recorder/src/controllers/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod core; -pub mod graphql; -pub mod oidc; diff --git a/apps/recorder/src/database/config.rs b/apps/recorder/src/database/config.rs new file mode 100644 index 0000000..0d67a2a --- /dev/null +++ b/apps/recorder/src/database/config.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct DatabaseConfig { + pub uri: String, + pub enable_logging: bool, + pub min_connections: u32, + pub max_connections: u32, + pub connect_timeout: u64, + pub idle_timeout: u64, + pub acquire_timeout: Option, + #[serde(default)] + pub auto_migrate: bool, +} diff --git a/apps/recorder/src/database/mod.rs b/apps/recorder/src/database/mod.rs new file mode 100644 index 0000000..f7f0b1a --- /dev/null +++ b/apps/recorder/src/database/mod.rs @@ -0,0 +1,5 @@ +pub mod config; +pub mod service; + +pub use config::DatabaseConfig; +pub use service::DatabaseService; diff --git a/apps/recorder/src/database/service.rs b/apps/recorder/src/database/service.rs new file mode 100644 index 0000000..ebdde18 --- /dev/null +++ b/apps/recorder/src/database/service.rs @@ -0,0 +1,97 @@ +use std::{ops::Deref, time::Duration}; + +use sea_orm::{ + ConnectOptions, ConnectionTrait, Database, DatabaseBackend, DatabaseConnection, DbBackend, + DbErr, ExecResult, QueryResult, Statement, +}; +use sea_orm_migration::MigratorTrait; + +use super::DatabaseConfig; +use crate::{errors::RResult, migrations::Migrator}; + +pub struct DatabaseService { + connection: DatabaseConnection, +} + +impl DatabaseService { + pub async fn from_config(config: DatabaseConfig) -> RResult { + let mut opt = ConnectOptions::new(&config.uri); + opt.max_connections(config.max_connections) + .min_connections(config.min_connections) + .connect_timeout(Duration::from_millis(config.connect_timeout)) + .idle_timeout(Duration::from_millis(config.idle_timeout)) + .sqlx_logging(config.enable_logging); + + if let Some(acquire_timeout) = config.acquire_timeout { + opt.acquire_timeout(Duration::from_millis(acquire_timeout)); + } + + let db = Database::connect(opt).await?; + + if db.get_database_backend() == DatabaseBackend::Sqlite { + db.execute(Statement::from_string( + DatabaseBackend::Sqlite, + " + PRAGMA foreign_keys = ON; + PRAGMA journal_mode = WAL; + PRAGMA synchronous = NORMAL; + PRAGMA mmap_size = 134217728; + PRAGMA journal_size_limit = 67108864; + PRAGMA cache_size = 2000; + ", + )) + .await?; + } + + if config.auto_migrate { + Migrator::up(&db, None).await?; + } + + Ok(Self { connection: db }) + } +} + +impl Deref for DatabaseService { + type Target = DatabaseConnection; + + fn deref(&self) -> &Self::Target { + &self.connection + } +} + +impl AsRef for DatabaseService { + fn as_ref(&self) -> &DatabaseConnection { + &self.connection + } +} + +#[async_trait::async_trait] +impl ConnectionTrait for DatabaseService { + fn get_database_backend(&self) -> DbBackend { + self.deref().get_database_backend() + } + + async fn execute(&self, stmt: Statement) -> Result { + self.deref().execute(stmt).await + } + + async fn execute_unprepared(&self, sql: &str) -> Result { + self.deref().execute_unprepared(sql).await + } + + async fn query_one(&self, stmt: Statement) -> Result, DbErr> { + self.deref().query_one(stmt).await + } + + async fn query_all(&self, stmt: Statement) -> Result, DbErr> { + self.deref().query_all(stmt).await + } + + fn support_returning(&self) -> bool { + self.deref().support_returning() + } + + fn is_mock_connection(&self) -> bool { + self.deref().is_mock_connection() + } +} diff --git a/apps/recorder/src/errors/mod.rs b/apps/recorder/src/errors/mod.rs index 6ae0d72..a4f4bd4 100644 --- a/apps/recorder/src/errors/mod.rs +++ b/apps/recorder/src/errors/mod.rs @@ -1,11 +1,23 @@ use std::{borrow::Cow, error::Error as StdError}; +use axum::response::{IntoResponse, Response}; +use http::StatusCode; use thiserror::Error as ThisError; -use crate::fetch::HttpClientError; +use crate::{auth::AuthError, fetch::HttpClientError}; #[derive(ThisError, Debug)] pub enum RError { + #[error(transparent)] + InvalidMethodError(#[from] http::method::InvalidMethod), + #[error(transparent)] + InvalidHeaderNameError(#[from] http::header::InvalidHeaderName), + #[error(transparent)] + TracingAppenderInitError(#[from] tracing_appender::rolling::InitError), + #[error(transparent)] + GraphQLSchemaError(#[from] async_graphql::dynamic::SchemaError), + #[error(transparent)] + AuthError(#[from] AuthError), #[error(transparent)] RSSError(#[from] rss::Error), #[error(transparent)] @@ -56,6 +68,10 @@ pub enum RError { }, #[error("Model Entity {entity} not found")] ModelEntityNotFound { entity: Cow<'static, str> }, + #[error("{0}")] + CustomMessageStr(&'static str), + #[error("{0}")] + CustomMessageString(String), } impl RError { @@ -88,4 +104,13 @@ impl RError { } } +impl IntoResponse for RError { + fn into_response(self) -> Response { + match self { + Self::AuthError(auth_error) => auth_error.into_response(), + err => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(), + } + } +} + pub type RResult = Result; diff --git a/apps/recorder/src/extract/mikan/client.rs b/apps/recorder/src/extract/mikan/client.rs index 5ad5fa7..4da3dd6 100644 --- a/apps/recorder/src/extract/mikan/client.rs +++ b/apps/recorder/src/extract/mikan/client.rs @@ -4,7 +4,7 @@ use reqwest_middleware::ClientWithMiddleware; use secrecy::{ExposeSecret, SecretString}; use url::Url; -use super::AppMikanConfig; +use super::MikanConfig; use crate::{ errors::RError, fetch::{HttpClient, HttpClientTrait, client::HttpClientCookiesAuth}, @@ -29,7 +29,7 @@ pub struct MikanClient { } impl MikanClient { - pub fn new(config: AppMikanConfig) -> Result { + pub async fn from_config(config: MikanConfig) -> Result { let http_client = HttpClient::from_config(config.http_client)?; let base_url = config.base_url; Ok(Self { diff --git a/apps/recorder/src/extract/mikan/config.rs b/apps/recorder/src/extract/mikan/config.rs index fd28e1c..480956c 100644 --- a/apps/recorder/src/extract/mikan/config.rs +++ b/apps/recorder/src/extract/mikan/config.rs @@ -4,7 +4,7 @@ use url::Url; use crate::fetch::HttpClientConfig; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct AppMikanConfig { +pub struct MikanConfig { pub http_client: HttpClientConfig, pub base_url: Url, } diff --git a/apps/recorder/src/extract/mikan/mod.rs b/apps/recorder/src/extract/mikan/mod.rs index d0b2faa..f8806fd 100644 --- a/apps/recorder/src/extract/mikan/mod.rs +++ b/apps/recorder/src/extract/mikan/mod.rs @@ -5,7 +5,7 @@ pub mod rss_extract; pub mod web_extract; pub use client::{MikanAuthSecrecy, MikanClient}; -pub use config::AppMikanConfig; +pub use config::MikanConfig; pub use constants::MIKAN_BUCKET_KEY; pub use rss_extract::{ MikanBangumiAggregationRssChannel, MikanBangumiRssChannel, MikanBangumiRssLink, diff --git a/apps/recorder/src/extract/mikan/rss_extract.rs b/apps/recorder/src/extract/mikan/rss_extract.rs index e8c622a..0cf662d 100644 --- a/apps/recorder/src/extract/mikan/rss_extract.rs +++ b/apps/recorder/src/extract/mikan/rss_extract.rs @@ -354,7 +354,7 @@ mod tests { let mikan_base_url = Url::parse(&mikan_server.url())?; - let mikan_client = build_testing_mikan_client(mikan_base_url.clone())?; + let mikan_client = build_testing_mikan_client(mikan_base_url.clone()).await?; { let bangumi_rss_url = diff --git a/apps/recorder/src/extract/mikan/web_extract.rs b/apps/recorder/src/extract/mikan/web_extract.rs index 2bef21e..c0709a6 100644 --- a/apps/recorder/src/extract/mikan/web_extract.rs +++ b/apps/recorder/src/extract/mikan/web_extract.rs @@ -509,7 +509,7 @@ mod test { async fn test_extract_mikan_poster_from_src(before_each: ()) -> eyre::Result<()> { let mut mikan_server = mockito::Server::new_async().await; let mikan_base_url = Url::parse(&mikan_server.url())?; - let mikan_client = build_testing_mikan_client(mikan_base_url.clone())?; + let mikan_client = build_testing_mikan_client(mikan_base_url.clone()).await?; let bangumi_poster_url = mikan_base_url.join("/images/Bangumi/202309/5ce9fed1.jpg")?; @@ -540,7 +540,7 @@ mod test { async fn test_extract_mikan_episode(before_each: ()) -> eyre::Result<()> { let mut mikan_server = mockito::Server::new_async().await; let mikan_base_url = Url::parse(&mikan_server.url())?; - let mikan_client = build_testing_mikan_client(mikan_base_url.clone())?; + let mikan_client = build_testing_mikan_client(mikan_base_url.clone()).await?; let episode_homepage_url = mikan_base_url.join("/Home/Episode/475184dce83ea2b82902592a5ac3343f6d54b36a")?; @@ -582,7 +582,7 @@ mod test { ) -> eyre::Result<()> { let mut mikan_server = mockito::Server::new_async().await; let mikan_base_url = Url::parse(&mikan_server.url())?; - let mikan_client = build_testing_mikan_client(mikan_base_url.clone())?; + let mikan_client = build_testing_mikan_client(mikan_base_url.clone()).await?; let bangumi_homepage_url = mikan_base_url.join("/Home/Bangumi/3416#370")?; @@ -625,7 +625,7 @@ mod test { let my_bangumi_page_url = mikan_base_url.join("/Home/MyBangumi")?; - let mikan_client = build_testing_mikan_client(mikan_base_url.clone())?; + let mikan_client = build_testing_mikan_client(mikan_base_url.clone()).await?; { let my_bangumi_without_cookie_mock = mikan_server diff --git a/apps/recorder/src/graphql/mod.rs b/apps/recorder/src/graphql/mod.rs index 0792bdc..a8c497c 100644 --- a/apps/recorder/src/graphql/mod.rs +++ b/apps/recorder/src/graphql/mod.rs @@ -5,5 +5,6 @@ pub mod schema_root; pub mod service; pub mod util; +pub use config::GraphQLConfig; pub use schema_root::schema; pub use service::GraphQLService; diff --git a/apps/recorder/src/graphql/service.rs b/apps/recorder/src/graphql/service.rs index 632f608..2151bcc 100644 --- a/apps/recorder/src/graphql/service.rs +++ b/apps/recorder/src/graphql/service.rs @@ -1,7 +1,8 @@ -use async_graphql::dynamic::{Schema, SchemaError}; +use async_graphql::dynamic::Schema; use sea_orm::DatabaseConnection; use super::{config::GraphQLConfig, schema_root}; +use crate::errors::RResult; #[derive(Debug)] pub struct GraphQLService { @@ -9,7 +10,10 @@ pub struct GraphQLService { } impl GraphQLService { - pub fn new(config: GraphQLConfig, db: DatabaseConnection) -> Result { + pub async fn from_config_and_database( + config: GraphQLConfig, + db: DatabaseConnection, + ) -> RResult { let schema = schema_root::schema(db, config.depth_limit, config.complexity_limit)?; Ok(Self { schema }) } diff --git a/apps/recorder/src/lib.rs b/apps/recorder/src/lib.rs index ce0b990..6b572f7 100644 --- a/apps/recorder/src/lib.rs +++ b/apps/recorder/src/lib.rs @@ -11,11 +11,12 @@ pub mod app; pub mod auth; pub mod cache; -pub mod controllers; +pub mod database; pub mod errors; pub mod extract; pub mod fetch; pub mod graphql; +pub mod logger; pub mod migrations; pub mod models; pub mod storage; @@ -24,3 +25,4 @@ pub mod tasks; #[cfg(test)] pub mod test_utils; pub mod views; +pub mod web; diff --git a/apps/recorder/src/logger/config.rs b/apps/recorder/src/logger/config.rs new file mode 100644 index 0000000..47eef0e --- /dev/null +++ b/apps/recorder/src/logger/config.rs @@ -0,0 +1,38 @@ +use serde::{Deserialize, Serialize}; + +use super::{ + LogRotation, + core::{LogFormat, LogLevel}, +}; + +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +pub struct LoggerConfig { + pub enable: bool, + + #[serde(default)] + pub pretty_backtrace: bool, + + pub level: LogLevel, + + pub format: LogFormat, + + pub filter: Option, + + pub override_filter: Option, + + pub file_appender: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +pub struct LoggerFileAppender { + pub enable: bool, + #[serde(default)] + pub non_blocking: bool, + pub level: LogLevel, + pub format: LogFormat, + pub rotation: LogRotation, + pub dir: Option, + pub filename_prefix: Option, + pub filename_suffix: Option, + pub max_log_files: usize, +} diff --git a/apps/recorder/src/logger/core.rs b/apps/recorder/src/logger/core.rs new file mode 100644 index 0000000..419dd35 --- /dev/null +++ b/apps/recorder/src/logger/core.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; +use serde_variant::to_variant_name; + +#[derive(Debug, Default, Clone, Deserialize, Serialize)] +pub enum LogLevel { + #[serde(rename = "off")] + Off, + #[serde(rename = "trace")] + Trace, + #[serde(rename = "debug")] + Debug, + #[serde(rename = "info")] + #[default] + Info, + #[serde(rename = "warn")] + Warn, + #[serde(rename = "error")] + Error, +} + +impl std::fmt::Display for LogLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + to_variant_name(self).expect("only enum supported").fmt(f) + } +} + +#[derive(Debug, Default, Clone, Deserialize, Serialize)] +pub enum LogFormat { + #[serde(rename = "compact")] + #[default] + Compact, + #[serde(rename = "pretty")] + Pretty, + #[serde(rename = "json")] + Json, +} + +#[derive(Debug, Default, Clone, Deserialize, Serialize)] +pub enum LogRotation { + #[serde(rename = "minutely")] + Minutely, + #[serde(rename = "hourly")] + #[default] + Hourly, + #[serde(rename = "daily")] + Daily, + #[serde(rename = "never")] + Never, +} diff --git a/apps/recorder/src/logger/mod.rs b/apps/recorder/src/logger/mod.rs new file mode 100644 index 0000000..0fc5abd --- /dev/null +++ b/apps/recorder/src/logger/mod.rs @@ -0,0 +1,8 @@ +pub mod config; +pub mod core; +pub mod service; + +pub use core::{LogFormat, LogLevel, LogRotation}; + +pub use config::{LoggerConfig, LoggerFileAppender}; +pub use service::LoggerService; diff --git a/apps/recorder/src/logger/service.rs b/apps/recorder/src/logger/service.rs new file mode 100644 index 0000000..9a43335 --- /dev/null +++ b/apps/recorder/src/logger/service.rs @@ -0,0 +1,162 @@ +use std::sync::OnceLock; + +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::{ + EnvFilter, Layer, Registry, + fmt::{self, MakeWriter}, + layer::SubscriberExt, + util::SubscriberInitExt, +}; + +use super::{LogFormat, LogLevel, LogRotation, LoggerConfig}; +use crate::errors::{RError, RResult}; + +// Function to initialize the logger based on the provided configuration +const MODULE_WHITELIST: &[&str] = &["sea_orm_migration", "tower_http", "sqlx::query", "sidekiq"]; + +// Keep nonblocking file appender work guard +static NONBLOCKING_WORK_GUARD_KEEP: OnceLock = OnceLock::new(); + +pub struct LoggerService {} + +impl LoggerService { + pub fn init_layer( + make_writer: W2, + format: &LogFormat, + ansi: bool, + ) -> Box + Sync + Send> + where + W2: for<'writer> MakeWriter<'writer> + Sync + Send + 'static, + { + match format { + LogFormat::Compact => fmt::Layer::default() + .with_ansi(ansi) + .with_writer(make_writer) + .compact() + .boxed(), + LogFormat::Pretty => fmt::Layer::default() + .with_ansi(ansi) + .with_writer(make_writer) + .pretty() + .boxed(), + LogFormat::Json => fmt::Layer::default() + .with_ansi(ansi) + .with_writer(make_writer) + .json() + .boxed(), + } + } + + fn init_env_filter(override_filter: Option<&String>, level: &LogLevel) -> EnvFilter { + EnvFilter::try_from_default_env() + .or_else(|_| { + // user wanted a specific filter, don't care about our internal whitelist + // or, if no override give them the default whitelisted filter (most common) + override_filter.map_or_else( + || { + EnvFilter::try_new( + MODULE_WHITELIST + .iter() + .map(|m| format!("{m}={level}")) + .chain(std::iter::once(format!( + "{}={}", + env!("CARGO_CRATE_NAME"), + level + ))) + .collect::>() + .join(","), + ) + }, + EnvFilter::try_new, + ) + }) + .expect("logger initialization failed") + } + + pub async fn from_config(config: LoggerConfig) -> RResult { + let mut layers: Vec + Sync + Send>> = Vec::new(); + + if let Some(file_appender_config) = config.file_appender.as_ref() { + if file_appender_config.enable { + let dir = file_appender_config + .dir + .as_ref() + .map_or_else(|| "./logs".to_string(), ToString::to_string); + + let mut rolling_builder = tracing_appender::rolling::Builder::default() + .max_log_files(file_appender_config.max_log_files); + + rolling_builder = match file_appender_config.rotation { + LogRotation::Minutely => { + rolling_builder.rotation(tracing_appender::rolling::Rotation::MINUTELY) + } + LogRotation::Hourly => { + rolling_builder.rotation(tracing_appender::rolling::Rotation::HOURLY) + } + LogRotation::Daily => { + rolling_builder.rotation(tracing_appender::rolling::Rotation::DAILY) + } + LogRotation::Never => { + rolling_builder.rotation(tracing_appender::rolling::Rotation::NEVER) + } + }; + + let file_appender = rolling_builder + .filename_prefix( + file_appender_config + .filename_prefix + .as_ref() + .map_or_else(String::new, ToString::to_string), + ) + .filename_suffix( + file_appender_config + .filename_suffix + .as_ref() + .map_or_else(String::new, ToString::to_string), + ) + .build(dir)?; + + let file_appender_layer = if file_appender_config.non_blocking { + let (non_blocking_file_appender, work_guard) = + tracing_appender::non_blocking(file_appender); + NONBLOCKING_WORK_GUARD_KEEP + .set(work_guard) + .map_err(|_| RError::CustomMessageStr("cannot lock for appender"))?; + Self::init_layer( + non_blocking_file_appender, + &file_appender_config.format, + false, + ) + } else { + Self::init_layer(file_appender, &file_appender_config.format, false) + }; + layers.push(file_appender_layer); + } + } + + if config.enable { + let stdout_layer = Self::init_layer(std::io::stdout, &config.format, true); + layers.push(stdout_layer); + } + + if !layers.is_empty() { + let env_filter = Self::init_env_filter(config.override_filter.as_ref(), &config.level); + tracing_subscriber::registry() + .with(layers) + .with(env_filter) + .init(); + } + + if config.pretty_backtrace { + unsafe { + std::env::set_var("RUST_BACKTRACE", "1"); + } + tracing::warn!( + "pretty backtraces are enabled (this is great for development but has a runtime \ + cost for production. disable with `logger.pretty_backtrace` in your config yaml)" + ); + } + + Ok(Self {}) + } +} diff --git a/apps/recorder/src/models/auth.rs b/apps/recorder/src/models/auth.rs index a1124df..b343d45 100644 --- a/apps/recorder/src/models/auth.rs +++ b/apps/recorder/src/models/auth.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use sea_orm::{Set, TransactionTrait, entity::prelude::*}; +use sea_orm::{EntityTrait, Set, TransactionTrait, prelude::*}; use serde::{Deserialize, Serialize}; use super::subscribers::{self, SEED_SUBSCRIBER}; diff --git a/apps/recorder/src/routeTree.gen.ts b/apps/recorder/src/routeTree.gen.ts index 0cf6ee0..86ba191 100644 --- a/apps/recorder/src/routeTree.gen.ts +++ b/apps/recorder/src/routeTree.gen.ts @@ -10,10 +10,10 @@ // Import Routes -import { Route as rootRoute } from './controllers/__root' -import { Route as IndexImport } from './controllers/index' -import { Route as GraphqlIndexImport } from './controllers/graphql/index' -import { Route as OidcCallbackImport } from './controllers/oidc/callback' +import { Route as rootRoute } from './web/controller/__root' +import { Route as IndexImport } from './web/controller/index' +import { Route as GraphqlIndexImport } from './web/controller/graphql/index' +import { Route as OidcCallbackImport } from './web/controller/oidc/callback' // Create/Update Routes diff --git a/apps/recorder/src/storage/client.rs b/apps/recorder/src/storage/client.rs index b02e94f..d05b9d9 100644 --- a/apps/recorder/src/storage/client.rs +++ b/apps/recorder/src/storage/client.rs @@ -8,7 +8,7 @@ use url::Url; use uuid::Uuid; use super::StorageConfig; -use crate::errors::RError; +use crate::errors::{RError, RResult}; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -50,10 +50,10 @@ pub struct StorageService { } impl StorageService { - pub fn from_config(config: StorageConfig) -> Self { - Self { - data_dir: config.data_dir, - } + pub async fn from_config(config: StorageConfig) -> RResult { + Ok(Self { + data_dir: config.data_dir.to_string(), + }) } pub fn get_fs(&self) -> Fs { diff --git a/apps/recorder/src/test_utils/mikan.rs b/apps/recorder/src/test_utils/mikan.rs index 916c515..09803ba 100644 --- a/apps/recorder/src/test_utils/mikan.rs +++ b/apps/recorder/src/test_utils/mikan.rs @@ -1,17 +1,18 @@ -use color_eyre::eyre; use reqwest::IntoUrl; use crate::{ - extract::mikan::{AppMikanConfig, MikanClient}, + errors::RResult, + extract::mikan::{MikanClient, MikanConfig}, fetch::HttpClientConfig, }; -pub fn build_testing_mikan_client(base_mikan_url: impl IntoUrl) -> eyre::Result { - let mikan_client = MikanClient::new(AppMikanConfig { +pub async fn build_testing_mikan_client(base_mikan_url: impl IntoUrl) -> RResult { + let mikan_client = MikanClient::from_config(MikanConfig { http_client: HttpClientConfig { ..Default::default() }, base_url: base_mikan_url.into_url()?, - })?; + }) + .await?; Ok(mikan_client) } diff --git a/apps/recorder/src/web/config.rs b/apps/recorder/src/web/config.rs new file mode 100644 index 0000000..7ad5c06 --- /dev/null +++ b/apps/recorder/src/web/config.rs @@ -0,0 +1,30 @@ +use serde::{Deserialize, Serialize}; + +use super::middleware::MiddlewareConfig; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct WebServerConfig { + /// The address on which the server should listen on for incoming + /// connections. + #[serde(default = "default_binding")] + pub binding: String, + /// The port on which the server should listen for incoming connections. + #[serde(default = "default_port")] + pub port: u16, + /// The webserver host + pub host: String, + /// Identify via the `Server` header + pub ident: Option, + /// Middleware configurations for the server, including payload limits, + /// logging, and error handling. + #[serde(default)] + pub middlewares: MiddlewareConfig, +} + +pub fn default_binding() -> String { + "127.0.0.1".to_string() +} + +pub fn default_port() -> u16 { + 5_001 +} diff --git a/apps/recorder/src/controllers/__root.tsx b/apps/recorder/src/web/controller/__root.tsx similarity index 100% rename from apps/recorder/src/controllers/__root.tsx rename to apps/recorder/src/web/controller/__root.tsx diff --git a/apps/recorder/src/controllers/core.rs b/apps/recorder/src/web/controller/core.rs similarity index 100% rename from apps/recorder/src/controllers/core.rs rename to apps/recorder/src/web/controller/core.rs diff --git a/apps/recorder/src/controllers/graphql/index.tsx b/apps/recorder/src/web/controller/graphql/index.tsx similarity index 90% rename from apps/recorder/src/controllers/graphql/index.tsx rename to apps/recorder/src/web/controller/graphql/index.tsx index 51cdf8e..2a8f542 100644 --- a/apps/recorder/src/controllers/graphql/index.tsx +++ b/apps/recorder/src/web/controller/graphql/index.tsx @@ -2,10 +2,10 @@ import { type Fetcher, createGraphiQLFetcher } from '@graphiql/toolkit'; import { createFileRoute } from '@tanstack/react-router'; import GraphiQL from 'graphiql'; import { useMemo } from 'react'; -import { beforeLoadGuard } from '../../auth/guard'; +import { beforeLoadGuard } from '../../../auth/guard'; import 'graphiql/graphiql.css'; import { firstValueFrom } from 'rxjs'; -import { useAuth } from '../../auth/hooks'; +import { useAuth } from '../../../auth/hooks'; export const Route = createFileRoute('/graphql/')({ component: RouteComponent, diff --git a/apps/recorder/src/controllers/graphql/mod.rs b/apps/recorder/src/web/controller/graphql/mod.rs similarity index 100% rename from apps/recorder/src/controllers/graphql/mod.rs rename to apps/recorder/src/web/controller/graphql/mod.rs diff --git a/apps/recorder/src/controllers/index.tsx b/apps/recorder/src/web/controller/index.tsx similarity index 100% rename from apps/recorder/src/controllers/index.tsx rename to apps/recorder/src/web/controller/index.tsx diff --git a/apps/recorder/src/web/controller/mod.rs b/apps/recorder/src/web/controller/mod.rs new file mode 100644 index 0000000..ea4ea53 --- /dev/null +++ b/apps/recorder/src/web/controller/mod.rs @@ -0,0 +1,5 @@ +pub mod core; +pub mod graphql; +pub mod oidc; + +pub use core::{Controller, ControllerTrait, PrefixController}; diff --git a/apps/recorder/src/controllers/oidc/callback.tsx b/apps/recorder/src/web/controller/oidc/callback.tsx similarity index 93% rename from apps/recorder/src/controllers/oidc/callback.tsx rename to apps/recorder/src/web/controller/oidc/callback.tsx index aa76131..5ad6f3d 100644 --- a/apps/recorder/src/controllers/oidc/callback.tsx +++ b/apps/recorder/src/web/controller/oidc/callback.tsx @@ -1,6 +1,6 @@ import { createFileRoute, redirect } from '@tanstack/react-router'; import { EventTypes } from 'oidc-client-rx'; -import { useAuth } from '../../auth/hooks'; +import { useAuth } from '../../../auth/hooks'; export const Route = createFileRoute('/oidc/callback')({ component: RouteComponent, diff --git a/apps/recorder/src/controllers/oidc/mod.rs b/apps/recorder/src/web/controller/oidc/mod.rs similarity index 100% rename from apps/recorder/src/controllers/oidc/mod.rs rename to apps/recorder/src/web/controller/oidc/mod.rs diff --git a/apps/recorder/src/web/middleware/catch_panic.rs b/apps/recorder/src/web/middleware/catch_panic.rs new file mode 100644 index 0000000..12e1e5a --- /dev/null +++ b/apps/recorder/src/web/middleware/catch_panic.rs @@ -0,0 +1,58 @@ +//! Catch Panic Middleware for Axum +//! +//! This middleware catches panics that occur during request handling in the +//! application. When a panic occurs, it logs the error and returns an +//! internal server error response. This middleware helps ensure that the +//! application can gracefully handle unexpected errors without crashing the +//! server. +use std::sync::Arc; + +use axum::{Router, response::IntoResponse}; +use http::StatusCode; +use serde::{Deserialize, Serialize}; +use tower_http::catch_panic::CatchPanicLayer; + +use crate::{app::AppContext, errors::RResult, web::middleware::MiddlewareLayer}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct CatchPanic { + #[serde(default)] + pub enable: bool, +} + +/// Handler function for the [`CatchPanicLayer`] middleware. +/// +/// This function processes panics by extracting error messages, logging them, +/// and returning an internal server error response. +#[allow(clippy::needless_pass_by_value)] +fn handle_panic(err: Box) -> axum::response::Response { + let err = err.downcast_ref::().map_or_else( + || err.downcast_ref::<&str>().map_or("no error details", |s| s), + |s| s.as_str(), + ); + + tracing::error!(err.msg = err, "server_panic"); + + StatusCode::INTERNAL_SERVER_ERROR.into_response() +} + +impl MiddlewareLayer for CatchPanic { + /// Returns the name of the middleware + fn name(&self) -> &'static str { + "catch_panic" + } + + /// Returns whether the middleware is enabled or not + fn is_enabled(&self) -> bool { + self.enable + } + + fn config(&self) -> serde_json::Result { + serde_json::to_value(self) + } + + /// Applies the Catch Panic middleware layer to the Axum router. + fn apply(&self, app: Router>) -> RResult>> { + Ok(app.layer(CatchPanicLayer::custom(handle_panic))) + } +} diff --git a/apps/recorder/src/web/middleware/compression.rs b/apps/recorder/src/web/middleware/compression.rs new file mode 100644 index 0000000..09e2531 --- /dev/null +++ b/apps/recorder/src/web/middleware/compression.rs @@ -0,0 +1,41 @@ +//! Compression Middleware for Axum +//! +//! This middleware applies compression to HTTP responses to reduce the size of +//! the data being transmitted. This can improve performance by decreasing load +//! times and reducing bandwidth usage. The middleware configuration allows for +//! enabling or disabling compression based on the application settings. + +use std::sync::Arc; + +use axum::Router; +use serde::{Deserialize, Serialize}; +use tower_http::compression::CompressionLayer; + +use crate::{app::AppContext, errors::RResult, web::middleware::MiddlewareLayer}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Compression { + #[serde(default)] + pub enable: bool, +} + +impl MiddlewareLayer for Compression { + /// Returns the name of the middleware + fn name(&self) -> &'static str { + "compression" + } + + /// Returns whether the middleware is enabled or not + fn is_enabled(&self) -> bool { + self.enable + } + + fn config(&self) -> serde_json::Result { + serde_json::to_value(self) + } + + /// Applies the Compression middleware layer to the Axum router. + fn apply(&self, app: Router>) -> RResult>> { + Ok(app.layer(CompressionLayer::new())) + } +} diff --git a/apps/recorder/src/web/middleware/cors.rs b/apps/recorder/src/web/middleware/cors.rs new file mode 100644 index 0000000..43d81eb --- /dev/null +++ b/apps/recorder/src/web/middleware/cors.rs @@ -0,0 +1,163 @@ +//! Configurable and Flexible CORS Middleware +//! +//! This middleware enables Cross-Origin Resource Sharing (CORS) by allowing +//! configurable origins, methods, and headers in HTTP requests. It can be +//! tailored to fit various application requirements, supporting permissive CORS +//! or specific rules as defined in the middleware configuration. + +use std::{sync::Arc, time::Duration}; + +use axum::Router; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use tower_http::cors::{self, Any}; + +use crate::{app::AppContext, web::middleware::MiddlewareLayer, errors::RResult}; + +/// CORS middleware configuration +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Cors { + #[serde(default)] + pub enable: bool, + /// Allow origins + #[serde(default = "default_allow_origins")] + pub allow_origins: Vec, + /// Allow headers + #[serde(default = "default_allow_headers")] + pub allow_headers: Vec, + /// Allow methods + #[serde(default = "default_allow_methods")] + pub allow_methods: Vec, + /// Allow credentials + #[serde(default)] + pub allow_credentials: bool, + /// Max age + pub max_age: Option, + // Vary headers + #[serde(default = "default_vary_headers")] + pub vary: Vec, +} + +fn default_allow_origins() -> Vec { + vec!["*".to_string()] +} + +fn default_allow_headers() -> Vec { + vec!["*".to_string()] +} + +fn default_allow_methods() -> Vec { + vec!["*".to_string()] +} + +fn default_vary_headers() -> Vec { + vec![ + "origin".to_string(), + "access-control-request-method".to_string(), + "access-control-request-headers".to_string(), + ] +} + +impl Default for Cors { + fn default() -> Self { + serde_json::from_value(json!({})).unwrap() + } +} + +impl Cors { + /// Creates cors layer + /// + /// # Errors + /// + /// This function returns an error in the following cases: + /// + /// - If any of the provided origins in `allow_origins` cannot be parsed as + /// a valid URI, the function will return a parsing error. + /// - If any of the provided headers in `allow_headers` cannot be parsed as + /// valid HTTP headers, the function will return a parsing error. + /// - If any of the provided methods in `allow_methods` cannot be parsed as + /// valid HTTP methods, the function will return a parsing error. + /// + /// In all of these cases, the error returned will be the result of the + /// `parse` method of the corresponding type. + pub fn cors(&self) -> RResult { + let mut cors: cors::CorsLayer = cors::CorsLayer::new(); + + // testing CORS, assuming https://example.com in the allow list: + // $ curl -v --request OPTIONS 'localhost:5150/api/_ping' -H 'Origin: https://example.com' -H 'Acces + // look for '< access-control-allow-origin: https://example.com' in response. + // if it doesn't appear (test with a bogus domain), it is not allowed. + if self.allow_origins == default_allow_origins() { + cors = cors.allow_origin(Any); + } else { + let mut list = vec![]; + for origin in &self.allow_origins { + list.push(origin.parse()?); + } + if !list.is_empty() { + cors = cors.allow_origin(list); + } + } + + if self.allow_headers == default_allow_headers() { + cors = cors.allow_headers(Any); + } else { + let mut list = vec![]; + for header in &self.allow_headers { + list.push(header.parse()?); + } + if !list.is_empty() { + cors = cors.allow_headers(list); + } + } + + if self.allow_methods == default_allow_methods() { + cors = cors.allow_methods(Any); + } else { + let mut list = vec![]; + for method in &self.allow_methods { + list.push(method.parse()?); + } + if !list.is_empty() { + cors = cors.allow_methods(list); + } + } + + let mut list = vec![]; + for v in &self.vary { + list.push(v.parse()?); + } + if !list.is_empty() { + cors = cors.vary(list); + } + + if let Some(max_age) = self.max_age { + cors = cors.max_age(Duration::from_secs(max_age)); + } + + cors = cors.allow_credentials(self.allow_credentials); + + Ok(cors) + } +} + +impl MiddlewareLayer for Cors { + /// Returns the name of the middleware + fn name(&self) -> &'static str { + "cors" + } + + /// Returns whether the middleware is enabled or not + fn is_enabled(&self) -> bool { + self.enable + } + + fn config(&self) -> serde_json::Result { + serde_json::to_value(self) + } + + /// Applies the CORS middleware layer to the Axum router. + fn apply(&self, app: Router>) -> RResult>> { + Ok(app.layer(self.cors()?)) + } +} diff --git a/apps/recorder/src/web/middleware/etag.rs b/apps/recorder/src/web/middleware/etag.rs new file mode 100644 index 0000000..545a41d --- /dev/null +++ b/apps/recorder/src/web/middleware/etag.rs @@ -0,0 +1,111 @@ +//! `ETag` Middleware for Caching Requests +//! +//! This middleware implements the [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) +//! HTTP header for caching responses in Axum. `ETags` are used to validate +//! cache entries by comparing a client's stored `ETag` with the one generated +//! by the server. If the `ETags` match, a `304 Not Modified` response is sent, +//! avoiding the need to resend the full content. + +use std::{ + sync::Arc, + task::{Context, Poll}, +}; + +use axum::{ + Router, + body::Body, + extract::Request, + http::{ + StatusCode, + header::{ETAG, IF_NONE_MATCH}, + }, + response::Response, +}; +use futures_util::future::BoxFuture; +use serde::{Deserialize, Serialize}; +use tower::{Layer, Service}; + +use crate::{app::AppContext, errors::RResult, web::middleware::MiddlewareLayer}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Etag { + #[serde(default)] + pub enable: bool, +} + +impl MiddlewareLayer for Etag { + /// Returns the name of the middleware + fn name(&self) -> &'static str { + "etag" + } + + /// Returns whether the middleware is enabled or not + fn is_enabled(&self) -> bool { + self.enable + } + + fn config(&self) -> serde_json::Result { + serde_json::to_value(self) + } + + /// Applies the `ETag` middleware to the application router. + fn apply(&self, app: Router>) -> RResult>> { + Ok(app.layer(EtagLayer)) + } +} + +/// [`EtagLayer`] struct for adding `ETag` functionality as a Tower service +/// layer. +#[derive(Default, Clone)] +struct EtagLayer; + +impl Layer for EtagLayer { + type Service = EtagMiddleware; + + fn layer(&self, inner: S) -> Self::Service { + EtagMiddleware { inner } + } +} + +#[derive(Clone)] +struct EtagMiddleware { + inner: S, +} + +impl Service> for EtagMiddleware +where + S: Service + Send + 'static, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = S::Error; + // `BoxFuture` is a type alias for `Pin>` + type Future = BoxFuture<'static, Result>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, request: Request) -> Self::Future { + let ifnm = request.headers().get(IF_NONE_MATCH).cloned(); + + let future = self.inner.call(request); + + let res_fut = async move { + let response = future.await?; + let etag_from_response = response.headers().get(ETAG).cloned(); + if let Some(etag_in_request) = ifnm { + if let Some(etag_from_response) = etag_from_response { + if etag_in_request == etag_from_response { + return Ok(Response::builder() + .status(StatusCode::NOT_MODIFIED) + .body(Body::empty()) + .unwrap()); + } + } + } + Ok(response) + }; + Box::pin(res_fut) + } +} diff --git a/apps/recorder/src/web/middleware/format.rs b/apps/recorder/src/web/middleware/format.rs new file mode 100644 index 0000000..0a7c5a0 --- /dev/null +++ b/apps/recorder/src/web/middleware/format.rs @@ -0,0 +1,71 @@ +//! Detect a content type and format and responds accordingly +use axum::{ + extract::FromRequestParts, + http::{ + header::{ACCEPT, CONTENT_TYPE, HeaderMap}, + request::Parts, + }, +}; +use serde::{Deserialize, Serialize}; + +use crate::errors::RError as Error; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Format(pub RespondTo); + +#[derive(Debug, Deserialize, Serialize)] +pub enum RespondTo { + None, + Html, + Json, + Xml, + Other(String), +} + +fn detect_format(content_type: &str) -> RespondTo { + if content_type.starts_with("application/json") { + RespondTo::Json + } else if content_type.starts_with("text/html") { + RespondTo::Html + } else if content_type.starts_with("text/xml") + || content_type.starts_with("application/xml") + || content_type.starts_with("application/xhtml") + { + RespondTo::Xml + } else { + RespondTo::Other(content_type.to_string()) + } +} + +pub fn get_respond_to(headers: &HeaderMap) -> RespondTo { + #[allow(clippy::option_if_let_else)] + if let Some(content_type) = headers.get(CONTENT_TYPE).and_then(|h| h.to_str().ok()) { + detect_format(content_type) + } else if let Some(content_type) = headers.get(ACCEPT).and_then(|h| h.to_str().ok()) { + detect_format(content_type) + } else { + RespondTo::None + } +} + +impl FromRequestParts for Format +where + S: Send + Sync, +{ + type Rejection = Error; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + Ok(Self(get_respond_to(&parts.headers))) + } +} + +impl FromRequestParts for RespondTo +where + S: Send + Sync, +{ + type Rejection = Error; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + Ok(get_respond_to(&parts.headers)) + } +} diff --git a/apps/recorder/src/web/middleware/logger.rs b/apps/recorder/src/web/middleware/logger.rs new file mode 100644 index 0000000..331fa7c --- /dev/null +++ b/apps/recorder/src/web/middleware/logger.rs @@ -0,0 +1,102 @@ +//! Logger Middleware +//! +//! This middleware provides logging functionality for HTTP requests. It uses +//! `TraceLayer` to log detailed information about each request, such as the +//! HTTP method, URI, version, user agent, and an associated request ID. +//! Additionally, it integrates the application's runtime environment +//! into the log context, allowing environment-specific logging (e.g., +//! "development", "production"). + +use std::sync::Arc; + +use axum::{Router, http}; +use serde::{Deserialize, Serialize}; +use tower_http::{add_extension::AddExtensionLayer, trace::TraceLayer}; + +use crate::{ + app::{AppContext, Environment}, + errors::RResult, + web::middleware::{MiddlewareLayer, request_id::LocoRequestId}, +}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Config { + #[serde(default)] + pub enable: bool, +} + +/// [`Middleware`] struct responsible for logging HTTP requests. +#[derive(Serialize, Debug)] +pub struct Middleware { + config: Config, + environment: Environment, +} + +/// Creates a new instance of [`Middleware`] by cloning the [`Config`] +/// configuration. +#[must_use] +pub fn new(config: &Config, context: Arc) -> Middleware { + Middleware { + config: config.clone(), + environment: context.environment.clone(), + } +} + +impl MiddlewareLayer for Middleware { + /// Returns the name of the middleware + fn name(&self) -> &'static str { + "logger" + } + + /// Returns whether the middleware is enabled or not + fn is_enabled(&self) -> bool { + self.config.enable + } + + fn config(&self) -> serde_json::Result { + serde_json::to_value(self) + } + + /// Applies the logger middleware to the application router by adding layers + /// for: + /// + /// - `TraceLayer`: Logs detailed information about each HTTP request. + /// - `AddExtensionLayer`: Adds the current environment to the request + /// extensions, making it accessible to the `TraceLayer` for logging. + /// + /// The `TraceLayer` is customized with `make_span_with` to extract + /// request-specific details like method, URI, version, user agent, and + /// request ID, then create a tracing span for the request. + fn apply(&self, app: Router>) -> RResult>> { + Ok(app + .layer( + TraceLayer::new_for_http().make_span_with(|request: &http::Request<_>| { + let ext = request.extensions(); + let request_id = ext + .get::() + .map_or_else(|| "req-id-none".to_string(), |r| r.get().to_string()); + let user_agent = request + .headers() + .get(axum::http::header::USER_AGENT) + .map_or("", |h| h.to_str().unwrap_or("")); + + let env: String = request + .extensions() + .get::() + .map(|e| e.full_name().to_string()) + .unwrap_or_default(); + + tracing::error_span!( + "http-request", + "http.method" = tracing::field::display(request.method()), + "http.uri" = tracing::field::display(request.uri()), + "http.version" = tracing::field::debug(request.version()), + "http.user_agent" = tracing::field::display(user_agent), + "environment" = tracing::field::display(env), + request_id = tracing::field::display(request_id), + ) + }), + ) + .layer(AddExtensionLayer::new(self.environment.clone()))) + } +} diff --git a/apps/recorder/src/web/middleware/mod.rs b/apps/recorder/src/web/middleware/mod.rs new file mode 100644 index 0000000..02a5a43 --- /dev/null +++ b/apps/recorder/src/web/middleware/mod.rs @@ -0,0 +1,165 @@ +pub mod catch_panic; +pub mod compression; +pub mod cors; +pub mod etag; +pub mod format; +pub mod logger; +pub mod remote_ip; +pub mod request_id; +pub mod secure_headers; +pub mod timeout; + +use std::sync::Arc; + +use axum::Router; +use serde::{Deserialize, Serialize}; + +use crate::{app::AppContext, errors::RResult}; + +/// Trait representing the behavior of middleware components in the application. +/// When implementing a new middleware, make sure to go over this checklist: +/// * The name of the middleware should be an ID that is similar to the field +/// name in configuration (look at how `serde` calls it) +/// * Default value implementation should be paired with `serde` default +/// handlers and default serialization implementation. Which means deriving +/// `Default` will _not_ work. You can use `serde_json` and serialize a new +/// config from an empty value, which will cause `serde` default value +/// handlers to kick in. +/// * If you need completely blank values for configuration (for example for +/// testing), implement an `::empty() -> Self` call ad-hoc. +pub trait MiddlewareLayer { + /// Returns the name of the middleware. + /// This should match the name of the property in the containing + /// `middleware` section in configuration (as named by `serde`) + fn name(&self) -> &'static str; + + /// Returns whether the middleware is enabled or not. + /// If the middleware is switchable, take this value from a configuration + /// value + fn is_enabled(&self) -> bool { + true + } + + /// Returns middleware config. + /// + /// # Errors + /// when could not convert middleware to [`serde_json::Value`] + fn config(&self) -> serde_json::Result; + + /// Applies the middleware to the given Axum router and returns the modified + /// router. + /// + /// # Errors + /// + /// If there is an issue when adding the middleware to the router. + fn apply(&self, app: Router>) -> RResult>>; +} + +#[allow(clippy::unnecessary_lazy_evaluations)] +#[must_use] +pub fn default_middleware_stack(ctx: Arc) -> Vec> { + // Shortened reference to middlewares + let middlewares = &ctx.config.server.middlewares; + + vec![ + // CORS middleware with a default if none + Box::new(middlewares.cors.clone().unwrap_or_else(|| cors::Cors { + enable: false, + ..Default::default() + })), + // Catch Panic middleware with a default if none + Box::new( + middlewares + .catch_panic + .clone() + .unwrap_or_else(|| catch_panic::CatchPanic { enable: true }), + ), + // Etag middleware with a default if none + Box::new( + middlewares + .etag + .clone() + .unwrap_or_else(|| etag::Etag { enable: true }), + ), + // Remote IP middleware with a default if none + Box::new( + middlewares + .remote_ip + .clone() + .unwrap_or_else(|| remote_ip::RemoteIpMiddleware { + enable: false, + ..Default::default() + }), + ), + // Compression middleware with a default if none + Box::new( + middlewares + .compression + .clone() + .unwrap_or_else(|| compression::Compression { enable: false }), + ), + // Timeout Request middleware with a default if none + Box::new( + middlewares + .timeout_request + .clone() + .unwrap_or_else(|| timeout::TimeOut { + enable: false, + ..Default::default() + }), + ), + // Secure Headers middleware with a default if none + Box::new(middlewares.secure_headers.clone().unwrap_or_else(|| { + secure_headers::SecureHeader { + enable: false, + ..Default::default() + } + })), + // Logger middleware with default logger configuration + Box::new(logger::new( + &middlewares + .logger + .clone() + .unwrap_or_else(|| logger::Config { enable: true }), + ctx.clone(), + )), + // Request ID middleware with a default if none + Box::new( + middlewares + .request_id + .clone() + .unwrap_or_else(|| request_id::RequestId { enable: true }), + ), + ] +} + +/// Server middleware configuration structure. +#[derive(Default, Debug, Clone, Deserialize, Serialize)] +pub struct MiddlewareConfig { + /// Compression for the response. + pub compression: Option, + + /// Etag cache headers. + pub etag: Option, + + /// Logger and augmenting trace id with request data + pub logger: Option, + + /// Catch any code panic and log the error. + pub catch_panic: Option, + + /// Setting a global timeout for requests + pub timeout_request: Option, + + /// CORS configuration + pub cors: Option, + + /// Sets a set of secure headers + pub secure_headers: Option, + + /// Calculates a remote IP based on `X-Forwarded-For` when behind a proxy + pub remote_ip: Option, + + /// Request ID + pub request_id: Option, +} diff --git a/apps/recorder/src/web/middleware/remote_ip.rs b/apps/recorder/src/web/middleware/remote_ip.rs new file mode 100644 index 0000000..78f1ca4 --- /dev/null +++ b/apps/recorder/src/web/middleware/remote_ip.rs @@ -0,0 +1,306 @@ +//! Remote IP Middleware for inferring the client's IP address based on the +//! `X-Forwarded-For` header. +//! +//! This middleware is useful when running behind proxies or load balancers that +//! add the `X-Forwarded-For` header, which includes the original client IP +//! address. +//! +//! The middleware provides a mechanism to configure trusted proxies and extract +//! the most likely client IP from the `X-Forwarded-For` header, skipping any +//! trusted proxy IPs. +use std::{ + fmt, + iter::Iterator, + net::{IpAddr, SocketAddr}, + str::FromStr, + sync::{Arc, OnceLock}, + task::{Context, Poll}, +}; + +use axum::{ + Router, + body::Body, + extract::{ConnectInfo, FromRequestParts, Request}, + http::{header::HeaderMap, request::Parts}, + response::Response, +}; +use futures_util::future::BoxFuture; +use ipnetwork::IpNetwork; +use serde::{Deserialize, Serialize}; +use tower::{Layer, Service}; +use tracing::error; + +use crate::{ + app::AppContext, + errors::{RError, RResult}, + web::middleware::MiddlewareLayer, +}; + +static LOCAL_TRUSTED_PROXIES: OnceLock> = OnceLock::new(); + +fn get_local_trusted_proxies() -> &'static Vec { + LOCAL_TRUSTED_PROXIES.get_or_init(|| { + [ + "127.0.0.0/8", // localhost IPv4 range, per RFC-3330 + "::1", // localhost IPv6 + "fc00::/7", // private IPv6 range fc00::/7 + "10.0.0.0/8", // private IPv4 range 10.x.x.x + "172.16.0.0/12", // private IPv4 range 172.16.0.0 .. 172.31.255.255 + "192.168.0.0/16", + ] + .iter() + .map(|ip| IpNetwork::from_str(ip).unwrap()) + .collect() + }) +} + +const X_FORWARDED_FOR: &str = "X-Forwarded-For"; + +/// +/// Performs a remote ip "calculation", inferring the most likely +/// client IP from the `X-Forwarded-For` header that is used by +/// load balancers and proxies. +/// +/// WARNING +/// ======= +/// +/// LIKE ANY SUCH REMOTE IP MIDDLEWARE, IN THE WRONG ARCHITECTURE IT CAN MAKE +/// YOU VULNERABLE TO IP SPOOFING. +/// +/// This middleware assumes that there is at least one proxy sitting around and +/// setting headers with the client's remote IP address. Otherwise any client +/// can claim to have any IP address by setting the `X-Forwarded-For` header. +/// +/// DO NOT USE THIS MIDDLEWARE IF YOU DONT KNOW THAT YOU NEED IT +/// +/// -- But if you need it, it's crucial to use it (since it's the only way to +/// get the original client IP) +/// +/// This middleware is mostly implemented after the Rails `remote_ip` +/// middleware, and looking at other production Rust services with Axum, taking +/// the best of both worlds to balance performance and pragmatism. +/// +/// Similarities to the Rails `remote_ip` middleware: +/// +/// * Uses `X-Forwarded-For` +/// * Uses the same built-in trusted proxies list +/// * You can provide a list of `trusted_proxies` which will **replace** the +/// built-in trusted proxies +/// +/// Differences from the Rails `remote_ip` middleware: +/// +/// * You get an indication if the remote IP is actually resolved or is the +/// socket IP (no `X-Forwarded-For` header or could not parse) +/// * We do not not use the `Client-IP` header, or try to detect "spoofing" +/// (spoofing while doing remote IP resolution is virtually non-detectable) +/// * Order of filtering IPs from `X-Forwarded-For` is done according to [the de +/// facto spec](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#selecting_an_ip_address) +/// "Trusted proxy list" +#[derive(Default, Serialize, Deserialize, Debug, Clone)] +pub struct RemoteIpMiddleware { + #[serde(default)] + pub enable: bool, + /// A list of alternative proxy list IP ranges and/or network range (will + /// replace built-in proxy list) + pub trusted_proxies: Option>, +} + +impl MiddlewareLayer for RemoteIpMiddleware { + /// Returns the name of the middleware + fn name(&self) -> &'static str { + "remote_ip" + } + + /// Returns whether the middleware is enabled or not + fn is_enabled(&self) -> bool { + self.enable + && (self.trusted_proxies.is_none() + || self.trusted_proxies.as_ref().is_some_and(|t| !t.is_empty())) + } + + fn config(&self) -> serde_json::Result { + serde_json::to_value(self) + } + + /// Applies the Remote IP middleware to the given Axum router. + fn apply(&self, app: Router>) -> RResult>> { + Ok(app.layer(RemoteIPLayer::new(self)?)) + } +} + +// implementation reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For +fn maybe_get_forwarded( + headers: &HeaderMap, + trusted_proxies: Option<&Vec>, +) -> Option { + /* + > There may be multiple X-Forwarded-For headers present in a request. The IP addresses in these headers must be treated as a single list, + > starting with the first IP address of the first header and continuing to the last IP address of the last header. + > There are two ways of making this single list: + > join the X-Forwarded-For full header values with commas and then split by comma into a list, or + > split each X-Forwarded-For header by comma into lists and then join the lists + */ + let xffs = headers + .get_all(X_FORWARDED_FOR) + .iter() + .map(|hdr| hdr.to_str()) + .filter_map(Result::ok) + .collect::>(); + + if xffs.is_empty() { + return None; + } + + let forwarded = xffs.join(","); + + forwarded + .split(',') + .map(str::trim) + .map(str::parse) + .filter_map(Result::ok) + /* + > Trusted proxy list: The IPs or IP ranges of the trusted reverse proxies are configured. + > The X-Forwarded-For IP list is searched from the rightmost, skipping all addresses that + > are on the trusted proxy list. The first non-matching address is the target address. + */ + .filter(|ip| { + // trusted proxies provided REPLACES our default local proxies + let proxies = trusted_proxies.unwrap_or_else(|| get_local_trusted_proxies()); + !proxies + .iter() + .any(|trusted_proxy| trusted_proxy.contains(*ip)) + }) + /* + > When choosing the X-Forwarded-For client IP address closest to the client (untrustworthy + > and not for security-related purposes), the first IP from the leftmost that is a valid + > address and not private/internal should be selected. + > + NOTE: + > The first trustworthy X-Forwarded-For IP address may belong to an untrusted intermediate + > proxy rather than the actual client computer, but it is the only IP suitable for security uses. + */ + .next_back() +} + +#[derive(Copy, Clone, Debug)] +pub enum RemoteIP { + Forwarded(IpAddr), + Socket(IpAddr), + None, +} + +impl FromRequestParts for RemoteIP +where + S: Send + Sync, +{ + type Rejection = (); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let ip = parts.extensions.get::(); + Ok(*ip.unwrap_or(&Self::None)) + } +} + +impl fmt::Display for RemoteIP { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Forwarded(ip) => write!(f, "remote: {ip}"), + Self::Socket(ip) => write!(f, "socket: {ip}"), + Self::None => write!(f, "--"), + } + } +} + +#[derive(Clone, Debug)] +struct RemoteIPLayer { + trusted_proxies: Option>, +} + +impl RemoteIPLayer { + /// Returns new secure headers middleware + /// + /// # Errors + /// Fails if invalid header values found + pub fn new(config: &RemoteIpMiddleware) -> RResult { + Ok(Self { + trusted_proxies: config + .trusted_proxies + .as_ref() + .map(|proxies| { + proxies + .iter() + .map(|proxy| { + IpNetwork::from_str(proxy).map_err(|err| { + RError::CustomMessageString(format!( + "remote ip middleare cannot parse trusted proxy \ + configuration: `{proxy}`, reason: `{err}`", + )) + }) + }) + .collect::>>() + }) + .transpose()?, + }) + } +} + +impl Layer for RemoteIPLayer { + type Service = RemoteIPMiddleware; + + fn layer(&self, inner: S) -> Self::Service { + RemoteIPMiddleware { + inner, + layer: self.clone(), + } + } +} + +/// Remote IP Detection Middleware +#[derive(Clone, Debug)] +#[must_use] +pub struct RemoteIPMiddleware { + inner: S, + layer: RemoteIPLayer, +} + +impl Service> for RemoteIPMiddleware +where + S: Service, Response = Response> + Send + 'static, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = S::Error; + type Future = BoxFuture<'static, Result>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, mut req: Request) -> Self::Future { + let layer = self.layer.clone(); + let xff_ip = maybe_get_forwarded(req.headers(), layer.trusted_proxies.as_ref()); + let remote_ip = xff_ip.map_or_else( + || { + let ip = req + .extensions() + .get::>() + .map_or_else( + || { + error!( + "remote ip middleware cannot get socket IP (not set in axum \ + extensions): setting IP to `127.0.0.1`" + ); + RemoteIP::None + }, + |info| RemoteIP::Socket(info.ip()), + ); + ip + }, + RemoteIP::Forwarded, + ); + + req.extensions_mut().insert(remote_ip); + + Box::pin(self.inner.call(req)) + } +} diff --git a/apps/recorder/src/web/middleware/request_id.rs b/apps/recorder/src/web/middleware/request_id.rs new file mode 100644 index 0000000..595ee68 --- /dev/null +++ b/apps/recorder/src/web/middleware/request_id.rs @@ -0,0 +1,132 @@ +//! Middleware to generate or ensure a unique request ID for every request. +//! +//! The request ID is stored in the `x-request-id` header, and it is either +//! generated or sanitized if already present in the request. +//! +//! This can be useful for tracking requests across services, logging, and +//! debugging. + +use axum::{Router, extract::Request, http::HeaderValue, middleware::Next, response::Response}; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{web::middleware::MiddlewareLayer, app::AppContext, errors::RResult}; + +const X_REQUEST_ID: &str = "x-request-id"; +const MAX_LEN: usize = 255; + +use std::sync::{Arc, OnceLock}; + +static ID_CLEANUP: OnceLock = OnceLock::new(); + +fn get_id_cleanup() -> &'static Regex { + ID_CLEANUP.get_or_init(|| Regex::new(r"[^\w\-@]").unwrap()) +} +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RequestId { + #[serde(default)] + pub enable: bool, +} + +impl MiddlewareLayer for RequestId { + /// Returns the name of the middleware + fn name(&self) -> &'static str { + "request_id" + } + + /// Returns whether the middleware is enabled or not + fn is_enabled(&self) -> bool { + self.enable + } + + fn config(&self) -> serde_json::Result { + serde_json::to_value(self) + } + + /// Applies the request ID middleware to the Axum router. + /// + /// This function sets up the middleware in the router and ensures that + /// every request passing through it will have a unique or sanitized + /// request ID. + /// + /// # Errors + /// This function returns an error if the middleware cannot be applied. + fn apply(&self, app: Router>) -> RResult>> { + Ok(app.layer(axum::middleware::from_fn(request_id_middleware))) + } +} + +/// Wrapper struct for storing the request ID in the request's extensions. +#[derive(Debug, Clone)] +pub struct LocoRequestId(String); + +impl LocoRequestId { + /// Retrieves the request ID as a string slice. + #[must_use] + pub fn get(&self) -> &str { + self.0.as_str() + } +} + +/// Middleware function to ensure or generate a unique request ID. +/// +/// This function intercepts requests, checks for the presence of the +/// `x-request-id` header, and either sanitizes its value or generates a new +/// UUID if absent. The resulting request ID is added to both the request +/// extensions and the response headers. +pub async fn request_id_middleware(mut request: Request, next: Next) -> Response { + let header_request_id = request.headers().get(X_REQUEST_ID).cloned(); + let request_id = make_request_id(header_request_id); + request + .extensions_mut() + .insert(LocoRequestId(request_id.clone())); + let mut res = next.run(request).await; + + if let Ok(v) = HeaderValue::from_str(request_id.as_str()) { + res.headers_mut().insert(X_REQUEST_ID, v); + } else { + tracing::warn!("could not set request ID into response headers: `{request_id}`",); + } + res +} + +/// Generates or sanitizes a request ID. +fn make_request_id(maybe_request_id: Option) -> String { + maybe_request_id + .and_then(|hdr| { + // see: https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/middleware/request_id.rb#L39 + let id: Option = hdr.to_str().ok().map(|s| { + get_id_cleanup() + .replace_all(s, "") + .chars() + .take(MAX_LEN) + .collect() + }); + id.filter(|s| !s.is_empty()) + }) + .unwrap_or_else(|| Uuid::new_v4().to_string()) +} + +#[cfg(test)] +mod tests { + use axum::http::HeaderValue; + use insta::assert_debug_snapshot; + + use super::make_request_id; + + #[test] + fn create_or_fetch_request_id() { + let id = make_request_id(Some(HeaderValue::from_static("foo-bar=baz"))); + assert_debug_snapshot!(id); + let id = make_request_id(Some(HeaderValue::from_static(""))); + assert_debug_snapshot!(id.len()); + let id = make_request_id(Some(HeaderValue::from_static("=========="))); + assert_debug_snapshot!(id.len()); + let long_id = "x".repeat(1000); + let id = make_request_id(Some(HeaderValue::from_str(&long_id).unwrap())); + assert_debug_snapshot!(id.len()); + let id = make_request_id(None); + assert_debug_snapshot!(id.len()); + } +} diff --git a/apps/recorder/src/web/middleware/secure_headers.json b/apps/recorder/src/web/middleware/secure_headers.json new file mode 100644 index 0000000..beb1f23 --- /dev/null +++ b/apps/recorder/src/web/middleware/secure_headers.json @@ -0,0 +1,26 @@ +{ + "empty":{}, + "github":{ + "Content-Security-Policy": "default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'", + "Strict-Transport-Security": "max-age=631138519", + "X-Content-Type-Options": "nosniff", + "X-Download-Options": "noopen", + "X-Frame-Options": "sameorigin", + "X-Permitted-Cross-Domain-Policies": "none", + "X-Xss-Protection": "0" + }, + "owasp":{ + "Cache-Control": "no-store, max-age=0", + "Clear-Site-Data": "\"cache\",\"cookies\",\"storage\"", + "Content-Security-Policy": "default-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content", + "Cross-Origin-Embedder-Policy": "require-corp", + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Resource-Policy": "same-origin", + "Permissions-Policy": "accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(self), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), unload=()", + "Referrer-Policy": "no-referrer", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "deny", + "X-Permitted-Cross-Domain-Policies": "none" + } +} diff --git a/apps/recorder/src/web/middleware/secure_headers.rs b/apps/recorder/src/web/middleware/secure_headers.rs new file mode 100644 index 0000000..138dc27 --- /dev/null +++ b/apps/recorder/src/web/middleware/secure_headers.rs @@ -0,0 +1,311 @@ +//! Sets secure headers for your backend to promote security-by-default. +//! +//! This middleware applies secure HTTP headers, providing pre-defined presets +//! (e.g., "github") and the ability to override or define custom headers. + +use std::{ + collections::{BTreeMap, HashMap}, + sync::{Arc, OnceLock}, + task::{Context, Poll}, +}; + +use axum::{ + Router, + body::Body, + http::{HeaderName, HeaderValue, Request}, + response::Response, +}; +use futures_util::future::BoxFuture; +use serde::{Deserialize, Serialize}; +use serde_json::{self, json}; +use tower::{Layer, Service}; + +use crate::{ + app::AppContext, + web::middleware::MiddlewareLayer, + errors::{RError, RResult}, +}; + +static PRESETS: OnceLock>> = OnceLock::new(); +fn get_presets() -> &'static HashMap> { + PRESETS.get_or_init(|| { + let json_data = include_str!("secure_headers.json"); + serde_json::from_str(json_data).unwrap() + }) +} +/// Sets a predefined or custom set of secure headers. +/// +/// We recommend our `github` preset. Presets values are derived +/// from the [secure_headers](https://github.com/github/secure_headers) Ruby +/// library which Github (and originally Twitter) use. +/// +/// To use a preset, in your `config/development.yaml`: +/// +/// ```yaml +/// middlewares: +/// secure_headers: +/// preset: github +/// ``` +/// +/// You can also override individual headers on a given preset: +/// +/// ```yaml +/// middlewares: +/// secure_headers: +/// preset: github +/// overrides: +/// foo: bar +/// ``` +/// +/// Or start from scratch: +/// +///```yaml +/// middlewares: +/// secure_headers: +/// preset: empty +/// overrides: +/// one: two +/// ``` +/// +/// To support `htmx`, You can add the following override, to allow some inline +/// running of scripts: +/// +/// ```yaml +/// secure_headers: +/// preset: github +/// overrides: +/// # this allows you to use HTMX, and has unsafe-inline. Remove or consider in production +/// "Content-Security-Policy": "default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src 'unsafe-inline' 'self' https:; style-src 'self' https: 'unsafe-inline'" +/// ``` +/// +/// For the list of presets and their content look at [secure_headers.json](https://github.com/loco-rs/loco/blob/master/src/controller/middleware/secure_headers.rs) +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SecureHeader { + #[serde(default)] + pub enable: bool, + #[serde(default = "default_preset")] + pub preset: String, + #[serde(default)] + pub overrides: Option>, +} + +impl Default for SecureHeader { + fn default() -> Self { + serde_json::from_value(json!({})).unwrap() + } +} + +fn default_preset() -> String { + "github".to_string() +} + +impl MiddlewareLayer for SecureHeader { + /// Returns the name of the middleware + fn name(&self) -> &'static str { + "secure_headers" + } + + /// Returns whether the middleware is enabled or not + fn is_enabled(&self) -> bool { + self.enable + } + + fn config(&self) -> serde_json::Result { + serde_json::to_value(self) + } + + /// Applies the secure headers layer to the application router + fn apply(&self, app: Router>) -> RResult>> { + Ok(app.layer(SecureHeaders::new(self)?)) + } +} + +impl SecureHeader { + /// Converts the configuration into a list of headers. + /// + /// Applies the preset headers and any custom overrides. + fn as_headers(&self) -> RResult> { + let mut headers = vec![]; + + let preset = &self.preset; + let p = get_presets().get(preset).ok_or_else(|| { + RError::CustomMessageString(format!( + "secure_headers: a preset named `{preset}` does not exist" + )) + })?; + + Self::push_headers(&mut headers, p)?; + if let Some(overrides) = &self.overrides { + Self::push_headers(&mut headers, overrides)?; + } + Ok(headers) + } + + /// Helper function to push headers into a mutable vector. + /// + /// This function takes a map of header names and values, converting them + /// into valid HTTP headers and adding them to the provided `headers` + /// vector. + fn push_headers( + headers: &mut Vec<(HeaderName, HeaderValue)>, + hm: &BTreeMap, + ) -> RResult<()> { + for (k, v) in hm { + headers.push(( + HeaderName::from_bytes(k.clone().as_bytes())?, + HeaderValue::from_str(v.clone().as_str())?, + )); + } + Ok(()) + } +} + +/// The [`SecureHeaders`] layer which wraps around the service and injects +/// security headers +#[derive(Clone, Debug)] +pub struct SecureHeaders { + headers: Vec<(HeaderName, HeaderValue)>, +} + +impl SecureHeaders { + /// Creates a new [`SecureHeaders`] instance with the provided + /// configuration. + /// + /// # Errors + /// Returns an error if any header values are invalid. + pub fn new(config: &SecureHeader) -> RResult { + Ok(Self { + headers: config.as_headers()?, + }) + } +} + +impl Layer for SecureHeaders { + type Service = SecureHeadersMiddleware; + + /// Wraps the provided service with the secure headers middleware. + fn layer(&self, inner: S) -> Self::Service { + SecureHeadersMiddleware { + inner, + layer: self.clone(), + } + } +} + +/// The secure headers middleware +#[derive(Clone, Debug)] +#[must_use] +pub struct SecureHeadersMiddleware { + inner: S, + layer: SecureHeaders, +} + +impl Service> for SecureHeadersMiddleware +where + S: Service, Response = Response> + Send + 'static, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = S::Error; + type Future = BoxFuture<'static, Result>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, request: Request) -> Self::Future { + let layer = self.layer.clone(); + let future = self.inner.call(request); + Box::pin(async move { + let mut response: Response = future.await?; + let headers = response.headers_mut(); + for (k, v) in &layer.headers { + headers.insert(k, v.clone()); + } + Ok(response) + }) + } +} + +#[cfg(test)] +mod tests { + + use axum::{ + Router, + http::{HeaderMap, Method}, + routing::get, + }; + use insta::assert_debug_snapshot; + use tower::ServiceExt; + + use super::*; + fn normalize_headers(headers: &HeaderMap) -> BTreeMap { + headers + .iter() + .map(|(k, v)| { + let key = k.to_string(); + let value = v.to_str().unwrap_or("").to_string(); + (key, value) + }) + .collect() + } + #[tokio::test] + async fn can_set_headers() { + let config = SecureHeader { + enable: true, + preset: "github".to_string(), + overrides: None, + }; + let app = Router::new() + .route("/", get(|| async {})) + .layer(SecureHeaders::new(&config).unwrap()); + + let req = Request::builder() + .uri("/") + .method(Method::GET) + .body(Body::empty()) + .unwrap(); + let response = app.oneshot(req).await.unwrap(); + assert_debug_snapshot!(normalize_headers(response.headers())); + } + + #[tokio::test] + async fn can_override_headers() { + let mut overrides = BTreeMap::new(); + overrides.insert("X-Download-Options".to_string(), "foobar".to_string()); + overrides.insert("New-Header".to_string(), "baz".to_string()); + + let config = SecureHeader { + enable: true, + preset: "github".to_string(), + overrides: Some(overrides), + }; + let app = Router::new() + .route("/", get(|| async {})) + .layer(SecureHeaders::new(&config).unwrap()); + + let req = Request::builder() + .uri("/") + .method(Method::GET) + .body(Body::empty()) + .unwrap(); + let response = app.oneshot(req).await.unwrap(); + assert_debug_snapshot!(normalize_headers(response.headers())); + } + + #[tokio::test] + async fn default_is_github_preset() { + let config = SecureHeader::default(); + let app = Router::new() + .route("/", get(|| async {})) + .layer(SecureHeaders::new(&config).unwrap()); + + let req = Request::builder() + .uri("/") + .method(Method::GET) + .body(Body::empty()) + .unwrap(); + let response = app.oneshot(req).await.unwrap(); + assert_debug_snapshot!(normalize_headers(response.headers())); + } +} diff --git a/apps/recorder/src/web/middleware/timeout.rs b/apps/recorder/src/web/middleware/timeout.rs new file mode 100644 index 0000000..1d4f9b0 --- /dev/null +++ b/apps/recorder/src/web/middleware/timeout.rs @@ -0,0 +1,64 @@ +//! Timeout Request Middleware. +//! +//! This middleware applies a timeout to requests processed by the application. +//! The timeout duration is configurable and defined via the +//! [`TimeoutRequestMiddleware`] configuration. The middleware ensures that +//! requests do not run beyond the specified timeout period, improving the +//! overall performance and responsiveness of the application. +//! +//! If a request exceeds the specified timeout duration, the middleware will +//! return a `408 Request Timeout` status code to the client, indicating that +//! the request took too long to process. +use std::{sync::Arc, time::Duration}; + +use axum::Router; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use tower_http::timeout::TimeoutLayer; + +use crate::{app::AppContext, errors::RResult, web::middleware::MiddlewareLayer}; + +/// Timeout middleware configuration +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TimeOut { + #[serde(default)] + pub enable: bool, + // Timeout request in milliseconds + #[serde(default = "default_timeout")] + pub timeout: u64, +} + +impl Default for TimeOut { + fn default() -> Self { + serde_json::from_value(json!({})).unwrap() + } +} + +fn default_timeout() -> u64 { + 5_000 +} + +impl MiddlewareLayer for TimeOut { + /// Returns the name of the middleware. + fn name(&self) -> &'static str { + "timeout_request" + } + + /// Checks if the timeout middleware is enabled. + fn is_enabled(&self) -> bool { + self.enable + } + + fn config(&self) -> serde_json::Result { + serde_json::to_value(self) + } + + /// Applies the timeout middleware to the application router. + /// + /// This method wraps the provided [`AXRouter`] in a [`TimeoutLayer`], + /// ensuring that requests exceeding the specified timeout duration will + /// be interrupted. + fn apply(&self, app: Router>) -> RResult>> { + Ok(app.layer(TimeoutLayer::new(Duration::from_millis(self.timeout)))) + } +} diff --git a/apps/recorder/src/web/mod.rs b/apps/recorder/src/web/mod.rs new file mode 100644 index 0000000..ed1b3b8 --- /dev/null +++ b/apps/recorder/src/web/mod.rs @@ -0,0 +1,5 @@ +pub mod config; +pub mod controller; +pub mod middleware; + +pub use config::WebServerConfig; diff --git a/apps/recorder/tests/requests/subscribers.rs b/apps/recorder/tests/requests/subscribers.rs index 3ea5539..0506ddd 100644 --- a/apps/recorder/tests/requests/subscribers.rs +++ b/apps/recorder/tests/requests/subscribers.rs @@ -1,6 +1,5 @@ #![allow(unused_imports)] use insta::{assert_debug_snapshot, with_settings}; -use recorder::app::App1; use serial_test::serial; macro_rules! configure_insta { diff --git a/apps/recorder/tsr.config.json b/apps/recorder/tsr.config.json index 977f114..e428e86 100644 --- a/apps/recorder/tsr.config.json +++ b/apps/recorder/tsr.config.json @@ -1,4 +1,4 @@ { - "routesDirectory": "./src/controllers", + "routesDirectory": "./src/web/controller", "generatedRouteTree": "./src/routeTree.gen.ts" } diff --git a/bacon.toml b/bacon.toml new file mode 100644 index 0000000..a8b46e0 --- /dev/null +++ b/bacon.toml @@ -0,0 +1,116 @@ +# This is a configuration file for the bacon tool +# +# Complete help on configuration: https://dystroy.org/bacon/config/ +# +# You may check the current default at +# https://github.com/Canop/bacon/blob/main/defaults/default-bacon.toml + +default_job = "check" +env.CARGO_TERM_COLOR = "always" + +[jobs.recorder] +command = ["cargo", "recorder"] +watch = ["apps/recorder"] +need_stdout = true + +[jobs.check] +command = ["cargo", "check"] +need_stdout = false + +[jobs.check-all] +command = ["cargo", "check", "--all-targets"] +need_stdout = false + +# Run clippy on the default target +[jobs.clippy] +command = ["cargo", "clippy"] +need_stdout = false + +# Run clippy on all targets +# To disable some lints, you may change the job this way: +# [jobs.clippy-all] +# command = [ +# "cargo", "clippy", +# "--all-targets", +# "--", +# "-A", "clippy::bool_to_int_with_if", +# "-A", "clippy::collapsible_if", +# "-A", "clippy::derive_partial_eq_without_eq", +# ] +# need_stdout = false +[jobs.clippy-all] +command = ["cargo", "clippy", "--all-targets"] +need_stdout = false + +# This job lets you run +# - all tests: bacon test +# - a specific test: bacon test -- config::test_default_files +# - the tests of a package: bacon test -- -- -p config +[jobs.test] +command = ["cargo", "test"] +need_stdout = true + +[jobs.nextest] +command = [ + "cargo", "nextest", "run", + "--hide-progress-bar", "--failure-output", "final" +] +need_stdout = true +analyzer = "nextest" + +[jobs.doc] +command = ["cargo", "doc", "--no-deps"] +need_stdout = false + +# If the doc compiles, then it opens in your browser and bacon switches +# to the previous job +[jobs.doc-open] +command = ["cargo", "doc", "--no-deps", "--open"] +need_stdout = false +on_success = "back" # so that we don't open the browser at each change + +# You can run your application and have the result displayed in bacon, +# if it makes sense for this crate. +[jobs.run] +command = [ + "cargo", "run", + # put launch parameters for your program behind a `--` separator +] +need_stdout = true +allow_warnings = true +background = true + +# Run your long-running application (eg server) and have the result displayed in bacon. +# For programs that never stop (eg a server), `background` is set to false +# to have the cargo run output immediately displayed instead of waiting for +# program's end. +# 'on_change_strategy' is set to `kill_then_restart` to have your program restart +# on every change (an alternative would be to use the 'F5' key manually in bacon). +# If you often use this job, it makes sense to override the 'r' key by adding +# a binding `r = job:run-long` at the end of this file . +[jobs.run-long] +command = [ + "cargo", "run", + # put launch parameters for your program behind a `--` separator +] +need_stdout = true +allow_warnings = true +background = false +on_change_strategy = "kill_then_restart" + +# This parameterized job runs the example of your choice, as soon +# as the code compiles. +# Call it as +# bacon ex -- my-example +[jobs.ex] +command = ["cargo", "run", "--example"] +need_stdout = true +allow_warnings = true + +# You may define here keybindings that would be specific to +# a project, for example a shortcut to launch a specific job. +# Shortcuts to internal functions (scrolling, toggling, etc.) +# should go in your personal global prefs.toml file instead. +[keybindings] +# alt-m = "job:my-job" +c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target diff --git a/justfile b/justfile index 3ac3c7b..62a6305 100644 --- a/justfile +++ b/justfile @@ -2,7 +2,6 @@ set windows-shell := ["pwsh.exe", "-c"] set dotenv-load prepare-dev-recorder: - cargo install loco-cli cargo install sea-orm-cli cargo install cargo-watch @@ -13,13 +12,10 @@ dev-proxy: pnpm run --filter=proxy dev dev-recorder: - cargo watch -w apps/recorder -i '**/*.{js,css,scss,tsx,ts,jsx,html}' -x 'recorder start' + bacon recorder dev-playground: pnpm run --filter=recorder dev -down-recorder: - cargo run -p recorder --bin recorder_cli -- db down 999 --environment development - play-recorder: cargo recorder-playground