diff --git a/.env b/.env index 4d1fd11..e69de29 100644 --- a/.env +++ b/.env @@ -1,4 +0,0 @@ -DATABASE_URL=postgres://ak:ak@localhost:25432/my_axum_template -POSTGRES_DB=my-axum_template -POSTGRES_USER=ak -POSTGRES_PASSWORD=ak diff --git a/Cargo.lock b/Cargo.lock index e401ff9..7ef2b1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,12 +189,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -284,15 +278,6 @@ dependencies = [ "syn", ] -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -402,9 +387,6 @@ name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -dependencies = [ - "serde_core", -] [[package]] name = "block-buffer" @@ -451,12 +433,6 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" version = "1.11.0" @@ -575,21 +551,6 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "convert_case" version = "0.10.0" @@ -624,21 +585,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc32fast" version = "1.5.0" @@ -648,21 +594,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - [[package]] name = "crypto-common" version = "0.1.6" @@ -689,7 +620,6 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", "pem-rfc7468", "zeroize", ] @@ -733,7 +663,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "const-oid", "crypto-common", "subtle", ] @@ -759,21 +688,6 @@ dependencies = [ "syn", ] -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -dependencies = [ - "serde", -] - [[package]] name = "encoding_rs" version = "0.8.35" @@ -799,28 +713,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "etcetera" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.48.0", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - [[package]] name = "fastrand" version = "2.3.0" @@ -855,17 +747,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "flume" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" -dependencies = [ - "futures-core", - "futures-sink", - "spin", -] - [[package]] name = "fnv" version = "1.0.7" @@ -944,17 +825,6 @@ dependencies = [ "futures-util", ] -[[package]] -name = "futures-intrusive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot", -] - [[package]] name = "futures-io" version = "0.3.31" @@ -1062,32 +932,12 @@ dependencies = [ "tracing", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "heck" version = "0.5.0" @@ -1100,15 +950,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - [[package]] name = "hmac" version = "0.12.1" @@ -1118,15 +959,6 @@ dependencies = [ "digest", ] -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "hostname" version = "0.4.2" @@ -1420,7 +1252,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown", "serde", "serde_core", ] @@ -1464,15 +1296,18 @@ dependencies = [ "chrono", "clap", "futures", + "hmac", "jsonwebtoken", "mimalloc", + "percent-encoding", "rand 0.8.5", "reqwest", "sentry", "serde", "serde_json", + "serde_urlencoded", "serde_variant", - "sqlx", + "sha2", "thiserror", "tokio", "toml", @@ -1531,9 +1366,6 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] [[package]] name = "libc" @@ -1541,12 +1373,6 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" -[[package]] -name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - [[package]] name = "libmimalloc-sys" version = "0.1.44" @@ -1557,27 +1383,6 @@ dependencies = [ "libc", ] -[[package]] -name = "libredox" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" -dependencies = [ - "bitflags", - "libc", - "redox_syscall 0.7.0", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" -dependencies = [ - "pkg-config", - "vcpkg", -] - [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1626,16 +1431,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - [[package]] name = "memchr" version = "2.7.6" @@ -1754,22 +1549,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.5", - "smallvec", - "zeroize", -] - [[package]] name = "num-conv" version = "0.1.0" @@ -1785,17 +1564,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1803,7 +1571,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -2046,12 +1813,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "parking_lot" version = "0.12.5" @@ -2070,7 +1831,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] @@ -2138,27 +1899,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - [[package]] name = "pkg-config" version = "0.3.32" @@ -2281,15 +2021,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "redox_syscall" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" -dependencies = [ - "bitflags", -] - [[package]] name = "regex" version = "1.12.2" @@ -2382,26 +2113,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "signature", - "spki", - "subtle", - "zeroize", -] - [[package]] name = "rustc-demangle" version = "0.1.26" @@ -2437,7 +2148,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "once_cell", - "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -2741,17 +2451,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha2" version = "0.10.9" @@ -2788,16 +2487,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - [[package]] name = "simd-adler32" version = "0.3.8" @@ -2827,9 +2516,6 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] [[package]] name = "socket2" @@ -2856,217 +2542,6 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "sqlx" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" -dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", -] - -[[package]] -name = "sqlx-core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" -dependencies = [ - "base64", - "bytes", - "chrono", - "crc", - "crossbeam-queue", - "either", - "event-listener", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashbrown 0.15.5", - "hashlink", - "indexmap", - "log", - "memchr", - "once_cell", - "percent-encoding", - "rustls", - "serde", - "serde_json", - "sha2", - "smallvec", - "thiserror", - "tokio", - "tokio-stream", - "tracing", - "url", - "uuid", - "webpki-roots 0.26.11", -] - -[[package]] -name = "sqlx-macros" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" -dependencies = [ - "proc-macro2", - "quote", - "sqlx-core", - "sqlx-macros-core", - "syn", -] - -[[package]] -name = "sqlx-macros-core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" -dependencies = [ - "dotenvy", - "either", - "heck", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2", - "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", - "syn", - "tokio", - "url", -] - -[[package]] -name = "sqlx-mysql" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" -dependencies = [ - "atoi", - "base64", - "bitflags", - "byteorder", - "bytes", - "chrono", - "crc", - "digest", - "dotenvy", - "either", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "percent-encoding", - "rand 0.8.5", - "rsa", - "serde", - "sha1", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-postgres" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" -dependencies = [ - "atoi", - "base64", - "bitflags", - "byteorder", - "chrono", - "crc", - "dotenvy", - "etcetera", - "futures-channel", - "futures-core", - "futures-util", - "hex", - "hkdf", - "hmac", - "home", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "rand 0.8.5", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-sqlite" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" -dependencies = [ - "atoi", - "chrono", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "serde", - "serde_urlencoded", - "sqlx-core", - "thiserror", - "tracing", - "url", - "uuid", -] [[package]] name = "stable_deref_trait" @@ -3074,17 +2549,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - [[package]] name = "strsim" version = "0.11.1" @@ -3232,21 +2696,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.49.0" @@ -3295,17 +2744,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.18" @@ -3517,33 +2955,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -3724,12 +3141,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "wasm-bindgen" version = "0.2.106" @@ -3807,34 +3218,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.5", -] - -[[package]] -name = "webpki-roots" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "whoami" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" -dependencies = [ - "libredox", - "wasite", -] - [[package]] name = "winapi" version = "0.3.9" @@ -3927,15 +3310,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -3963,21 +3337,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -4011,12 +3370,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4029,12 +3382,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4047,12 +3394,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4077,12 +3418,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -4095,12 +3430,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4113,12 +3442,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -4131,12 +3454,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 17277bd..7155ae8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ path = "src/main.rs" [dependencies] serde = { version = "1.0.228", features = [ "derive" ] } serde_json = "1.0.149" +serde_urlencoded = "0.7.1" tokio = { version = "1.49.0", features = [ "signal", "rt-multi-thread", @@ -46,13 +47,6 @@ utoipa = { version = "5.4.0", features = [ ] } utoipa-axum = { version = "0.2.0", features = [ "debug" ] } utoipa-scalar = { version = "0.3.0", features = [ "axum" ] } -sqlx = { version = "0.8.6", features = [ - "runtime-tokio", - "postgres", - "tls-rustls", - "chrono", - "uuid", -] } sentry = { version = "0.46.1", features = [ "tracing", "tower", @@ -66,3 +60,6 @@ serde_variant = "0.1.3" reqwest = { version = "0.12.28", features = ["json", "multipart"] } rand = "0.8" jsonwebtoken = "9.3" +sha2 = "0.10" +hmac = "0.12" +percent-encoding = "2.3.2" diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 5305778..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,13 +0,0 @@ -services: - db: - image: postgres:17.6 - restart: unless-stopped - ports: - - 25432:5432 - volumes: - - postgres-data:/var/lib/postgresql/data - env_file: - - .env - -volumes: - postgres-data: diff --git a/example.toml b/example.toml index c7d0a74..53e3164 100644 --- a/example.toml +++ b/example.toml @@ -21,15 +21,22 @@ host = "http://localhost" # auth.user = "support@mooncell.wiki" # Uncomment and add SMTP user # auth.password = "Mooncell70080190" # Uncomment and add SMTP password -# Database Configuration -[database] - uri = "postgres://janus:janus@localhost:25432/janus" - # Bilibili Configuration [bilibili] sessdata = "your_bilibili_sessdata_cookie" bili_jct = "your_bilibili_bili_jct" +# Aliyun Configuration +[aliyun] +access_key_id = "your_aliyun_access_key_id" +access_key_secret = "your_aliyun_access_key_secret" + +# Bucket to URL template mapping +# The {object_key} placeholder will be replaced with the actual object key (URL-encoded) +[aliyun.bucket_url_map] +prts-static = "https://static.prts.wiki/{object_key}" +ak-media = "https://media.prts.wiki/{object_key}" + # JWT Configuration (required for API authentication) # Generate ES256 key pair (compatible with jsonwebtoken): # openssl ecparam -genkey -name prime256v1 -noout -out private.pem diff --git a/migrations/00000000000000_initial.sql b/migrations/00000000000000_initial.sql deleted file mode 100644 index 3381270..0000000 --- a/migrations/00000000000000_initial.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Initial migration -CREATE TABLE IF NOT EXISTS health_checks ( - id SERIAL PRIMARY KEY, - checked_at TIMESTAMP DEFAULT NOW() -); diff --git a/src/aliyun/cdn.rs b/src/aliyun/cdn.rs new file mode 100644 index 0000000..dae4d67 --- /dev/null +++ b/src/aliyun/cdn.rs @@ -0,0 +1,181 @@ +use crate::config::AliyunConfig; +use crate::error::{AppError, AppResult}; +use anyhow::Context; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use utoipa::ToSchema; + +use super::signature::{AliyunSignInput, AliyunSigner}; + +/// CDN API endpoint +const CDN_ENDPOINT: &str = "https://cdn.aliyuncs.com"; +const CDN_HOST: &str = "cdn.aliyuncs.com"; + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct TasksContainer { + #[serde(rename = "CDNTask")] + pub cdn_tasks: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct RefreshTask { + #[serde(rename = "TaskId")] + pub task_id: String, + + #[serde(rename = "ObjectPath")] + pub object_path: String, + + #[serde(rename = "ObjectType")] + pub object_type: String, + + #[serde(rename = "Status")] + pub status: String, + + #[serde(rename = "Process")] + pub process: String, + + #[serde(rename = "CreationTime")] + pub creation_time: String, + + #[serde(rename = "Description", skip_serializing_if = "Option::is_none")] + pub description: Option, +} + +/// Request parameters for RefreshObjectCaches API +/// +/// Reference: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-refreshobjectcaches +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RefreshObjectCachesRequest { + /// Object paths to refresh (separated by newlines, max 1000 URLs or 100 directories per request) + pub object_path: String, + + /// Object type: "File" for file refresh, "Directory" for directory refresh + #[serde(skip_serializing_if = "Option::is_none")] + pub object_type: Option, + + /// Whether to directly delete CDN cache nodes (default false) + #[serde(skip_serializing_if = "Option::is_none")] + pub force: Option, +} + +/// Form parameters for RefreshObjectCaches API +/// This struct is used for URL encoding the request body +#[derive(Debug, Clone, Serialize)] +struct RefreshObjectCachesFormParams { + #[serde(rename = "ObjectPath")] + object_path: String, + + #[serde(rename = "ObjectType", skip_serializing_if = "Option::is_none")] + object_type: Option, + + #[serde(rename = "Force", skip_serializing_if = "Option::is_none")] + force: Option, +} + +/// Response from RefreshObjectCaches API +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct RefreshObjectCachesResponse { + #[serde(rename = "RequestId")] + pub request_id: String, + + #[serde(rename = "RefreshTaskId")] + pub refresh_task_id: String, +} + +/// Aliyun CDN API client +pub struct AliyunCdnClient { + signer: AliyunSigner, + client: reqwest::Client, +} + +impl AliyunCdnClient { + /// Create a new Aliyun CDN client + pub fn new(config: &AliyunConfig, client: reqwest::Client) -> Self { + let signer = AliyunSigner::new( + config.access_key_id.clone(), + config.access_key_secret.clone(), + ); + + Self { signer, client } + } + + /// Call RefreshObjectCaches API + /// + /// # Arguments + /// * `request` - Request parameters + /// + /// # Returns + /// Response containing refresh task ID + pub async fn refresh_object_caches( + &self, + request: &RefreshObjectCachesRequest, + ) -> AppResult { + // RefreshObjectCaches is a POST request with parameters in an HTML form body. + // Reference: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-refreshobjectcaches + let form_params = RefreshObjectCachesFormParams { + object_path: request.object_path.clone(), + object_type: request.object_type.clone(), + force: request.force, + }; + + let form_body = serde_urlencoded::to_string(&form_params) + .context("Failed to encode form parameters")?; + + // Sign the request (ACS3-HMAC-SHA256). For this API, the form body must be included + // in the body hash, so keep the canonical query empty. + let signed = self + .signer + .sign_request(AliyunSignInput { + method: "POST", + host: CDN_HOST, + canonical_uri: "/", + action: "RefreshObjectCaches", + version: "2018-05-10", + query_params: BTreeMap::new(), + body: form_body.as_bytes(), + content_type: Some("application/x-www-form-urlencoded"), + extra_headers: BTreeMap::new(), + }) + .context("Failed to sign Aliyun request")?; + + let query_string = signed.query_string; + let headers = signed.headers; + + let url = if query_string.is_empty() { + format!("{}/", CDN_ENDPOINT) + } else { + format!("{}/?{}", CDN_ENDPOINT, query_string) + }; + + // Send request + let response = self + .client + .post(&url) + .headers(headers) + .body(form_body) + .send() + .await + .context("Failed to send RefreshObjectCaches request")?; + + // Parse response + let status = response.status(); + let body = response + .text() + .await + .context("Failed to read response body")?; + + if !status.is_success() { + return Err(AppError::InternalError(anyhow::anyhow!( + "Aliyun API error (status {}): {}", + status, + body + ))); + } + + // Parse JSON response + let result: RefreshObjectCachesResponse = + serde_json::from_str(&body).context("Failed to parse RefreshObjectCaches response")?; + + Ok(result) + } +} diff --git a/src/aliyun/mod.rs b/src/aliyun/mod.rs new file mode 100644 index 0000000..a0f51ed --- /dev/null +++ b/src/aliyun/mod.rs @@ -0,0 +1,5 @@ +pub mod cdn; +mod signature; + +pub use cdn::{AliyunCdnClient, RefreshObjectCachesRequest, RefreshObjectCachesResponse}; +pub use signature::{AliyunSigner, UNRESERVED}; diff --git a/src/aliyun/signature.rs b/src/aliyun/signature.rs new file mode 100644 index 0000000..df8480d --- /dev/null +++ b/src/aliyun/signature.rs @@ -0,0 +1,405 @@ +use anyhow::{Context, Result}; +use chrono::Utc; +use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_encode}; +use rand::RngCore; +use sha2::{Digest, Sha256}; +use std::collections::BTreeMap; + +/// RFC 3986 unreserved characters: ALPHA / DIGIT / "-" / "_" / "." / "~" +/// These characters should NOT be percent-encoded. +pub const UNRESERVED: &AsciiSet = &NON_ALPHANUMERIC + .remove(b'-') + .remove(b'_') + .remove(b'.') + .remove(b'~'); + +/// Aliyun OpenAPI V3 signature generator (ACS3-HMAC-SHA256) +/// +/// Docs: https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature +pub struct AliyunSigner { + access_key_id: String, + access_key_secret: String, +} + +pub struct AliyunSignInput<'a> { + pub method: &'a str, + pub host: &'a str, + pub canonical_uri: &'a str, + pub action: &'a str, + pub version: &'a str, + pub query_params: BTreeMap, + pub body: &'a [u8], + pub content_type: Option<&'a str>, + /// Any extra request headers. If the name is `x-acs-*`, `host`, or `content-type`, it will be included in the signature. + pub extra_headers: BTreeMap, +} + +pub struct AliyunSignedRequest { + /// RFC3986-encoded canonical query string. + pub query_string: String, + pub headers: reqwest::header::HeaderMap, +} + +impl AliyunSigner { + pub fn new(access_key_id: String, access_key_secret: String) -> Self { + Self { + access_key_id, + access_key_secret, + } + } + + /// Generate a random nonce (hex string) for request. + fn generate_nonce() -> String { + let mut bytes = [0u8; 16]; + rand::thread_rng().fill_bytes(&mut bytes); + hex_encode_lower(&bytes) + } + + /// Get current timestamp in ISO 8601 format (UTC) + fn get_timestamp() -> String { + Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string() + } + + fn build_canonical_query_string(params: &BTreeMap) -> String { + params + .iter() + .map(|(k, v)| { + format!( + "{}={}", + percent_encode(k.as_bytes(), UNRESERVED), + percent_encode(v.as_bytes(), UNRESERVED) + ) + }) + .collect::>() + .join("&") + } + + /// Canonicalize a request path (CanonicalURI). + /// + /// For RPC-style APIs this is typically just `/`. + fn canonicalize_uri(path: &str) -> String { + if path.is_empty() { + return "/".to_string(); + } + if path == "/" { + return "/".to_string(); + } + + let has_trailing_slash = path.ends_with('/'); + let trimmed = path.trim_matches('/'); + let mut out = String::from("/"); + if !trimmed.is_empty() { + out.push_str( + &trimmed + .split('/') + .map(|segment| percent_encode(segment.as_bytes(), UNRESERVED).to_string()) + .collect::>() + .join("/"), + ); + } + if has_trailing_slash { + out.push('/'); + } + out + } + + pub fn sign_request(&self, input: AliyunSignInput<'_>) -> Result { + let host = input.host.trim(); + let action = input.action.trim(); + let version = input.version.trim(); + + let x_acs_date = Self::get_timestamp(); + let x_acs_signature_nonce = Self::generate_nonce(); + let x_acs_content_sha256 = sha256_hex(input.body); + + // Canonical query + let canonical_query = Self::build_canonical_query_string(&input.query_params); + let canonical_uri = Self::canonicalize_uri(input.canonical_uri); + + // Build headers participating in signing. + // Must include: host + all x-acs-* headers (except Authorization). content-type is included if present. + let mut signing_headers: BTreeMap = BTreeMap::new(); + + for (k, v) in input.extra_headers { + let key = k.trim().to_ascii_lowercase(); + if key == "host" || key == "content-type" || key.starts_with("x-acs-") { + signing_headers.insert(key, v.trim().to_string()); + } + } + + signing_headers.insert("host".to_string(), host.to_string()); + signing_headers.insert("x-acs-action".to_string(), action.to_string()); + signing_headers.insert("x-acs-version".to_string(), version.to_string()); + signing_headers.insert("x-acs-date".to_string(), x_acs_date.clone()); + signing_headers.insert( + "x-acs-signature-nonce".to_string(), + x_acs_signature_nonce.clone(), + ); + signing_headers.insert( + "x-acs-content-sha256".to_string(), + x_acs_content_sha256.clone(), + ); + + if let Some(ct) = input.content_type { + signing_headers.insert("content-type".to_string(), ct.trim().to_string()); + } + + let canonical_headers = signing_headers + .iter() + .map(|(k, v)| format!("{}:{}\n", k, v.trim())) + .collect::(); + let signed_headers = signing_headers + .keys() + .cloned() + .collect::>() + .join(";"); + + let canonical_request = format!( + "{}\n{}\n{}\n{}\n{}\n{}", + input.method.to_uppercase(), + canonical_uri, + canonical_query, + canonical_headers, + signed_headers, + x_acs_content_sha256 + ); + + let hashed_canonical_request = sha256_hex(canonical_request.as_bytes()); + let string_to_sign = format!("ACS3-HMAC-SHA256\n{}", hashed_canonical_request); + let signature = hmac_sha256_hex(&self.access_key_secret, &string_to_sign); + + let authorization = format!( + "ACS3-HMAC-SHA256 Credential={},SignedHeaders={},Signature={}", + self.access_key_id, signed_headers, signature + ); + + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::HOST, + host.parse().context("invalid host header value")?, + ); + headers.insert( + reqwest::header::HeaderName::from_static("x-acs-action"), + action + .parse() + .context("invalid x-acs-action header value")?, + ); + headers.insert( + reqwest::header::HeaderName::from_static("x-acs-version"), + version + .parse() + .context("invalid x-acs-version header value")?, + ); + headers.insert( + reqwest::header::HeaderName::from_static("x-acs-date"), + x_acs_date + .parse() + .context("invalid x-acs-date header value")?, + ); + headers.insert( + reqwest::header::HeaderName::from_static("x-acs-signature-nonce"), + x_acs_signature_nonce + .parse() + .context("invalid x-acs-signature-nonce header value")?, + ); + headers.insert( + reqwest::header::HeaderName::from_static("x-acs-content-sha256"), + x_acs_content_sha256 + .parse() + .context("invalid x-acs-content-sha256 header value")?, + ); + if let Some(ct) = input.content_type { + headers.insert( + reqwest::header::CONTENT_TYPE, + ct.parse().context("invalid content-type header value")?, + ); + } + headers.insert( + reqwest::header::AUTHORIZATION, + authorization + .parse() + .context("invalid authorization header value")?, + ); + + Ok(AliyunSignedRequest { + query_string: canonical_query, + headers, + }) + } +} + +fn sha256_hex(input: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(input); + hex_encode_lower(&hasher.finalize()) +} + +fn hmac_sha256_hex(secret: &str, message: &str) -> String { + use hmac::{Hmac, Mac}; + type HmacSha256 = Hmac; + + let mut mac = + HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size"); + mac.update(message.as_bytes()); + let result = mac.finalize().into_bytes(); + hex_encode_lower(&result) +} + +fn hex_encode_lower(input: &[u8]) -> String { + let mut out = String::with_capacity(input.len() * 2); + for b in input { + use std::fmt::Write; + write!(&mut out, "{:02x}", b).expect("write into string"); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_v3_signature_example_from_docs() { + // Example from Aliyun docs (V3 request structure & signature) + // https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature + let signer = AliyunSigner::new( + "YourAccessKeyId".to_string(), + "YourAccessKeySecret".to_string(), + ); + + // Build query params (these are the API request parameters in the docs example) + let mut query_params = BTreeMap::new(); + query_params.insert( + "ImageId".to_string(), + "win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd".to_string(), + ); + query_params.insert("RegionId".to_string(), "cn-shanghai".to_string()); + + // Keep deterministic verification without injecting timestamp/nonce into `sign_request`. + let method = "POST"; + let host = "ecs.cn-shanghai.aliyuncs.com"; + let canonical_uri = "/"; + let action = "RunInstances"; + let version = "2014-05-26"; + let x_acs_date = "2023-10-26T10:22:32Z"; + let x_acs_signature_nonce = "3156853299f313e23d1673dc12e1703d"; + let x_acs_content_sha256 = + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + + let canonical_query = AliyunSigner::build_canonical_query_string(&query_params); + assert_eq!( + canonical_query, + "ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai" + ); + + let mut signing_headers: BTreeMap = BTreeMap::new(); + signing_headers.insert("host".to_string(), host.to_string()); + signing_headers.insert("x-acs-action".to_string(), action.to_string()); + signing_headers.insert( + "x-acs-content-sha256".to_string(), + x_acs_content_sha256.to_string(), + ); + signing_headers.insert("x-acs-date".to_string(), x_acs_date.to_string()); + signing_headers.insert( + "x-acs-signature-nonce".to_string(), + x_acs_signature_nonce.to_string(), + ); + signing_headers.insert("x-acs-version".to_string(), version.to_string()); + + let canonical_headers = signing_headers + .iter() + .map(|(k, v)| format!("{}:{}\n", k, v)) + .collect::(); + let signed_headers = signing_headers + .keys() + .cloned() + .collect::>() + .join(";"); + + assert_eq!( + signed_headers, + "host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version" + ); + + let canonical_request = format!( + "{}\n{}\n{}\n{}\n{}\n{}", + method, + AliyunSigner::canonicalize_uri(canonical_uri), + canonical_query, + canonical_headers, + signed_headers, + x_acs_content_sha256 + ); + + let hashed_canonical_request = sha256_hex(canonical_request.as_bytes()); + assert_eq!( + hashed_canonical_request, + "7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259" + ); + + let string_to_sign = format!("ACS3-HMAC-SHA256\n{}", hashed_canonical_request); + let signature = hmac_sha256_hex(&signer.access_key_secret, &string_to_sign); + assert_eq!( + signature, + "06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0" + ); + } + + #[test] + fn test_canonicalize_uri_with_unreserved_chars() { + // Test that unreserved characters (-, _, ., ~) are NOT percent-encoded + assert_eq!( + AliyunSigner::canonicalize_uri("/path-with_dots.and~tilde"), + "/path-with_dots.and~tilde" + ); + + // Test with multiple segments + assert_eq!( + AliyunSigner::canonicalize_uri("/api/v1.0/user_name-123~test"), + "/api/v1.0/user_name-123~test" + ); + + // Test that special characters ARE encoded + assert_eq!( + AliyunSigner::canonicalize_uri("/path with spaces"), + "/path%20with%20spaces" + ); + + // Test mixed case + assert_eq!( + AliyunSigner::canonicalize_uri("/valid-_.~/but spaces"), + "/valid-_.~/but%20spaces" + ); + + // Test that path separators (/) are preserved and not encoded + assert_eq!( + AliyunSigner::canonicalize_uri("/path/to/resource"), + "/path/to/resource" + ); + } + + #[test] + fn test_build_canonical_query_string_with_unreserved_chars() { + // Test that unreserved characters (-, _, ., ~) are NOT percent-encoded + let mut params = BTreeMap::new(); + params.insert("key-1".to_string(), "value_1".to_string()); + params.insert("key.2".to_string(), "value.2".to_string()); + params.insert("key~3".to_string(), "value~3".to_string()); + + let result = AliyunSigner::build_canonical_query_string(¶ms); + // BTreeMap orders keys alphabetically + assert_eq!(result, "key-1=value_1&key.2=value.2&key~3=value~3"); + + // Test that special characters ARE encoded + let mut params2 = BTreeMap::new(); + params2.insert("key with space".to_string(), "value with space".to_string()); + let result2 = AliyunSigner::build_canonical_query_string(¶ms2); + assert_eq!(result2, "key%20with%20space=value%20with%20space"); + + // Test mixed case + let mut params3 = BTreeMap::new(); + params3.insert("valid-_~.key".to_string(), "needs encoding!".to_string()); + let result3 = AliyunSigner::build_canonical_query_string(¶ms3); + assert_eq!(result3, "valid-_~.key=needs%20encoding%21"); + } +} diff --git a/src/app.rs b/src/app.rs index b8f9584..237b2e6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,10 +7,9 @@ use tracing::info; use crate::{ auth::generate_token, config::AppSettings, - repository::Repository, routes::build_router, shutdown::shutdown_signal, - state::init_state_with_pg, + state::init_state, tracing::{init_sentry, init_tracing}, }; @@ -39,8 +38,7 @@ async fn start(config: &AppSettings) -> Result<()> { // // Build router let listener = TcpListener::bind(config.server.full_url()).await?; info!("Server is running on {}", config.server.full_url()); - let state = init_state_with_pg(config).await; - state.repository.migrate().await?; + let state = init_state(config).await; let router = build_router(state); axum::serve(listener, router) .with_graceful_shutdown(shutdown_signal()) diff --git a/src/auth.rs b/src/auth.rs index 6d3db5a..c2fcd53 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -5,7 +5,10 @@ use axum::{ }; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode}; use serde::{Deserialize, Serialize}; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{ + collections::HashSet, + time::{SystemTime, UNIX_EPOCH}, +}; use crate::error::{AppError, AppResult}; use crate::state::AppState; @@ -55,6 +58,7 @@ pub fn verify_token( let decoding_key = DecodingKey::from_ec_pem(public_key_pem.as_bytes())?; let mut validation = Validation::new(Algorithm::ES256); validation.validate_exp = false; // No expiration validation + validation.required_spec_claims = HashSet::new(); // don't validate “exp”, “nbf”, “aud”, “iss”, “sub” let token_data = decode::(token, &decoding_key, &validation)?; Ok(token_data.claims) diff --git a/src/config.rs b/src/config.rs index ba50be9..81178c8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,19 +1,9 @@ use serde::{Deserialize, Serialize}; use serde_variant::to_variant_name; -use std::{fs, path::Path}; +use std::{collections::HashMap, fs, path::Path}; use thiserror::Error; use tracing::info; -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct DatabaseConfig { - /// The URI for connecting to the database. For example: - /// * Postgres: `postgres://root:12341234@localhost:5432/myapp_development` - /// * Sqlite: `sqlite://db.sqlite?mode=rwc` - pub uri: String, - pub max_connections: Option, - pub connection_timeout_seconds: Option, -} - /// SMTP configuration for application use #[derive(Debug, Clone, Deserialize, Serialize)] pub struct SmtpConfig { @@ -111,6 +101,19 @@ pub struct JwtConfig { pub public_key: String, } +/// Aliyun configuration for CDN API +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct AliyunConfig { + /// Aliyun Access Key ID + pub access_key_id: String, + /// Aliyun Access Key Secret + pub access_key_secret: String, + /// Bucket name to URL template mapping + /// The URL template can contain {object_key} placeholder which will be replaced with the actual object key + #[serde(default)] + pub bucket_url_map: HashMap, +} + /// Server configuration for application use #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ServerConfig { @@ -140,11 +143,11 @@ impl ServerConfig { pub struct AppSettings { pub logger: LoggerConfig, pub server: ServerConfig, - pub database: DatabaseConfig, pub mailer: Option, pub sentry: Option, pub bilibili: BilibiliConfig, pub jwt: JwtConfig, + pub aliyun: AliyunConfig, } impl AppSettings { diff --git a/src/error.rs b/src/error.rs index 49d2a46..2a06338 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,6 +5,7 @@ use axum::{ }; use serde_json::json; use thiserror::Error; +use tracing::error; /// Application-level errors for HTTP handlers #[derive(Error, Debug)] @@ -35,7 +36,11 @@ impl IntoResponse for AppError { let status = self.status_code(); // Log the detailed error with full context chain - tracing::error!("Handler error: {:?}", self); + error!( + error = ?self, + status_code = %status, + "Handler error" + ); let body = json!({ "code": 1, @@ -45,13 +50,6 @@ impl IntoResponse for AppError { } } -// Implement From for common error types to allow automatic conversion -impl From for AppError { - fn from(err: sqlx::Error) -> Self { - AppError::InternalError(anyhow::Error::new(err)) - } -} - impl From for AppError { fn from(err: serde_json::Error) -> Self { AppError::BadRequest(anyhow::Error::new(err)) diff --git a/src/lib.rs b/src/lib.rs index ed65b3d..e012983 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,9 @@ +pub mod aliyun; pub mod app; pub mod auth; mod config; pub mod error; mod middleware; -mod repository; mod routes; mod shutdown; mod state; diff --git a/src/repository.rs b/src/repository.rs deleted file mode 100644 index 33cb938..0000000 --- a/src/repository.rs +++ /dev/null @@ -1,29 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; -use sqlx::{Pool, Postgres}; -use tracing::info; - -#[async_trait] -pub trait Repository: Send + Sync + Clone + 'static { - async fn health_check(&self) -> bool; - async fn migrate(&self) -> Result<()>; -} - -#[derive(Debug, Clone)] -pub struct PostgresRepository { - pub pool: Pool, -} - -#[async_trait] -impl Repository for PostgresRepository { - async fn health_check(&self) -> bool { - sqlx::query("SELECT 1").execute(&self.pool).await.is_ok() - } - - async fn migrate(&self) -> Result<()> { - info!("Running database migrations"); - sqlx::migrate!("./migrations").run(&self.pool).await?; - info!("Database migrations completed"); - Ok(()) - } -} diff --git a/src/routes/aliyun_handlers.rs b/src/routes/aliyun_handlers.rs new file mode 100644 index 0000000..f97badf --- /dev/null +++ b/src/routes/aliyun_handlers.rs @@ -0,0 +1,175 @@ +use axum::{Json, extract::State, http::HeaderMap}; +use percent_encoding::percent_encode; +use serde::{Deserialize, Serialize}; +use tracing::info; +use utoipa::ToSchema; + +use crate::aliyun::UNRESERVED; +use crate::state::AppState; +use crate::{ + aliyun::{AliyunCdnClient, RefreshObjectCachesRequest}, + error::{AppError, AppResult}, +}; + +/// OSS bucket information in event data +#[derive(ToSchema, Serialize, Deserialize, Debug)] +pub struct OssBucket { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub arn: Option, + #[serde(rename = "ownerIdentity", skip_serializing_if = "Option::is_none")] + pub owner_identity: Option, +} + +/// OSS object information in event data +#[derive(ToSchema, Serialize, Deserialize, Debug)] +pub struct OssObject { + pub key: String, + #[serde(rename = "eTag", skip_serializing_if = "Option::is_none")] + pub etag: Option, + #[serde(rename = "deltaSize", skip_serializing_if = "Option::is_none")] + pub delta_size: Option, +} + +/// OSS-specific data in event +#[derive(ToSchema, Serialize, Deserialize, Debug)] +pub struct OssData { + pub bucket: OssBucket, + pub object: OssObject, + #[serde(rename = "ossSchemaVersion", skip_serializing_if = "Option::is_none")] + pub oss_schema_version: Option, +} + +/// Complete event data structure from OSS +#[derive(ToSchema, Serialize, Deserialize, Debug)] +pub struct OssEventData { + #[serde(skip_serializing_if = "Option::is_none")] + pub region: Option, + #[serde(rename = "eventVersion", skip_serializing_if = "Option::is_none")] + pub event_version: Option, + #[serde(rename = "eventSource", skip_serializing_if = "Option::is_none")] + pub event_source: Option, + #[serde(rename = "eventName", skip_serializing_if = "Option::is_none")] + pub event_name: Option, + #[serde(rename = "eventTime", skip_serializing_if = "Option::is_none")] + pub event_time: Option, + pub oss: OssData, +} + +/// EventBridge OSS event payload +#[derive(ToSchema, Serialize, Deserialize, Debug)] +pub struct OssEventPayload { + pub id: String, + pub source: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub specversion: Option, + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub event_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub datacontenttype: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub subject: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub time: Option, + pub data: OssEventData, +} + +/// Response for OSS event handler +#[derive(ToSchema, Serialize, Deserialize, Debug)] +pub struct OssEventResponse { + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub task_id: Option, +} + +/// Handle Aliyun EventBridge OSS events +#[utoipa::path( + post, + tag = "aliyun", + path = "/aliyun/events", + request_body = OssEventPayload, + responses( + (status = OK, description = "Successfully processed OSS event and triggered CDN refresh", body = OssEventResponse), + (status = UNAUTHORIZED, description = "Missing or invalid x-eventbridge-signature-token"), + (status = BAD_REQUEST, description = "Invalid request or unsupported bucket"), + (status = INTERNAL_SERVER_ERROR, description = "Internal server error") + ), + security( + ("eventbridge_token" = []) + ) +)] +pub async fn handle_oss_events( + State(state): State, + headers: HeaderMap, + Json(raw_payload): Json, +) -> AppResult> { + let token = headers + .get("x-eventbridge-signature-token") + .ok_or_else(|| { + AppError::Unauthorized(anyhow::anyhow!( + "Missing x-eventbridge-signature-token header" + )) + })? + .to_str() + .map_err(|_| { + AppError::Unauthorized(anyhow::anyhow!( + "Invalid x-eventbridge-signature-token header format" + )) + })? + .trim(); + + crate::auth::verify_token(token, &state.jwt_config.public_key).map_err(|err| { + AppError::Unauthorized(anyhow::anyhow!( + "JWT verification failed (x-eventbridge-signature-token): {err}" + )) + })?; + + // Parse the raw JSON into OssEventPayload + let payload: OssEventPayload = serde_json::from_value(raw_payload).map_err(|err| { + AppError::BadRequest(anyhow::anyhow!( + "Failed to parse OSS event payload: {}", + err + )) + })?; + + info!( + event = ?payload, + "Received OSS event" + ); + + let bucket_name = &payload.data.oss.bucket.name; + let object_key = &payload.data.oss.object.key; + + // Get URL template from bucket map + let url_template = state + .aliyun_config + .bucket_url_map + .get(bucket_name) + .ok_or_else(|| { + AppError::BadRequest(anyhow::anyhow!("Unsupported bucket: {}", bucket_name)) + })?; + + // Build the full URL by replacing {object_key} with the actual encoded object key + let encoded_object_key = percent_encode(object_key.as_bytes(), UNRESERVED).to_string(); + let object_url = url_template.replace("{object_key}", &encoded_object_key); + + // Create CDN client + let client = AliyunCdnClient::new(&state.aliyun_config, state.http_client.clone()); + + // Refresh the object cache + let request = RefreshObjectCachesRequest { + object_path: object_url.clone(), + object_type: Some("File".to_string()), + force: Some(false), + }; + + let response = client.refresh_object_caches(&request).await?; + + Ok(Json(OssEventResponse { + message: format!( + "CDN refresh triggered for {} in bucket {}", + object_key, bucket_name + ), + task_id: Some(response.refresh_task_id), + })) +} diff --git a/src/routes/bilibili_handlers.rs b/src/routes/bilibili_handlers.rs index 3471248..e4cbf4e 100644 --- a/src/routes/bilibili_handlers.rs +++ b/src/routes/bilibili_handlers.rs @@ -7,7 +7,7 @@ use rand::Rng; use reqwest::multipart::{Form, Part}; use serde::{Deserialize, Serialize}; use std::time::{SystemTime, UNIX_EPOCH}; -use tracing::info; +use tracing::{info, warn}; use utoipa::ToSchema; use crate::error::{AppError, AppResult}; @@ -167,7 +167,10 @@ async fn handle_create_dynamic_response( let body = resp.text().await.context("Read response failed")?; - info!("Create dynamic response: {}", body); + info!( + response_body = %body, + "Create dynamic response received" + ); let r: BilibiliCreateResponse = serde_json::from_str(&body).context("Parse create dynamic response failed")?; @@ -306,7 +309,10 @@ pub async fn create_dynamic( break; } Err(e) => { - info!("Error reading multipart field: {}", e); + warn!( + error = %e, + "Error reading multipart field" + ); break; } } @@ -323,7 +329,7 @@ pub async fn create_dynamic( // If files are present, upload them first if !files.is_empty() { - info!("Uploading {} files", files.len()); + info!(file_count = files.len(), "Uploading files"); let mut pics: Vec = Vec::new(); for (file_data, file_name, content_type) in files { diff --git a/src/routes/misc_handlers.rs b/src/routes/misc_handlers.rs index a4e52e9..2d6b8b6 100644 --- a/src/routes/misc_handlers.rs +++ b/src/routes/misc_handlers.rs @@ -1,5 +1,4 @@ -use crate::{error::AppResult, repository::Repository, state::AppState}; -use axum::{Json, debug_handler, extract::State}; +use axum::{Json, debug_handler}; use serde::Serialize; use utoipa::ToSchema; @@ -14,12 +13,3 @@ pub struct Health { pub async fn ping() -> Json { Json(Health { ok: true }) } - -/// /_health -#[debug_handler] -#[utoipa::path(get, path = "/_health", tag = "health", responses((status = OK, body = Health)))] -pub async fn health(State(state): State) -> AppResult> { - Ok(Json(Health { - ok: state.repository.health_check().await, - })) -} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 1bd9553..334214a 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,4 +1,5 @@ #![allow(clippy::needless_for_each)] +mod aliyun_handlers; mod bilibili_handlers; mod misc_handlers; use crate::{auth::jwt_auth_middleware, middleware::apply_axum_middleware, state::AppState}; @@ -12,9 +13,18 @@ use utoipa_scalar::{Scalar, Servable}; tags( (name = "health", description = "Health check endpoints"), (name = "bilibili", description = "Bilibili dynamic posting endpoints"), + (name = "aliyun", description = "Aliyun CDN API endpoints"), ), components( - schemas(bilibili_handlers::DynamicResponse) + schemas( + bilibili_handlers::DynamicResponse, + aliyun_handlers::OssEventPayload, + aliyun_handlers::OssEventResponse, + aliyun_handlers::OssEventData, + aliyun_handlers::OssData, + aliyun_handlers::OssBucket, + aliyun_handlers::OssObject, + ) ), modifiers(&SecurityAddon) )] @@ -33,25 +43,47 @@ impl utoipa::Modify for SecurityAddon { .bearer_format("JWT") .build(), ), - ) + ); + components.add_security_scheme( + "eventbridge_token", + utoipa::openapi::security::SecurityScheme::ApiKey( + utoipa::openapi::security::ApiKey::Header( + utoipa::openapi::security::ApiKeyValue::new( + "x-eventbridge-signature-token", + ), + ), + ), + ); } } } pub fn build_router(state: AppState) -> Router { - let (api_routes, mut openapi) = OpenApiRouter::with_openapi(ApiDoc::openapi()) + // Routes without JWT auth (public + custom auth) + let (public_routes, openapi_public) = OpenApiRouter::with_openapi(ApiDoc::openapi()) // Health endpoints (no auth required) .routes(routes!(misc_handlers::ping)) - .routes(routes!(misc_handlers::health)) - // Apply JWT authentication for subsequent routes + // Aliyun EventBridge endpoint with custom JWT auth via `x-eventbridge-signature-token` header + .routes(routes!(aliyun_handlers::handle_oss_events)) + .split_for_parts(); + + // Routes protected by Authorization header JWT + let (protected_routes, openapi_protected) = OpenApiRouter::new() + // Bilibili routes (protected by JWT auth) + .routes(routes!(bilibili_handlers::create_dynamic)) .route_layer(middleware::from_fn_with_state( state.clone(), jwt_auth_middleware, )) - // Bilibili routes (protected by JWT auth) - .routes(routes!(bilibili_handlers::create_dynamic)) .split_for_parts(); + // Merge OpenAPI specs + let mut openapi = openapi_public; + openapi.merge(openapi_protected); + + // Merge route handlers + let api_routes = public_routes.merge(protected_routes); + openapi.paths.paths = openapi .paths .paths diff --git a/src/state.rs b/src/state.rs index 73bd475..27e54ce 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,26 +1,18 @@ -use crate::{ - config::{AppSettings, BilibiliConfig, JwtConfig}, - repository::PostgresRepository, -}; +use crate::config::{AliyunConfig, AppSettings, BilibiliConfig, JwtConfig}; #[derive(Debug, Clone)] pub struct AppState { - pub repository: PostgresRepository, pub bilibili_config: BilibiliConfig, pub jwt_config: JwtConfig, + pub aliyun_config: AliyunConfig, pub http_client: reqwest::Client, } -pub async fn init_state_with_pg(config: &AppSettings) -> AppState { - let pool = sqlx::postgres::PgPoolOptions::new() - .connect(&config.database.uri) - .await - .expect("Failed to connect to the database"); - +pub async fn init_state(config: &AppSettings) -> AppState { AppState { - repository: PostgresRepository { pool }, bilibili_config: config.bilibili.clone(), jwt_config: config.jwt.clone(), + aliyun_config: config.aliyun.clone(), http_client: reqwest::Client::new(), } } diff --git a/src/tracing.rs b/src/tracing.rs index 4765bfe..e8fd1db 100644 --- a/src/tracing.rs +++ b/src/tracing.rs @@ -12,7 +12,7 @@ use tracing_subscriber::{ use crate::config::{LogFormat, LogLevel, LoggerConfig, SentryConfig}; -const MODULE_WHITELIST: &[&str] = &["tower_http", "sqlx::query", "my_axum_template"]; +const MODULE_WHITELIST: &[&str] = &["tower_http", "sqlx::query", "janus"]; fn init_env_filter(override_filter: Option<&String>, level: &LogLevel) -> EnvFilter { EnvFilter::try_from_default_env()