diff --git a/Cargo.lock b/Cargo.lock index b42b8e70..9f139294 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,7 +34,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cipher", "cpufeatures", ] @@ -70,7 +70,7 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "once_cell", "version_check", "zerocopy 0.7.32", @@ -177,18 +177,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - [[package]] name = "arrayvec" version = "0.7.4" @@ -268,7 +256,6 @@ dependencies = [ "brotli", "flate2", "futures-core", - "futures-io", "memchr", "pin-project-lite", "tokio", @@ -282,19 +269,10 @@ version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c2886ab563af5038f79ec016dd7b87947ed138b794e8dd64992962c9cca0411" dependencies = [ - "async-lock 3.4.2", + "async-lock", "futures-io", ] -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - [[package]] name = "async-lock" version = "3.4.2" @@ -306,27 +284,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-session" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07da4ce523b4e2ebaaf330746761df23a465b951a83d84bbce4233dabedae630" -dependencies = [ - "anyhow", - "async-lock 2.8.0", - "async-trait", - "base64 0.13.1", - "bincode", - "blake3", - "chrono", - "hmac 0.11.0", - "log", - "rand 0.8.5", - "serde", - "serde_json", - "sha2 0.9.9", -] - [[package]] name = "async-stream" version = "0.3.5" @@ -538,12 +495,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.7" @@ -576,15 +527,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -612,30 +554,6 @@ dependencies = [ "wyz", ] -[[package]] -name = "blake3" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3" -dependencies = [ - "arrayref", - "arrayvec 0.5.2", - "cc", - "cfg-if 0.1.10", - "constant_time_eq", - "crypto-mac 0.8.0", - "digest 0.9.0", -] - -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -754,12 +672,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -784,7 +696,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cipher", "cpufeatures", ] @@ -816,12 +728,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "cidr" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d18b093eba54c9aaa1e3784d4361eb2ba944cf7d0a932a830132238f483e8d8" - [[package]] name = "cipher" version = "0.4.4" @@ -993,25 +899,18 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - [[package]] name = "cookie" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ - "aes-gcm", "base64 0.22.1", "hkdf", - "hmac 0.12.1", + "hmac", "percent-encoding", "rand 0.8.5", - "sha2 0.10.9", + "sha2", "subtle", "time", "version_check", @@ -1063,7 +962,7 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -1119,26 +1018,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" -dependencies = [ - "generic-array", - "subtle", -] - -[[package]] -name = "crypto-mac" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "ctr" version = "0.9.2" @@ -1154,7 +1033,7 @@ version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "curve25519-dalek-derive", "fiat-crypto", @@ -1214,7 +1093,7 @@ version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "hashbrown 0.14.3", "lock_api", "once_cell", @@ -1289,22 +1168,13 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", "const-oid", "crypto-common", "subtle", @@ -1326,11 +1196,12 @@ name = "divviup-api" version = "0.5.0-pre.1" dependencies = [ "aes-gcm", - "async-lock 3.4.2", - "async-session", + "async-lock", + "async-trait", "axum 0.8.9", "base64 0.22.1", "console-subscriber", + "cookie", "educe", "email_address", "fastrand", @@ -1357,12 +1228,13 @@ dependencies = [ "serde", "serde_json", "serde_path_to_error", - "sha2 0.10.9", + "sha2", "subtle", "test-support", "thiserror 2.0.18", "time", "tokio", + "tokio-util", "tower 0.5.3", "tower-http", "tower-sessions", @@ -1375,19 +1247,11 @@ dependencies = [ "trillium-api", "trillium-caching-headers", "trillium-client", - "trillium-compression", - "trillium-conn-id", - "trillium-cookies", - "trillium-forwarding", "trillium-http", "trillium-logger", "trillium-macros 0.0.6", - "trillium-opentelemetry", - "trillium-prometheus", - "trillium-redirect", "trillium-router", "trillium-rustls", - "trillium-sessions", "trillium-static-compiled", "trillium-testing", "trillium-tokio", @@ -1499,7 +1363,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest 0.10.7", + "digest", "ff", "generic-array", "group", @@ -1525,7 +1389,7 @@ version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -1593,17 +1457,11 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "home", "windows-sys 0.48.0", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - [[package]] name = "event-listener" version = "4.0.3" @@ -1867,7 +1725,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" dependencies = [ "cc", - "cfg-if 1.0.0", + "cfg-if", "libc", "log", "rustversion", @@ -1892,7 +1750,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -1905,7 +1763,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "libc", "wasi 0.13.3+wasi-0.2.2", @@ -1985,7 +1843,7 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crunchy", ] @@ -2065,17 +1923,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac 0.12.1", -] - -[[package]] -name = "hmac" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" -dependencies = [ - "crypto-mac 0.11.0", - "digest 0.9.0", + "hmac", ] [[package]] @@ -2084,7 +1932,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -2105,15 +1953,15 @@ dependencies = [ "aead", "aes-gcm", "chacha20poly1305", - "digest 0.10.7", + "digest", "generic-array", "hkdf", - "hmac 0.12.1", + "hmac", "p256", "p384", "p521", "rand_core 0.6.4", - "sha2 0.10.9", + "sha2", "subtle", "x25519-dalek", "zeroize", @@ -2125,7 +1973,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fab97941666d81e06e29cc426612b03adfa5a6717284a7081ad96cf109c4cfc1" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "getrandom 0.2.16", "hpke", "num_enum", @@ -2482,7 +2330,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -2648,7 +2496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", - "cfg-if 1.0.0", + "cfg-if", "combine", "jni-sys", "log", @@ -2766,7 +2614,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "generator", "scoped-tls", "tracing", @@ -2817,8 +2665,8 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ - "cfg-if 1.0.0", - "digest 0.10.7", + "cfg-if", + "digest", ] [[package]] @@ -2900,7 +2748,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.11.0", - "cfg-if 1.0.0", + "cfg-if", "cfg_aliases 0.2.1", "libc", "memoffset", @@ -3046,7 +2894,7 @@ dependencies = [ "serde", "serde_json", "serde_path_to_error", - "sha2 0.10.9", + "sha2", "thiserror 1.0.69", "url", ] @@ -3143,12 +2991,6 @@ dependencies = [ "tonic 0.12.3", ] -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc1b6902ff63b32ef6c489e8048c5e253e2e4a803ea3ea7e783914536eb15c52" - [[package]] name = "opentelemetry_sdk" version = "0.27.1" @@ -3262,7 +3104,7 @@ version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "redox_syscall", "smallvec", @@ -3379,7 +3221,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "opaque-debug", "universal-hash", @@ -3453,7 +3295,7 @@ dependencies = [ "fiat-crypto", "fixed", "getrandom 0.2.16", - "hmac 0.12.1", + "hmac", "num-bigint", "num-integer", "num-iter", @@ -3462,7 +3304,7 @@ dependencies = [ "rand 0.8.5", "rand_core 0.6.4", "serde", - "sha2 0.10.9", + "sha2", "sha3", "subtle", "thiserror 2.0.18", @@ -3550,7 +3392,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "fnv", "lazy_static", "memchr", @@ -3899,7 +3741,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" dependencies = [ "cc", - "cfg-if 1.0.0", + "cfg-if", "getrandom 0.2.16", "libc", "untrusted 0.9.0", @@ -3962,7 +3804,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", - "digest 0.10.7", + "digest", "num-bigint-dig", "num-integer", "num-traits", @@ -3981,7 +3823,7 @@ version = "1.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06676aec5ccb8fc1da723cc8c0f9a46549f21ebb8753d3915c6c41db1e7f1dc4" dependencies = [ - "arrayvec 0.7.4", + "arrayvec", "borsh", "bytes", "num-traits", @@ -4447,22 +4289,9 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if 1.0.0", - "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", - "digest 0.9.0", - "opaque-debug", + "digest", ] [[package]] @@ -4471,9 +4300,9 @@ version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", ] [[package]] @@ -4482,7 +4311,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest 0.10.7", + "digest", "keccak", ] @@ -4538,7 +4367,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.10.7", + "digest", "rand_core 0.6.4", ] @@ -4679,7 +4508,7 @@ dependencies = [ "rustls", "serde", "serde_json", - "sha2 0.10.9", + "sha2", "smallvec", "thiserror 2.0.18", "time", @@ -4719,7 +4548,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2 0.10.9", + "sha2", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -4743,7 +4572,7 @@ dependencies = [ "bytes", "chrono", "crc", - "digest 0.10.7", + "digest", "dotenvy", "either", "futures-channel", @@ -4753,7 +4582,7 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac 0.12.1", + "hmac", "itoa", "log", "md-5", @@ -4765,7 +4594,7 @@ dependencies = [ "rust_decimal", "serde", "sha1", - "sha2 0.10.9", + "sha2", "smallvec", "sqlx-core", "stringprep", @@ -4796,7 +4625,7 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac 0.12.1", + "hmac", "home", "itoa", "log", @@ -4808,7 +4637,7 @@ dependencies = [ "rust_decimal", "serde", "serde_json", - "sha2 0.10.9", + "sha2", "smallvec", "sqlx-core", "stringprep", @@ -5063,7 +4892,7 @@ version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "once_cell", ] @@ -5185,6 +5014,8 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", + "hashbrown 0.14.3", "pin-project-lite", "tokio", "tracing", @@ -5576,50 +5407,6 @@ dependencies = [ "trillium-server-common", ] -[[package]] -name = "trillium-compression" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ba60c4bdb74196654425381255b3e1e6a0e10955a9bcbd0db474b82ad3b6c1d" -dependencies = [ - "async-compression", - "futures-lite", - "log", - "trillium", -] - -[[package]] -name = "trillium-conn-id" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969858ab1e08f59888dea5a018a523e6125991cad94a2fed1fed5b93825808f2" -dependencies = [ - "fastrand", - "trillium", -] - -[[package]] -name = "trillium-cookies" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b131f74b2f61b2cfa01b52b8abec1ee3248624b1472ab34df463a51be3891240" -dependencies = [ - "cookie", - "log", - "trillium", -] - -[[package]] -name = "trillium-forwarding" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683796aeee40649ad14ef91d90c4963ffeea3ff283edfb9b0ea750eea03c40c0" -dependencies = [ - "cidr", - "log", - "trillium", -] - [[package]] name = "trillium-http" version = "0.3.17" @@ -5679,39 +5466,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "trillium-opentelemetry" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b5e36d8e79ad6b8858715b9342b4d761d936dcd884f5680ecfb15c0076fa46a" -dependencies = [ - "opentelemetry", - "opentelemetry-semantic-conventions", - "trillium", - "trillium-macros 0.0.6", -] - -[[package]] -name = "trillium-prometheus" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37f303a99a7a00c127cff63985b16d4d78c8ec9af17102a40debe80eb19a68e1" -dependencies = [ - "prometheus", - "tracing", - "trillium", - "trillium-router", -] - -[[package]] -name = "trillium-redirect" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c361ebb0501bdf703502e25522e62903bd2961db2a4a07ac53d9870ef6cdcff" -dependencies = [ - "trillium", -] - [[package]] name = "trillium-router" version = "0.4.1" @@ -5755,18 +5509,6 @@ dependencies = [ "url", ] -[[package]] -name = "trillium-sessions" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1331abc9d84e851b555569dee4d0bf76a72117a07a40a34d5271e0c220d420c6" -dependencies = [ - "async-session", - "log", - "trillium", - "trillium-cookies", -] - [[package]] name = "trillium-static-compiled" version = "0.5.3" @@ -5799,7 +5541,7 @@ checksum = "4c6c5d4d9d6f6844131f166cbe4d779894f09f9b846945ab2024272bcd6e198c" dependencies = [ "async-channel", "async-dup", - "cfg-if 1.0.0", + "cfg-if", "dashmap", "fastrand", "futures-lite", @@ -6045,7 +5787,7 @@ version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", @@ -6071,7 +5813,7 @@ version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "wasm-bindgen", "web-sys", diff --git a/Cargo.toml b/Cargo.toml index e10aab0c..79096901 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,19 +29,21 @@ default = [] api-mocks = ["dep:trillium-testing"] integration-testing = [] # Enables a non-production axum middleware that reads an `X-Integration-Testing-User` -# header and injects the decoded [`User`] into request extensions. Used by -# `test-support` to impersonate specific users in tests; never enable in deployed -# builds. TODO(Part 9): fold into `integration-testing` when test-support is rewritten. +# header and injects the decoded [`User`] into request extensions. Strictly for +# use by the test harness (`test-support`); never enable in deployed builds. +# TODO: fold into `integration-testing` in Part 9 (test-support rewrite). test-header-injection = [] -otlp-trace = ["opentelemetry/trace", "opentelemetry-otlp", "opentelemetry_sdk/trace", "trillium-opentelemetry/trace"] +otlp-trace = ["opentelemetry/trace", "opentelemetry-otlp", "opentelemetry_sdk/trace"] [dependencies] aes-gcm = "0.10.3" +async-trait = "0.1" axum = "0.8" async-lock = "3.4.1" -async-session = "3.0.0" base64 = "0.22.1" console-subscriber = "0.5.0" +# Enables Key::derive_from in tower-sessions's re-exported cookie crate (via Cargo feature unification) +cookie = { version = "0.18", features = ["key-expansion"] } educe = "0.6.0" email_address = "0.2.9" fastrand = "2.3.0" @@ -66,6 +68,7 @@ subtle = "2.6.1" thiserror = "2.0.12" time = { version = "0.3.41", features = ["serde", "serde-well-known"] } tokio = { version = "1.47.1", features = ["full"] } +tokio-util = { version = "0.7", features = ["rt"] } tracing = "0.1.41" trillium = "0.2.20" tracing-chrome = "0.7.2" @@ -80,18 +83,11 @@ tracing-subscriber = { version = "0.3.19", features = [ trillium-api = { version = "0.2.0-rc.12", default-features = false } trillium-caching-headers = "0.2.3" trillium-client = { version = "0.6.2", features = ["json"] } -trillium-compression = "0.1.3" -trillium-conn-id = "0.2.3" -trillium-cookies = "0.4.2" -trillium-forwarding = "0.2.4" trillium-http = { version = "0.3.14", features = ["http-compat-1", "serde"] } trillium-logger = "0.4.5" trillium-macros = "0.0.6" -trillium-prometheus = "0.2.0" -trillium-redirect = "0.1.2" trillium-router = "0.4.1" trillium-rustls = "0.9.0" -trillium-sessions = "0.4.3" trillium-static-compiled = "0.5.2" trillium-testing = { version = "0.7.0", optional = true } trillium-tokio = "0.4.0" @@ -99,7 +95,6 @@ typenum = "1.18.0" url = "2.5.2" uuid = { version = "1.16.0", features = ["v4", "fast-rng", "serde"] } validator = { version = "0.20.0", features = ["derive"] } -trillium-opentelemetry = { version = "0.10.0", default-features = false, features = ["metrics"] } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } tower = { version = "0.5", features = ["util"] } tower-http = { version = "0.6", features = ["trace", "cors", "compression-full", "set-header"] } diff --git a/src/bin.rs b/src/bin.rs index 8301b285..77b43e12 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -1,20 +1,42 @@ use divviup_api::{ - trace::{install_trace_subscriber, traceconfig_handler}, - Config, DivviupApi, Queue, + handler::build_app, telemetry, trace, trace::install_trace_subscriber, Config, Queue, }; -use trillium::HttpConfig; -use trillium_http::Stopper; -use trillium_tokio::CloneCounterObserver; +use prometheus::Registry; +use std::sync::Arc; +use tokio::{ + net::TcpListener, + signal::{ + self, + unix::{signal, SignalKind}, + }, +}; +use tokio_util::sync::CancellationToken; -/// Maximum request body size: 1 MiB. This JSON API never needs bodies -/// larger than this under normal operation. -const MAX_REQUEST_BODY_SIZE: u64 = 1024 * 1024; +#[derive(Clone, Debug)] +struct MonitoringState { + registry: Registry, + trace_reload_handle: Arc, +} + +impl axum::extract::FromRef for Registry { + fn from_ref(state: &MonitoringState) -> Self { + state.registry.clone() + } +} + +impl axum::extract::FromRef for Arc { + fn from_ref(state: &MonitoringState) -> Self { + state.trace_reload_handle.clone() + } +} #[tokio::main] async fn main() { - // Choose aws-lc-rs as the default rustls crypto provider. This is what's currently enabled by - // the default Cargo feature. Specifying a default provider here prevents runtime errors if - // another dependency also enables the ring feature. + // Choose aws-lc-rs as the default rustls crypto provider. This is what's + // currently enabled by the default Cargo feature. Specifying a default + // provider here prevents runtime errors if another dependency also enables + // the ring feature. + // TODO: switch to a direct `rustls` dep when trillium-rustls is removed let _ = trillium_rustls::rustls::crypto::aws_lc_rs::default_provider().install_default(); let config = match Config::from_env() { @@ -26,31 +48,72 @@ async fn main() { }; let (_guards, trace_reload_handle) = install_trace_subscriber(&config.trace_config()).unwrap(); + let cancel = CancellationToken::new(); + + // Monitoring server (metrics + traceconfig) + let registry = telemetry::install_metrics().expect("failed to install metrics provider"); + let monitoring_state = MonitoringState { + registry, + trace_reload_handle: Arc::new(trace_reload_handle), + }; + let monitoring_router = axum::Router::new() + .route("/metrics", axum::routing::get(telemetry::metrics_handler)) + .route( + "/traceconfig", + axum::routing::get(trace::get_traceconfig).put(trace::put_traceconfig), + ) + .with_state(monitoring_state); + let monitoring_listener = TcpListener::bind(config.monitoring_listen_address) + .await + .expect("failed to bind monitoring listener"); + let monitoring_cancel = cancel.clone(); + let monitoring_handle = tokio::spawn(async move { + if let Err(e) = axum::serve(monitoring_listener, monitoring_router) + .with_graceful_shutdown(monitoring_cancel.cancelled_owned()) + .await + { + tracing::error!("monitoring server error: {e}"); + } + }); - let stopper = Stopper::new(); - let observer = CloneCounterObserver::default(); - - trillium_tokio::config() - .without_signals() - .with_socketaddr(config.monitoring_listen_address) - .with_observer(observer.clone()) - .with_stopper(stopper.clone()) - .spawn(( - divviup_api::telemetry::metrics_exporter().unwrap(), - traceconfig_handler(trace_reload_handle), - )); - - let app = DivviupApi::new(config).await; - - Queue::new(app.db(), app.config()) - .with_observer(observer.clone()) - .with_stopper(stopper.clone()) - .spawn_workers(); - - trillium_tokio::config() - .with_http_config(HttpConfig::default().with_received_body_max_len(MAX_REQUEST_BODY_SIZE)) - .with_stopper(stopper) - .with_observer(observer) - .spawn(app) + // Main application + let listen_address = config.listen_address; + let app = build_app(config).await; + + tracing::info!( + "divviup-api {} listening on {listen_address}", + env!("CARGO_PKG_VERSION") + ); + + let queue_handle = Queue::new(&app.db, &app.config, cancel.clone()).spawn_workers(); + + let listener = TcpListener::bind(listen_address) + .await + .expect("failed to bind main listener"); + + let serve_result = axum::serve(listener, app.router) + .with_graceful_shutdown(shutdown_signal(cancel.clone())) .await; + cancel.cancel(); + + if let Err(e) = serve_result { + tracing::error!("server error: {e}"); + } + + if let Err(e) = queue_handle.await { + tracing::error!("queue worker panic: {e}"); + } + + let _ = monitoring_handle.await; +} + +async fn shutdown_signal(cancel: CancellationToken) { + let ctrl_c = signal::ctrl_c(); + let mut sigterm = signal(SignalKind::terminate()).expect("failed to register SIGTERM handler"); + tokio::select! { + _ = ctrl_c => {}, + _ = sigterm.recv() => {}, + } + tracing::info!("shutdown signal received, draining connections"); + cancel.cancel(); } diff --git a/src/config.rs b/src/config.rs index 78727d00..fbb99aa2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,7 +11,7 @@ use std::{ collections::VecDeque, env::{self, VarError}, error::Error, - net::SocketAddr, + net::{IpAddr, Ipv6Addr, SocketAddr}, str::FromStr, }; use thiserror::Error; @@ -48,6 +48,8 @@ pub struct Config { pub postmark_token: String, /// The URL to postmark. pub postmark_url: Url, + /// The address to listen on for the main HTTP server. + pub listen_address: SocketAddr, /// The address to listen on for prometheus metrics and tracing configuration. pub monitoring_listen_address: SocketAddr, /// Comma-joined unpadded base64url encoded cryptographically random secrets, 32 bytes long @@ -158,9 +160,14 @@ impl Config { email_address: var("EMAIL_ADDRESS")?, postmark_token: var("POSTMARK_TOKEN")?, postmark_url: Url::parse(POSTMARK_URL).unwrap(), + listen_address: { + let host: IpAddr = var_optional("HOST", IpAddr::from(Ipv6Addr::UNSPECIFIED))?; + let port: u16 = var_optional("PORT", 8080)?; + SocketAddr::new(host, port) + }, monitoring_listen_address: var_optional( "MONITORING_LISTEN_ADDRESS", - "127.0.0.1:9464".parse().unwrap(), + SocketAddr::from((Ipv6Addr::LOCALHOST, 9464)), )?, session_secrets: var("SESSION_SECRETS")?, trace_use_test_writer: false, @@ -282,7 +289,8 @@ mod tests { email_address: "test@example.test".parse().unwrap(), postmark_token: "pmak-secret-token".into(), postmark_url: "https://postmark.example".parse().unwrap(), - monitoring_listen_address: "127.0.0.1:9464".parse().unwrap(), + listen_address: SocketAddr::from((Ipv6Addr::UNSPECIFIED, 8080)), + monitoring_listen_address: SocketAddr::from((Ipv6Addr::LOCALHOST, 9464)), session_secrets: vec![0u8; 32].into(), trace_use_test_writer: false, trace_force_json_writer: false, diff --git a/src/db.rs b/src/db.rs index 4c402786..ddc3eba0 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,8 +1,6 @@ use log::LevelFilter; use sea_orm::{ConnectOptions, ConnectionTrait, Database, DbConn}; use std::ops::{Deref, DerefMut}; -use trillium::{async_trait, Conn, Handler}; -use trillium_api::FromConn; #[derive(Clone, Debug)] pub struct Db(DbConn); @@ -15,26 +13,6 @@ impl Db { } } -impl From for Db { - fn from(value: DbConn) -> Self { - Self(value) - } -} - -#[async_trait] -impl FromConn for Db { - async fn from_conn(conn: &mut Conn) -> Option { - conn.state().cloned() - } -} - -#[async_trait] -impl Handler for Db { - async fn run(&self, conn: Conn) -> Conn { - conn.with_state(self.clone()) - } -} - impl Deref for Db { type Target = DbConn; @@ -49,7 +27,7 @@ impl DerefMut for Db { } } -#[async_trait] +#[async_trait::async_trait] impl ConnectionTrait for Db { fn get_database_backend(&self) -> sea_orm::DbBackend { self.0.get_database_backend() diff --git a/src/handler.rs b/src/handler.rs index 1f1a3720..b0e3d767 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,54 +1,45 @@ pub(crate) mod account_bearer_token; +// TODO: migrate assets to Axum in Part 9/10 (currently uses trillium-static-compiled) #[cfg(assets)] pub(crate) mod assets; pub(crate) mod cors; pub(crate) mod custom_mime_types; pub(crate) mod error; pub(crate) mod extract; -pub(crate) mod logger; pub(crate) mod oauth2; -pub(crate) mod opentelemetry; +// TODO: remove origin_router in Part 9/10 (used by assets + api_mocks) pub(crate) mod origin_router; pub(crate) mod session_store; +// TODO: remove proxy in Part 9 (only kept for DivviupApi test shim) pub(crate) mod proxy; use crate::{clients::Auth0Client, routes, routes::axum_routes, Config, Crypter, Db, FeatureFlags}; -use trillium_client::Client; - -use axum::extract::{DefaultBodyLimit, FromRef}; -use axum::http::{header, HeaderValue}; +use axum::{ + extract::{DefaultBodyLimit, FromRef}, + http::{header, HeaderValue}, +}; use cors::axum_cors_layer; use error::ErrorHandler; -use logger::logger; use oauth2::OauthClient; +// TODO: remove proxy + trillium imports in Part 9 (test-support rewrite) use proxy::AxumProxy; use session_store::axum_session_layer; -use std::{borrow::Cow, net::Ipv6Addr, net::SocketAddr, sync::Arc}; +use std::{net::Ipv6Addr, sync::Arc}; use tokio::net::TcpListener; +use tokio_util::sync::CancellationToken; use tower::ServiceBuilder; -use tower_http::compression::CompressionLayer; -use tower_http::set_header::SetResponseHeaderLayer; -use tower_http::trace::TraceLayer; +use tower_http::{ + compression::CompressionLayer, set_header::SetResponseHeaderLayer, trace::TraceLayer, +}; use trillium::{Handler, Info}; -use trillium_caching_headers::caching_headers; -use trillium_conn_id::conn_id; -use trillium_forwarding::Forwarding; +use trillium_client::Client; use trillium_macros::Handler; pub use error::Error; pub use origin_router::origin_router; -use self::opentelemetry::opentelemetry; - -#[cfg(all(assets, feature = "otlp-trace"))] -use trillium_opentelemetry::global::instrument_handler; -#[cfg(all(assets, not(feature = "otlp-trace")))] -fn instrument_handler(handler: impl Handler) -> impl Handler { - handler -} - -/// Shared state for the Axum side of the application during migration. +/// Shared state for the Axum application. #[derive(Clone, Debug)] pub struct AxumAppState { pub(crate) db: Db, @@ -102,85 +93,102 @@ impl FromRef for Client { } } +/// 1 MiB — this JSON API never needs bodies larger than this under normal operation. +const MAX_REQUEST_BODY_SIZE: usize = 1024 * 1024; + +/// The result of [`build_app`]: an Axum router ready to serve, plus the +/// shared state that callers (e.g. the queue) need. +#[derive(Debug)] +pub struct BuiltApp { + pub router: axum::Router, + pub db: Db, + pub config: Arc, +} + +/// Build the Axum application router and connect to the database. +pub async fn build_app(config: Config) -> BuiltApp { + let config = Arc::new(config); + let db = Db::connect(config.database_url.as_ref()).await; + + let auth0_client = Auth0Client::new(&config); + let axum_state = AxumAppState { + db: db.clone(), + config: config.clone(), + auth0_client, + oauth_client: OauthClient::new(&config.oauth_config()), + crypter: config.crypter.clone(), + feature_flags: config.feature_flags(), + client: config.client.clone(), + }; + + // TODO(Part 9): add OpenTelemetry HTTP metrics middleware. The deleted + // trillium-opentelemetry handler provided http.server.* histograms and + // optional OTLP per-request spans; TraceLayer only emits tracing events. + let middleware = ServiceBuilder::new() + .layer(TraceLayer::new_for_http()) + .layer(DefaultBodyLimit::max(MAX_REQUEST_BODY_SIZE)) + .layer(CompressionLayer::new()) + .layer(SetResponseHeaderLayer::if_not_present( + header::CACHE_CONTROL, + HeaderValue::from_static("private, must-revalidate"), + )) + .layer(axum_cors_layer(&config)) + .layer(axum_session_layer(db.clone(), &config.session_secrets)); + + #[cfg(feature = "integration-testing")] + let middleware = middleware.layer(axum::middleware::from_fn(inject_integration_testing_user)); + + #[cfg(feature = "test-header-injection")] + let middleware = middleware.layer(axum::middleware::from_fn(inject_test_header_user)); + + let router = axum::Router::new() + .route("/health", axum::routing::get(routes::health_check)) + .route("/login", axum::routing::get(oauth2::redirect)) + .route("/logout", axum::routing::get(oauth2::logout)) + .route("/callback", axum::routing::get(oauth2::callback)) + .nest("/api", axum_routes::api_router(&axum_state)) + .layer(middleware) + .with_state(axum_state); + + BuiltApp { router, db, config } +} + +// --------------------------------------------------------------------------- +// Test-only shim: DivviupApi +// +// test-support constructs a DivviupApi, calls .db()/.config()/.init(), and +// passes it to trillium_testing's .run_async(&app). This shim keeps that +// working by spawning the Axum router on a loopback port and proxying via +// AxumProxy. +// +// TODO: remove in Part 9 (test-support rewrite) +// --------------------------------------------------------------------------- + #[derive(Handler, Debug)] pub struct DivviupApi { #[handler(except = init)] handler: Box, db: Db, config: Arc, - axum_addr: SocketAddr, } impl DivviupApi { pub async fn init(&mut self, info: &mut Info) { *info.server_description_mut() = format!("divviup-api {}", env!("CARGO_PKG_VERSION")); - *info.listener_description_mut() = format!( - "api url: {}\n app url: {}\n axum: {}\n", - self.config.api_url, self.config.app_url, self.axum_addr, - ); self.handler.init(info).await } pub async fn new(config: Config) -> Self { - let config = Arc::new(config); - let db = Db::connect(config.database_url.as_ref()).await; - - // Spawn the Axum server on an ephemeral port. Routes will be migrated - // here incrementally. - let auth0_client = Auth0Client::new(&config); - let axum_state = AxumAppState { - db: db.clone(), - config: config.clone(), - auth0_client: auth0_client.clone(), - oauth_client: OauthClient::new(&config.oauth_config()), - crypter: config.crypter.clone(), - feature_flags: config.feature_flags(), - client: config.client.clone(), - }; - // Middleware stack in logical order (outermost first), matching the - // Trillium api() handler chain. - let middleware = ServiceBuilder::new() - .layer(TraceLayer::new_for_http()) - .layer(DefaultBodyLimit::max(1024 * 1024)) - .layer(CompressionLayer::new()) - .layer(SetResponseHeaderLayer::if_not_present( - header::CACHE_CONTROL, - HeaderValue::from_static("private, must-revalidate"), - )) - .layer(axum_cors_layer(&config)) - .layer(axum_session_layer(db.clone(), &config.session_secrets)); - - #[cfg(feature = "integration-testing")] - let middleware = - middleware.layer(axum::middleware::from_fn(inject_integration_testing_user)); - - #[cfg(feature = "test-header-injection")] - let middleware = middleware.layer(axum::middleware::from_fn(inject_test_header_user)); - - let axum_router = axum::Router::new() - // Temporary test endpoint to verify the proxy bridge works. - // TODO: Remove once enough routes have migrated to make it redundant. - .route( - "/internal/test/axum_ready", - axum::routing::get(|| async { "axum OK" }), - ) - .route("/health", axum::routing::get(routes::health_check)) - .route("/login", axum::routing::get(oauth2::redirect)) - .route("/logout", axum::routing::get(oauth2::logout)) - .route("/callback", axum::routing::get(oauth2::callback)) - .nest("/api", axum_routes::api_router(&axum_state)) - .layer(middleware) - .with_state(axum_state); + let app = build_app(config).await; + let axum_listener = TcpListener::bind((Ipv6Addr::LOCALHOST, 0)) .await .expect("failed to bind Axum listener on IPv6 loopback"); let axum_addr = axum_listener .local_addr() .expect("failed to get Axum listener address"); - // TODO: Wire graceful shutdown into axum::serve(...).with_graceful_shutdown() - // so that in-flight requests are drained when the Trillium server stops. tokio::spawn(async move { - if let Err(e) = axum::serve(axum_listener, axum_router).await { + if let Err(e) = axum::serve(axum_listener, app.router).await { log::error!("axum server error: {e}"); } }); @@ -189,21 +197,13 @@ impl DivviupApi { Self { handler: Box::new(( - conn_id(), - Forwarding::trust_always(), - opentelemetry(), - caching_headers(), - logger(), - #[cfg(assets)] - instrument_handler(assets::static_assets(&config)), #[cfg(feature = "test-header-injection")] inject_test_user_trillium, proxy, ErrorHandler, )), - db, - config, - axum_addr, + db: app.db, + config: app.config, } } @@ -218,10 +218,15 @@ impl DivviupApi { pub fn crypter(&self) -> &crate::Crypter { &self.config.crypter } +} - #[expect(dead_code)] // Scaffolded for later migration parts. - pub(crate) fn axum_addr(&self) -> SocketAddr { - self.axum_addr +// TODO: remove in Part 9 (test-support rewrite) +// NOTE: the CancellationToken created here is never cancelled, so workers +// spawned from this Queue would run forever. This is fine because callers +// only use perform_one_queue_job(), never spawn_workers(). +impl From<&DivviupApi> for crate::Queue { + fn from(app: &DivviupApi) -> Self { + Self::new(app.db(), app.config(), CancellationToken::new()) } } @@ -234,7 +239,7 @@ impl AsRef for DivviupApi { /// Trillium-side test shim: if a `User` was injected via `.with_state()` /// (legacy test pattern), serialize it into the `X-Integration-Testing-User` /// header so the proxy forwards it to Axum. -// TODO: remove in Part 8 (Trillium removal) +// TODO: remove in Part 9 (test-support rewrite — tests will set the header directly) #[cfg(feature = "test-header-injection")] async fn inject_test_user_trillium(mut conn: trillium::Conn) -> trillium::Conn { if let Some(json) = conn @@ -285,15 +290,3 @@ async fn inject_test_header_user( } next.run(request).await } - -#[derive(Handler, Debug, Clone)] -pub struct NamedHandler(#[handler(except = name)] H, Cow<'static, str>); -impl NamedHandler { - fn name(&self) -> Cow<'static, str> { - self.1.clone() - } - - pub fn new(name: impl Into>, handler: H) -> Self { - Self(handler, name.into()) - } -} diff --git a/src/handler/account_bearer_token.rs b/src/handler/account_bearer_token.rs index 2f5417d3..3ff738b9 100644 --- a/src/handler/account_bearer_token.rs +++ b/src/handler/account_bearer_token.rs @@ -5,8 +5,6 @@ use crate::{ use axum::extract::FromRef; use axum::http::{header, request::Parts}; use sea_orm::ActiveModelTrait; -use trillium::{Conn, KnownHeaderName}; -use trillium_api::FromConn; #[derive(Clone, Debug)] pub struct AccountBearerToken { @@ -14,26 +12,6 @@ pub struct AccountBearerToken { pub api_token: ApiToken, } -#[trillium::async_trait] -impl FromConn for AccountBearerToken { - async fn from_conn(conn: &mut Conn) -> Option { - if let Some(token) = conn.state::() { - return Some(token.clone()); - } - - let db: &Db = conn.state()?; - let token = conn - .request_headers() - .get_str(KnownHeaderName::Authorization)? - .strip_prefix("Bearer ")?; - let (api_token, account) = ApiTokens::load_and_check(token, db).await?; - let api_token = api_token.mark_last_used().update(db).await.ok()?; - let account_bearer_token = Self { account, api_token }; - conn.insert_state(account_bearer_token.clone()); - Some(account_bearer_token) - } -} - impl AccountBearerToken { /// Try to extract a bearer token from the request, returning `None` on /// any failure (missing header, invalid token, DB miss). This is diff --git a/src/handler/assets.rs b/src/handler/assets.rs index 3955d56c..b0b4689b 100644 --- a/src/handler/assets.rs +++ b/src/handler/assets.rs @@ -1,3 +1,4 @@ +// TODO: migrate to Axum in Part 9/10 (uses trillium-static-compiled + OriginRouter) use crate::{handler::origin_router, Config}; use std::time::Duration; use trillium::{ diff --git a/src/handler/cors.rs b/src/handler/cors.rs index ee1f78a4..951c67c2 100644 --- a/src/handler/cors.rs +++ b/src/handler/cors.rs @@ -2,64 +2,8 @@ use crate::Config; use axum::http::{header, HeaderValue, Method as HttpMethod}; use time::Duration; use tower_http::cors::CorsLayer; -use trillium::{ - Conn, Handler, - KnownHeaderName::{ - AccessControlAllowCredentials, AccessControlAllowHeaders, AccessControlAllowMethods, - AccessControlAllowOrigin, AccessControlMaxAge, Origin, - }, - Method, Status, -}; -#[derive(Debug)] -struct CorsHeaders { - origin: String, -} - -#[trillium::async_trait] -impl Handler for CorsHeaders { - async fn run(&self, mut conn: Conn) -> Conn { - let conn_origin = conn.request_headers().get_str(Origin); - - if conn_origin == Some(&self.origin) { - conn.response_headers_mut().extend([ - ( - AccessControlAllowMethods, - "POST, DELETE, OPTIONS, GET, PATCH", - ), - (AccessControlAllowCredentials, "true"), - ( - AccessControlAllowHeaders, - "Content-Type, If-None-Match, If-Modified-Since, Etag", - ), - (AccessControlMaxAge, "86400"), - ]); - conn.response_headers_mut() - .insert(AccessControlAllowOrigin, self.origin.clone()); - - if conn.method() == Method::Options { - return conn.with_status(Status::NoContent).halt(); - } - } - conn - } -} - -impl CorsHeaders { - pub fn new(config: &Config) -> Self { - let mut origin = config.app_url.to_string(); - origin.pop(); - Self { origin } - } -} - -#[expect(dead_code)] // TODO: remove in Part 8 (Trillium removal) -pub fn cors_headers(config: &Config) -> impl Handler { - CorsHeaders::new(config) -} - -/// Build a [`tower_http::cors::CorsLayer`] matching the Trillium [`CorsHeaders`] -/// behavior above. +/// Build a [`tower_http::cors::CorsLayer`] for the Axum router. pub fn axum_cors_layer(config: &Config) -> CorsLayer { let origin = config .app_url diff --git a/src/handler/custom_mime_types.rs b/src/handler/custom_mime_types.rs index a144e338..26b1e669 100644 --- a/src/handler/custom_mime_types.rs +++ b/src/handler/custom_mime_types.rs @@ -5,51 +5,14 @@ use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; use tower::{Layer, Service}; -use trillium::{ - Conn, Handler, - KnownHeaderName::{Accept, ContentType}, - Status::{NotAcceptable, UnsupportedMediaType}, -}; pub const DIVVIUP_API_MEDIA_TYPE: &str = "application/vnd.divviup+json;version=0.1"; const APPLICATION_JSON: header::HeaderValue = header::HeaderValue::from_static("application/json"); -#[expect(dead_code)] // TODO: remove in Part 8 (Trillium removal) -pub struct ReplaceMimeTypes; - -#[trillium::async_trait] -impl Handler for ReplaceMimeTypes { - async fn run(&self, mut conn: Conn) -> Conn { - let request_headers = conn.inner_mut().request_headers_mut(); - if let Some(DIVVIUP_API_MEDIA_TYPE) | None = request_headers.get_str(ContentType) { - request_headers.insert(ContentType, "application/json"); - } else { - return conn.with_status(UnsupportedMediaType).halt(); - } - - if Some(DIVVIUP_API_MEDIA_TYPE) == request_headers.get_str(Accept) { - request_headers.insert(Accept, "application/json"); - } else { - return conn.with_status(NotAcceptable).halt(); - } - - conn - } - - async fn before_send(&self, conn: Conn) -> Conn { - conn.with_response_header(ContentType, DIVVIUP_API_MEDIA_TYPE) - } -} - -// --------------------------------------------------------------------------- -// Axum / Tower equivalent of ReplaceMimeTypes -// --------------------------------------------------------------------------- - /// Tower [`Layer`] that applies [`ReplaceMimeTypesService`] to an inner service. /// -/// This replicates the Trillium [`ReplaceMimeTypes`] handler for Axum routes: -/// requests with the custom content type (or no content type) have their headers -/// normalized to `application/json`; responses get the custom content type set. +/// This normalizes the custom divviup content type to `application/json` on +/// requests, and sets the custom content type on responses. #[derive(Clone, Debug)] pub struct ReplaceMimeTypesLayer; diff --git a/src/handler/error.rs b/src/handler/error.rs index a2d2a80f..8dad63c5 100644 --- a/src/handler/error.rs +++ b/src/handler/error.rs @@ -4,54 +4,21 @@ use axum::response::{IntoResponse, Response}; use axum::Json as AxumJson; use sea_orm::DbErr; use serde_json::json; -use std::{backtrace::Backtrace, sync::Arc}; -use trillium::{async_trait, Conn, Handler, Status}; -use trillium_api::{ApiConnExt, Error as ApiError}; +use std::sync::Arc; +use trillium_api::Error as ApiError; use validator::ValidationErrors; +// TODO: remove in Part 9 (test-support rewrite) — ErrorHandler is only kept +// in the DivviupApi test shim's handler tuple. pub struct ErrorHandler; -#[async_trait] -impl Handler for ErrorHandler { - async fn run(&self, conn: Conn) -> Conn { +#[trillium::async_trait] +impl trillium::Handler for ErrorHandler { + async fn run(&self, conn: trillium::Conn) -> trillium::Conn { conn } - async fn before_send(&self, mut conn: Conn) -> Conn { - if let Some(error) = conn.take_state::() { - conn.insert_state(Error::from(error)); - }; - - let Some(error) = conn.state().cloned() else { - return conn; - }; - - match error { - Error::AccessDenied => conn.with_status(Status::Forbidden).with_body(""), - - Error::NotFound => conn.with_status(Status::NotFound).with_body(""), - - Error::Json(e @ ApiError::UnsupportedMimeType { .. }) => conn - .with_status(Status::NotAcceptable) - .with_body(e.to_string()), - - Error::Json(ApiError::ParseError { path, message }) => conn - .with_status(Status::BadRequest) - .with_json(&json!({"path": path, "message": message})), - - Error::Validation(e) => conn.with_status(Status::BadRequest).with_json(&e), - - e => { - let string = e.to_string(); - log::error!("{e}"); - let mut conn = conn.with_status(Status::InternalServerError); - if cfg!(debug_assertions) { - conn.with_body(string) - } else { - conn.inner_mut().take_response_body(); - conn - } - } - } + async fn before_send(&self, conn: trillium::Conn) -> trillium::Conn { + conn } } @@ -142,16 +109,7 @@ impl From for Error { } } -#[async_trait] -impl Handler for Error { - async fn run(&self, conn: Conn) -> Conn { - conn.with_state(self.clone()) - .with_state(Backtrace::capture()) - } -} - -/// Axum-side error-to-response conversion, mirroring the Trillium -/// [`ErrorHandler::before_send`] logic above. +/// Error-to-response conversion for Axum handlers. impl IntoResponse for Error { fn into_response(self) -> Response { match self { diff --git a/src/handler/extract.rs b/src/handler/extract.rs index 847b64d6..4b0c44de 100644 --- a/src/handler/extract.rs +++ b/src/handler/extract.rs @@ -98,6 +98,6 @@ where .ok_or(Error::AccessDenied)?; actor - .if_allowed_http(&parts.method, entity) + .if_allowed(&parts.method, entity) .ok_or(Error::AccessDenied) } diff --git a/src/handler/logger.rs b/src/handler/logger.rs deleted file mode 100644 index 0ed419bd..00000000 --- a/src/handler/logger.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::user::User; -use std::borrow::Cow; -use trillium::{Conn, Handler, KnownHeaderName}; -use trillium_logger::{dev_formatter, formatters::request_header}; - -fn redirect_url(conn: &Conn, _color: bool) -> Cow<'static, str> { - match conn - .inner() - .response_headers() - .get(KnownHeaderName::Location) - { - Some(h) => format!(" -> {h}").into(), - None => "".into(), - } -} - -fn user(conn: &Conn, _color: bool) -> String { - match conn.state::() { - Some(User { - email, - admin: Some(true), - .. - }) => format!("{email} (admin)"), - Some(User { email, .. }) => String::from(email), - None => String::from("-"), - } -} - -pub fn logger() -> impl Handler { - trillium_logger::logger().with_formatter(( - request_header(KnownHeaderName::Host), - " ", - user, - ": ", - dev_formatter, - redirect_url, - )) -} diff --git a/src/handler/opentelemetry.rs b/src/handler/opentelemetry.rs deleted file mode 100644 index 5a8cebc2..00000000 --- a/src/handler/opentelemetry.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::Error; -use std::borrow::Cow; -use trillium_router::RouterConnExt; - -fn error_type(value: &Error) -> Option> { - match value { - Error::Database(_) => Some("DbError"), - Error::Client(_) => Some("ClientError"), - Error::Other(_) => Some("Other"), - Error::NumericConversion(_) => None, - Error::TimeComponentRange(_) => Some("TimeComponentRangeError"), - Error::TaskProvisioning(_) => Some("TaskProvisioningError"), - Error::Encryption => Some("EncryptionError"), - Error::String(s) => Some(*s), - Error::Codec(_) => Some("CodecError"), - _ => None, - } - .map(Cow::Borrowed) -} - -#[cfg(feature = "otlp-trace")] -pub fn opentelemetry() -> impl trillium::Handler { - trillium_opentelemetry::global::instrument() - .with_route(|conn| conn.route().map(|r| r.to_string().into())) - .with_error_type(|conn| conn.state().and_then(error_type)) - .with_headers([trillium::KnownHeaderName::XrequestId]) -} - -#[cfg(not(feature = "otlp-trace"))] -pub fn opentelemetry() -> impl trillium::Handler { - trillium_opentelemetry::global::metrics() - .with_route(|conn| conn.route().map(|r| r.to_string().into())) - .with_error_type(|conn| conn.state().and_then(error_type)) -} diff --git a/src/handler/origin_router.rs b/src/handler/origin_router.rs index b3993de8..45452504 100644 --- a/src/handler/origin_router.rs +++ b/src/handler/origin_router.rs @@ -1,3 +1,4 @@ +// TODO: remove in Part 9/10 (kept for assets + api_mocks) use std::collections::HashMap; use trillium::{Conn, Handler, Info}; diff --git a/src/handler/proxy.rs b/src/handler/proxy.rs index 76f5039d..63c487c7 100644 --- a/src/handler/proxy.rs +++ b/src/handler/proxy.rs @@ -57,6 +57,12 @@ impl Handler for AxumProxy { let mut builder = self.client.request(reqwest_method, &url); + // Preserve the original Host so Axum middleware (e.g. asset serving) + // can distinguish virtual hosts behind this proxy. + if let Some(host) = conn.request_headers().get_str("host") { + builder = builder.header("x-forwarded-host", host); + } + // Forward request headers, filtering out hop-by-hop headers. for (name, values) in conn.request_headers() { let header_name = match HeaderName::from_bytes(name.as_ref().as_bytes()) { diff --git a/src/handler/session_store.rs b/src/handler/session_store.rs index 341e7690..6a2d96a1 100644 --- a/src/handler/session_store.rs +++ b/src/handler/session_store.rs @@ -1,14 +1,9 @@ use crate::{config::SessionSecrets, entity::session, Db}; -use async_session::{ - async_trait, - chrono::{DateTime, Utc}, - Session, -}; +use async_trait::async_trait; use sea_orm::{ sea_query::{any, OnConflict}, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, }; -use serde_json::json; use std::collections::HashMap; use time::{Duration, OffsetDateTime}; use tower_sessions::{ @@ -18,114 +13,14 @@ use tower_sessions::{ session_store as tower_store, Expiry, SessionManagerLayer, }; -/// Cookie name shared with the Trillium-side session middleware. Both sides -/// set a cookie with this name; the wire formats are incompatible, so each -/// side only successfully parses its own. -// TODO: remove this comment when Trillium is removed — the incompatibility -// note will no longer apply. +/// Cookie name used by the session middleware. pub const SESSION_COOKIE_NAME: &str = "divviup.sid"; -#[allow(dead_code)] // TODO: remove in Part 8 (Trillium removal) -#[derive(Debug, Clone)] -pub struct SessionStore { - db: Db, -} - -#[allow(dead_code)] // TODO: remove in Part 8 (Trillium removal) -impl SessionStore { - pub fn new(db: Db) -> Self { - Self { db } - } -} - -impl TryFrom<&Session> for session::Model { - type Error = serde_json::Error; - - fn try_from(session: &Session) -> Result { - Ok(Self { - id: session.id().to_string(), - // unwrap safety: session object comes from the session handler, and its timestamp - // we made ourselves. - expiry: session - .expiry() - .map(|e| OffsetDateTime::from_unix_timestamp(e.timestamp()).unwrap()), - // unwrap safety: if the serialization is successful, the data element - // will be there. - data: serde_json::from_value( - serde_json::to_value(session)?.get("data").unwrap().clone(), - )?, - }) - } -} - -impl TryFrom for Session { - type Error = serde_json::Error; - fn try_from(db_session: session::Model) -> Result { - let mut session: Session = serde_json::from_value(json!({ - "id": db_session.id, - "data": db_session.data, - }))?; - if let Some(x) = db_session.expiry { - // unwrap safety: the expiry time from the database is a timestamp we made - // ourselves. - session.set_expiry( - DateTime::::from_timestamp(x.unix_timestamp(), x.nanosecond()).unwrap(), - ); - } - Ok(session) - } -} - -#[async_trait] -impl async_session::SessionStore for SessionStore { - async fn load_session(&self, cookie_value: String) -> async_session::Result> { - let id = Session::id_from_cookie_value(&cookie_value)?; - Ok(session::Entity::find_by_id(id) - .filter(any![ - session::Column::Expiry.is_null(), - session::Column::Expiry.gt(OffsetDateTime::now_utc()) - ]) - .one(&self.db) - .await? - .map(Session::try_from) - .transpose()?) - } - - async fn store_session(&self, session: Session) -> async_session::Result> { - let session_model = session::Model::try_from(&session)?.into_active_model(); - - session::Entity::insert(session_model) - .on_conflict( - OnConflict::column(session::Column::Id) - .update_columns([session::Column::Data, session::Column::Expiry]) - .clone(), - ) - .exec(&self.db) - .await?; - - Ok(session.into_cookie_value()) - } - - async fn destroy_session(&self, session: Session) -> async_session::Result { - session::Entity::delete_by_id(session.id()) - .exec(&self.db) - .await?; - Ok(()) - } - - async fn clear_store(&self) -> async_session::Result { - session::Entity::delete_many().exec(&self.db).await?; - Ok(()) - } -} - -/// Axum-side session store implementing [`tower_sessions_core::SessionStore`]. +/// Database-backed session store for [`tower-sessions`]. /// -/// Uses the same `session` database table as the Trillium-side [`SessionStore`], -/// but with a different session ID format (`tower-sessions` uses base64-encoded -/// `i128` rather than `async_session`'s UUID strings). -// TODO: remove the Trillium-side `SessionStore` and the `async_session` -// dependency when Trillium is removed. +/// Sessions are stored in the `sessions` table. Expired sessions are +/// cleaned up by the [`SessionCleanup`](crate::queue::SessionCleanup) +/// queue job rather than by the store itself. #[derive(Debug, Clone)] pub struct TowerSessionStore { db: Db, @@ -140,8 +35,8 @@ impl TowerSessionStore { /// Build the Axum-side [`SessionManagerLayer`]. /// /// `tower-sessions` 0.15 accepts only a single signing key; there is no -/// `with_older_secrets` equivalent. Key rotation for the Axum cookie will be -/// revisited once the Trillium server is removed in Part 8. +/// `with_older_secrets` equivalent. +/// TODO: revisit key rotation support in Part 9 (test-support rewrite). pub fn axum_session_layer( db: Db, secrets: &SessionSecrets, diff --git a/src/lib.rs b/src/lib.rs index 3891dc94..06dbf9b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,9 @@ mod user; pub use config::{Config, ConfigError, FeatureFlags}; pub use crypter::Crypter; pub use db::Db; -pub use handler::{custom_mime_types::DIVVIUP_API_MEDIA_TYPE, AxumAppState, DivviupApi, Error}; +pub use handler::{ + build_app, custom_mime_types::DIVVIUP_API_MEDIA_TYPE, AxumAppState, BuiltApp, DivviupApi, Error, +}; pub use opentelemetry; pub use permissions::{AdminPermissionsActor, Permissions, PermissionsActor}; pub use queue::Queue; diff --git a/src/permissions.rs b/src/permissions.rs index 00955a6b..13ed783b 100644 --- a/src/permissions.rs +++ b/src/permissions.rs @@ -5,8 +5,6 @@ use crate::{ }; use axum::extract::{FromRef, FromRequestParts}; use axum::http::{self, request::Parts}; -use trillium::Conn; -use trillium_api::FromConn; #[derive(Debug, Clone)] pub enum PermissionsActor { @@ -30,11 +28,11 @@ impl PermissionsActor { } } - pub fn is_allowed(&self, method: trillium::Method, t: &T) -> bool { + pub fn is_allowed(&self, method: &http::Method, t: &T) -> bool { self.check_permission(method.is_safe(), t) } - pub fn if_allowed(&self, method: trillium::Method, t: T) -> Option { + pub fn if_allowed(&self, method: &http::Method, t: T) -> Option { if self.is_allowed(method, &t) { Some(t) } else { @@ -42,20 +40,6 @@ impl PermissionsActor { } } - /// Axum-side equivalent of [`is_allowed`](Self::is_allowed). - pub fn is_allowed_http(&self, method: &http::Method, t: &T) -> bool { - self.check_permission(method.is_safe(), t) - } - - /// Axum-side equivalent of [`if_allowed`](Self::if_allowed). - pub fn if_allowed_http(&self, method: &http::Method, t: T) -> Option { - if self.is_allowed_http(method, &t) { - Some(t) - } else { - None - } - } - pub fn is_user(&self) -> bool { matches!(self, Self::User(_, _)) } @@ -74,36 +58,6 @@ impl PermissionsActor { } } -#[trillium::async_trait] -impl FromConn for PermissionsActor { - async fn from_conn(conn: &mut Conn) -> Option { - if let Some(actor) = conn.state::() { - return Some(actor.clone()); - } - let abt = AccountBearerToken::from_conn(conn).await; - let user = User::from_conn(conn).await; - let actor = match (abt, user) { - (Some(abt), None) => Some(Self::ApiToken(abt)), - (None, Some(user)) => { - let db: &Db = conn.state()?; - let memberships = user.memberships().all(db).await.ok()?; - Some(Self::User(user, memberships)) - } - _ => None, - }; - - if let Some(actor) = &actor { - conn.insert_state(actor.clone()); - } - - actor - } -} - -// --------------------------------------------------------------------------- -// Axum extractor — mirrors the Trillium FromConn above -// --------------------------------------------------------------------------- - impl FromRequestParts for PermissionsActor where Db: FromRef, diff --git a/src/queue.rs b/src/queue.rs index 147d86de..77546b70 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -4,7 +4,7 @@ pub use job::*; use crate::{ entity::queue::{ActiveModel, Column, Entity, Model}, - Config, Db, DivviupApi, + Config, Db, }; use sea_orm::{ sea_query::{all, Expr}, @@ -17,12 +17,11 @@ use tokio::{ task::{JoinHandle, JoinSet}, time::sleep, }; -use trillium_tokio::{CloneCounterObserver, Stopper}; +use tokio_util::sync::CancellationToken; #[derive(Clone, Debug)] pub struct Queue { - observer: CloneCounterObserver, - stopper: Stopper, + cancel: CancellationToken, db: Db, job_state: Arc, } @@ -46,32 +45,15 @@ fn reschedule_based_on_failure_count(failure_count: i32) -> Option for Queue { - fn from(app: &DivviupApi) -> Self { - Self::new(app.db(), app.config()) - } -} - impl Queue { - pub fn new(db: &Db, config: &Config) -> Self { + pub fn new(db: &Db, config: &Config, cancel: CancellationToken) -> Self { Self { - observer: Default::default(), + cancel, db: db.clone(), - stopper: Default::default(), job_state: Arc::new(config.into()), } } - pub fn with_observer(mut self, observer: CloneCounterObserver) -> Self { - self.observer = observer; - self - } - - pub fn with_stopper(mut self, stopper: Stopper) -> Self { - self.stopper = stopper; - self - } - pub async fn schedule_recurring_tasks_if_needed(&self) -> Result<(), DbErr> { let tx = self.db.begin().await?; @@ -108,7 +90,6 @@ impl Queue { pub async fn perform_one_queue_job(&self) -> Result, DbErr> { let tx = self.db.begin().await?; let model = if let Some(queue_item) = Entity::next(&tx).await? { - let _counter = self.observer.counter(); let mut queue_item = queue_item.into_active_model(); let mut job = queue_item.job.take().ok_or_else(|| { @@ -170,7 +151,7 @@ impl Queue { fn spawn_worker(self, join_set: &mut JoinSet<()>) { join_set.spawn(async move { loop { - if self.stopper.is_stopped() { + if self.cancel.is_cancelled() { break; } @@ -182,9 +163,12 @@ impl Queue { Ok(Some(_)) => {} Ok(None) => { - let sleep_future = - sleep(Duration::from_millis(fastrand::u64(QUEUE_CHECK_INTERVAL))); - self.stopper.stop_future(sleep_future).await; + let sleep_duration = + Duration::from_millis(fastrand::u64(QUEUE_CHECK_INTERVAL)); + tokio::select! { + () = self.cancel.cancelled() => break, + () = sleep(sleep_duration) => {} + } } } } @@ -199,7 +183,7 @@ impl Queue { } while join_set.join_next().await.is_some() { - if !self.stopper.is_stopped() { + if !self.cancel.is_cancelled() { tracing::error!("Worker task shut down. Restarting."); self.clone().spawn_worker(&mut join_set); } diff --git a/src/routes/tasks.rs b/src/routes/tasks.rs index 121da190..593dcde4 100644 --- a/src/routes/tasks.rs +++ b/src/routes/tasks.rs @@ -49,7 +49,7 @@ where .ok_or(Error::AccessDenied)?; actor - .if_allowed_http(&parts.method, task) + .if_allowed(&parts.method, task) .ok_or(Error::AccessDenied) } } diff --git a/src/telemetry.rs b/src/telemetry.rs index 30221908..b09a563d 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -1,22 +1,25 @@ +use axum::{ + extract::State, + http::{header, StatusCode}, + response::IntoResponse, +}; use git_version::git_version; use opentelemetry::{global, KeyValue}; use opentelemetry_sdk::{ metrics::{MetricError, SdkMeterProvider}, Resource, }; -use prometheus::Registry; +use prometheus::{Encoder, Registry, TextEncoder}; -/// Install a Prometheus metrics provider and exporter. The -/// OpenTelemetry global API can be used to create and update -/// instruments, and they will be sent through this exporter. -pub fn metrics_exporter() -> Result { +/// Install the Prometheus metrics provider and return the [`Registry`] so it +/// can be shared with the metrics HTTP handler. +pub fn install_metrics() -> Result { let registry = Registry::new(); let exporter = opentelemetry_prometheus::exporter() .with_registry(registry.clone()) .build() .unwrap(); - // Note that the implementation of `Default` pulls in attributes set via environment variables. let default_resource = Resource::default(); let mut git_revision: &str = git_version!(fallback = "unknown"); @@ -55,5 +58,19 @@ pub fn metrics_exporter() -> Result { .build() }); - Ok(trillium_prometheus::text_format_handler(registry)) + Ok(registry) +} + +/// Axum handler that serves Prometheus metrics in text format. +pub async fn metrics_handler( + State(registry): State, +) -> Result { + let encoder = TextEncoder::new(); + let content_type = encoder.format_type().to_owned(); + let metrics = registry.gather(); + let mut buf = String::new(); + encoder + .encode_utf8(&metrics, &mut buf) + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + Ok(([(header::CONTENT_TYPE, content_type)], buf)) } diff --git a/src/trace.rs b/src/trace.rs index da819c19..d90c22e7 100644 --- a/src/trace.rs +++ b/src/trace.rs @@ -1,5 +1,6 @@ //! Configures a tracing subscriber for divviup-api. +use axum::{extract::State, http::StatusCode}; use serde::{Deserialize, Serialize}; use std::{ io::{stdout, IsTerminal}, @@ -12,10 +13,6 @@ use tracing_log::LogTracer; use tracing_subscriber::{ filter::FromEnvError, layer::SubscriberExt, reload, EnvFilter, Layer, Registry, }; -use trillium::Handler; -use trillium_api::{api, State}; -use trillium_http::Status; -use trillium_router::Router; /// Errors from initializing trace subscriber. #[derive(Debug, thiserror::Error)] @@ -183,46 +180,36 @@ pub struct TraceGuards { _chrome_guard: Option, } -pub fn traceconfig_handler(trace_reload_handle: TraceReloadHandle) -> impl Handler { - ( - State(Arc::new(trace_reload_handle)), - Router::new() - .get("/traceconfig", api(get_traceconfig)) - .put("/traceconfig", api(put_traceconfig)), - ) +pub async fn get_traceconfig( + State(handle): State>, +) -> Result { + handle.with_current(|f| f.to_string()).map_err(|err| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("failed to get current filter: {err}"), + ) + }) } -async fn get_traceconfig( - conn: &mut trillium::Conn, - State(trace_reload_handle): State>, -) -> Result { - trace_reload_handle - .with_current(|trace_filter| trace_filter.to_string()) - .map_err(|err| { - conn.set_body(format!("failed to get current filter: {err}")); - Status::InternalServerError - }) -} - -/// Allows modifying the runtime tracing filter. Accepts a request whose body is a valid tracing -/// filter. Responds with the updated filter. See [`EnvFilter::try_new`] for details on the accepted -/// format. -async fn put_traceconfig( - conn: &mut trillium::Conn, - (State(trace_reload_handle), request): (State>, String), -) -> Result { - let new_filter = EnvFilter::try_new(request).map_err(|err| { - conn.set_body(format!("invalid filter: {err}")); - Status::BadRequest - })?; - trace_reload_handle.reload(new_filter).map_err(|err| { - conn.set_body(format!("failed to update filter: {err}")); - Status::InternalServerError +/// Allows modifying the runtime tracing filter. Accepts a request whose body +/// is a valid tracing filter string. Responds with the updated filter. See +/// [`EnvFilter::try_new`] for details on the accepted format. +pub async fn put_traceconfig( + State(handle): State>, + body: String, +) -> Result { + let new_filter = EnvFilter::try_new(body) + .map_err(|err| (StatusCode::BAD_REQUEST, format!("invalid filter: {err}")))?; + handle.reload(new_filter).map_err(|err| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("failed to update filter: {err}"), + ) })?; - trace_reload_handle - .with_current(|trace_filter| trace_filter.to_string()) - .map_err(|err| { - conn.set_body(format!("failed to get current filter: {err}")); - Status::InternalServerError - }) + handle.with_current(|f| f.to_string()).map_err(|err| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("failed to get current filter: {err}"), + ) + }) } diff --git a/src/user.rs b/src/user.rs index 60024c25..a5089b47 100644 --- a/src/user.rs +++ b/src/user.rs @@ -11,9 +11,6 @@ use sea_orm::{ use serde::{Deserialize, Serialize}; use time::{Duration, OffsetDateTime}; use tower_sessions::Session; -use trillium::{async_trait, Conn}; -use trillium_api::FromConn; -use trillium_sessions::SessionConnExt; pub const USER_SESSION_KEY: &str = "user"; @@ -73,23 +70,6 @@ impl User { } } -#[async_trait] -impl FromConn for User { - async fn from_conn(conn: &mut Conn) -> Option { - let mut user: Self = conn - .take_state() - .or_else(|| conn.session().get(USER_SESSION_KEY))?; - let db: &Db = conn.state()?; - user.populate_admin(db).await; - conn.insert_state(user.clone()); - Some(user) - } -} - -// --------------------------------------------------------------------------- -// Axum extractor — mirrors the Trillium FromConn above -// --------------------------------------------------------------------------- - impl User { /// Inner helper for the extractor impls below. /// diff --git a/test-support/src/lib.rs b/test-support/src/lib.rs index 19db3416..02c8b9b4 100644 --- a/test-support/src/lib.rs +++ b/test-support/src/lib.rs @@ -11,7 +11,13 @@ use divviup_api::{clients::aggregator_client::api_types, Config, Crypter, Db}; use serde::{de::DeserializeOwned, Serialize}; -use std::{error::Error, future::Future, iter::repeat_with, process::Termination}; +use std::{ + error::Error, + future::Future, + iter::repeat_with, + net::{Ipv6Addr, SocketAddr}, + process::Termination, +}; use tracing::install_test_trace_subscriber; use trillium::Handler; use trillium_client::Client; @@ -91,7 +97,8 @@ pub fn config(api_mocks: impl Handler) -> Config { auth_client_id: "client id".into(), auth_client_secret: "client secret".into(), auth_audience: "aud".into(), - monitoring_listen_address: "127.0.0.1:9464".parse().unwrap(), + listen_address: SocketAddr::from((Ipv6Addr::UNSPECIFIED, 0)), + monitoring_listen_address: SocketAddr::from((Ipv6Addr::LOCALHOST, 9464)), postmark_token: "-".into(), email_address: "test@example.test".parse().unwrap(), postmark_url: POSTMARK_URL.parse().unwrap(), diff --git a/tests/integration/axum_proxy.rs b/tests/integration/axum_proxy.rs deleted file mode 100644 index eec2f467..00000000 --- a/tests/integration/axum_proxy.rs +++ /dev/null @@ -1,21 +0,0 @@ -use test_support::{assert_eq, test, *}; - -#[test(harness = set_up)] -async fn axum_proxy_bridge(app: DivviupApi) -> TestResult { - let mut conn = get("/internal/test/axum_ready") - .with_api_host() - .run_async(&app) - .await; - assert_eq!(conn.status().unwrap(), Status::Ok); - assert_eq!(conn.take_response_body_string().unwrap(), "axum OK"); - Ok(()) -} - -/// Verify that `/health` — the first real route migrated to Axum — is -/// reached via the Trillium → proxy → Axum path. -#[test(harness = set_up)] -async fn axum_proxies_health(app: DivviupApi) -> TestResult { - let conn = get("/health").with_api_host().run_async(&app).await; - assert_eq!(conn.status().unwrap(), Status::Ok); - Ok(()) -} diff --git a/tests/integration/jobs.rs b/tests/integration/jobs.rs index 7c3f32bd..515df17c 100644 --- a/tests/integration/jobs.rs +++ b/tests/integration/jobs.rs @@ -3,6 +3,7 @@ use divviup_api::{ queue::{CreateUser, JobStatus, ResetPassword, SendInvitationEmail, V1}, }; use test_support::{assert_eq, test, *}; +use tokio_util::sync::CancellationToken; use uuid::Uuid; #[test(harness = with_client_logs)] @@ -96,7 +97,7 @@ async fn all_together(app: DivviupApi, client_logs: ClientLogs) -> TestResult { .await?; let mut completed_queue_jobs = vec![]; - let queue = Queue::new(app.db(), app.config()); + let queue = Queue::new(app.db(), app.config(), CancellationToken::new()); while let Some(queue_job) = queue.perform_one_queue_job().await? { completed_queue_jobs.push(queue_job); } diff --git a/tests/integration/main.rs b/tests/integration/main.rs index 79cc4755..ed73e391 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -5,7 +5,6 @@ mod aggregators; mod api_tokens; mod assets; mod auth; -mod axum_proxy; mod collector_credentials; mod crypter; mod health_check;