From 54aad9d841039844a69eb6b160309d3c954288a0 Mon Sep 17 00:00:00 2001 From: yuanzui-cf Date: Wed, 18 Mar 2026 09:48:11 +0800 Subject: [PATCH 01/10] chore(build): Bump rust-version to 1.88 --- Cargo.toml | 1 + Justfile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 04d41c4..5978947 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ description = "Universal Auth System for Grass Development Team." repository = "https://github.com/Grass-Development-Team/auth" license = "Apache-2.0" edition = "2024" +rust-version = "1.88" [workspace.dependencies] anyhow = "1" diff --git a/Justfile b/Justfile index cd9dd27..6dd58de 100644 --- a/Justfile +++ b/Justfile @@ -44,7 +44,7 @@ fmt-check: cargo outdated -R @msrv: - cargo msrv find + cargo msrv find --min 1.85 # Combined quality check @quality: audit outdated msrv fmt-check clippy test From d01fccbb65d949aca314c7f69099df3f25a3ece9 Mon Sep 17 00:00:00 2001 From: yuanzui-cf Date: Wed, 18 Mar 2026 12:45:55 +0800 Subject: [PATCH 02/10] chore: Switch TLS to rustls and update SeaORM features --- Cargo.lock | 518 +++++++++++++++++++++++++++++------------------------ Cargo.toml | 20 ++- 2 files changed, 299 insertions(+), 239 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc9baca..3265842 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,56 +55,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - [[package]] name = "anyhow" version = "1.0.102" @@ -225,7 +175,7 @@ dependencies = [ "sea-orm-migration", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "time", "token", "tokio", @@ -472,6 +422,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -508,52 +464,6 @@ dependencies = [ "stacker", ] -[[package]] -name = "clap" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - [[package]] name = "colored" version = "3.1.1" @@ -603,6 +513,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -668,7 +588,7 @@ dependencies = [ "rand", "sha2", "subtle", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -885,21 +805,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1414,12 +1319,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - [[package]] name = "itertools" version = "0.13.0" @@ -1435,6 +1334,28 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "js-sys" version = "0.3.91" @@ -1500,13 +1421,15 @@ dependencies = [ "httpdate", "idna", "mime", - "native-tls", "nom", "percent-encoding", "quoted_printable", + "rustls", + "rustls-platform-verifier", "socket2 0.6.3", "tokio", "url", + "webpki-roots 1.0.5", ] [[package]] @@ -1539,17 +1462,10 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ - "cc", "pkg-config", "vcpkg", ] -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - [[package]] name = "litemap" version = "0.8.1" @@ -1645,23 +1561,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "native-tls" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nom" version = "8.0.0" @@ -1758,36 +1657,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "openssl" -version = "0.10.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" +name = "openssl-probe" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-probe" @@ -1795,18 +1668,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" -[[package]] -name = "openssl-sys" -version = "0.9.112" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "ordered-float" version = "4.6.0" @@ -2146,15 +2007,18 @@ dependencies = [ "futures-util", "itertools", "itoa", - "native-tls", "num-bigint", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-native-certs 0.7.3", + "rustls-pemfile", + "rustls-pki-types", "ryu", "sha1_smol", "socket2 0.5.10", "tokio", - "tokio-native-tls", + "tokio-rustls", "tokio-util", "url", ] @@ -2339,16 +2203,99 @@ dependencies = [ ] [[package]] -name = "rustix" -version = "1.1.4" +name = "rustls" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe 0.1.6", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.7.0", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4910321ebe4151be888e35fe062169554e74aad01beafed60410131420ceffbc" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs 0.8.3", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.7.0", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -2423,7 +2370,7 @@ dependencies = [ "serde_json", "sqlx", "strum", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "url", @@ -2437,10 +2384,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c94492e2ab6c045b4cc38013809ce255d14c3d352c9f0d11e6b920e2adc948ad" dependencies = [ "chrono", - "clap", - "dotenvy", "glob", "regex", + "sea-schema", + "sqlx", + "tokio", "tracing", "tracing-subscriber", "url", @@ -2467,8 +2415,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7315c0cadb7e60fb17ee2bb282aa27d01911fc2a7e5836ec1d4ac37d19250bb4" dependencies = [ "async-trait", - "clap", - "dotenvy", "sea-orm", "sea-orm-cli", "sea-schema", @@ -2520,7 +2466,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -2531,7 +2477,9 @@ checksum = "2239ff574c04858ca77485f112afea1a15e53135d3097d0c86509cef1def1338" dependencies = [ "futures", "sea-query", + "sea-query-binder", "sea-schema-derive", + "sqlx", ] [[package]] @@ -2552,6 +2500,19 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -2559,7 +2520,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2733,7 +2694,7 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 2.0.18", "time", ] @@ -2827,21 +2788,22 @@ dependencies = [ "indexmap", "log", "memchr", - "native-tls", "once_cell", "percent-encoding", "rust_decimal", + "rustls", "serde", "serde_json", "sha2", "smallvec", - "thiserror", + "thiserror 2.0.18", "time", "tokio", "tokio-stream", "tracing", "url", "uuid", + "webpki-roots 0.26.11", ] [[package]] @@ -2922,7 +2884,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "uuid", @@ -2965,7 +2927,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "uuid", @@ -2992,7 +2954,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "url", @@ -3035,12 +2997,6 @@ dependencies = [ "unicode-properties", ] -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "strum" version = "0.26.3" @@ -3099,16 +3055,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] -name = "tempfile" -version = "3.27.0" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix", - "windows-sys 0.61.2", + "thiserror-impl 1.0.69", ] [[package]] @@ -3117,7 +3069,18 @@ version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -3205,7 +3168,7 @@ dependencies = [ "redis", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "uuid", ] @@ -3238,12 +3201,12 @@ dependencies = [ ] [[package]] -name = "tokio-native-tls" -version = "0.3.1" +name = "tokio-rustls" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "native-tls", + "rustls", "tokio", ] @@ -3520,12 +3483,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - [[package]] name = "uuid" version = "1.22.0" @@ -3679,6 +3636,33 @@ dependencies = [ "semver", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" +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" @@ -3757,6 +3741,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -3793,6 +3786,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -3824,6 +3832,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3836,6 +3850,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3848,6 +3868,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3866,6 +3892,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3878,6 +3910,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3890,6 +3928,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3902,6 +3946,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 5978947..5662f33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,14 +38,17 @@ regex = "1" minijinja = "2" sea-orm = { version = "1.1", features = [ - "sqlx-all", - "runtime-tokio-native-tls", + "sqlx-postgres", + "runtime-tokio-rustls", "macros", "with-chrono", "with-uuid", ] } -sea-orm-migration = "1.1" -redis = { version = "0.27.5", features = ["tokio-native-tls-comp"] } +sea-orm-migration = { version = "1.1", default-features = false, features = [ + "runtime-tokio-rustls", + "sqlx-postgres", +] } +redis = { version = "0.27.5", features = ["tokio-rustls-comp"] } sha2 = "0.10.9" argon2 = "0.5" @@ -57,7 +60,14 @@ subtle = "2.6" jsonwebtoken = "9.3.1" uuid = { version = "1.16.0", features = ["v4"] } -lettre = "0.11" +lettre = { version = "0.11", default-features = false, features = [ + "builder", + "hostname", + "pool", + "smtp-transport", + "rustls-tls", + "rustls-platform-verifier", +] } auth = { path = "auth" } assets = { path = "crates/assets" } From 91106f0f419d6fe899d577050644c149a764cce6 Mon Sep 17 00:00:00 2001 From: yuanzui-cf Date: Wed, 18 Mar 2026 14:00:00 +0800 Subject: [PATCH 03/10] feat(auth): Enable Redis-backed registration tokens - Introduce Redis-backed registration token issuance with Lua scripts and new RegisterTokenLease - Wire Redis token issuance into registration flow; pass redis handle to email verification - Define TTL constants for token and reuse window; expose Lease type in token module - Update email template to include verification link and expiry information --- auth/src/routers/controllers/auth.rs | 16 ++- auth/src/services/auth/register.rs | 89 ++++++++++--- .../assets/templates/mails/register.html | 18 ++- crates/token/src/services/mod.rs | 2 +- crates/token/src/services/register_token.rs | 125 +++++++++++++++++- 5 files changed, 225 insertions(+), 25 deletions(-) diff --git a/auth/src/routers/controllers/auth.rs b/auth/src/routers/controllers/auth.rs index 24a963e..c7c81e1 100644 --- a/auth/src/routers/controllers/auth.rs +++ b/auth/src/routers/controllers/auth.rs @@ -3,6 +3,7 @@ use axum::{ http::StatusCode, }; use axum_extra::extract::CookieJar; +use redis::aio::MultiplexedConnection; use token::services::SessionService; use crate::{ @@ -23,8 +24,21 @@ pub async fn register( State(state): State, Json(req): Json, ) -> Response { + let mut redis: Option = if state.mail.is_some() + && let Ok(redis) = state.redis.get_multiplexed_tokio_connection().await + { + Some(redis) + } else { + None + }; + match req - .register(&state.db, &state.config, state.mail.as_deref()) + .register( + &state.db, + &state.config, + state.mail.as_deref(), + redis.as_mut(), + ) .await { Ok(message) => Response::new( diff --git a/auth/src/services/auth/register.rs b/auth/src/services/auth/register.rs index 5daa669..7e5202d 100644 --- a/auth/src/services/auth/register.rs +++ b/auth/src/services/auth/register.rs @@ -2,9 +2,11 @@ use std::sync::OnceLock; use crypto::password::{PasswordHashAlgorithm, PasswordManager}; use minijinja::context; +use redis::aio::MultiplexedConnection; use regex::Regex; use sea_orm::{DatabaseConnection, TransactionError, TransactionTrait}; use serde::Deserialize; +use token::services::RegisterTokenService; use validator::Validatable; use crate::{ @@ -17,6 +19,8 @@ use crate::{ }; static EMAIL_RE: OnceLock = OnceLock::new(); +const REGISTER_TOKEN_TTL_SECONDS: u64 = 60 * 60; +const REGISTER_TOKEN_REUSE_MIN_TTL_SECONDS: u64 = 60; #[derive(Deserialize)] pub struct RegisterService { @@ -32,6 +36,7 @@ impl RegisterService { conn: &DatabaseConnection, config: &Config, mailer: Option<&Mailer>, + redis: Option<&mut MultiplexedConnection>, ) -> Result { if !config.site.enable_registration { return Err(AppError::biz( @@ -42,15 +47,21 @@ impl RegisterService { self.validate()?; - if let Ok(user) = users::get_user_by_email(conn, &self.email).await { + if let Ok((user, _, _)) = users::get_user_by_email(conn, &self.email).await { if let Some(mailer) = mailer - && user.0.status.is_inactive() + && user.status.is_inactive() { - // TODO: Check if the verification token has expired - return match self.send_verification_email(mailer, config).await { - Ok(_) => Ok("Verification email sent successfully".into()), - Err(err) => Err(err), - }; + Self::send_verification_email( + mailer, + redis, + config, + user.uid, + &user.username, + &user.email, + ) + .await?; + + return Ok("Verification email sent successfully".into()); } return Err(AppError::biz( @@ -122,31 +133,77 @@ impl RegisterService { } if let Some(mailer) = mailer { - return match self.send_verification_email(mailer, config).await { - Ok(_) => Ok("Verification email sent successfully".into()), - Err(err) => Err(err), - }; + let (user, _, _) = + users::get_user_by_email(conn, &self.email) + .await + .map_err(|err| { + AppError::infra( + AppErrorKind::InternalError, + "auth.register.find_created_user", + err, + ) + })?; + + Self::send_verification_email( + mailer, + redis, + config, + user.uid, + &user.username, + &user.email, + ) + .await?; + return Ok("Verification email sent successfully".into()); } Ok("Register successfully".into()) } async fn send_verification_email( - &self, mailer: &Mailer, + redis: Option<&mut MultiplexedConnection>, config: &Config, + uid: i32, + username: &str, + email: &str, ) -> Result<(), AppError> { - // TODO: Send verification link + let Some(redis) = redis else { + return Err(AppError::infra( + AppErrorKind::InternalError, + "auth.register.send_verification_email", + anyhow::anyhow!("Redis connection not available"), + )); + }; + + let token = RegisterTokenService::issue_or_reuse_for_user( + redis, + uid, + email, + REGISTER_TOKEN_TTL_SECONDS, + REGISTER_TOKEN_REUSE_MIN_TTL_SECONDS, + ) + .await + .map_err(|err| { + AppError::infra( + AppErrorKind::InternalError, + "auth.register.issue_or_reuse_token", + err, + ) + })?; + let expires_minutes = token.ttl_secs.saturating_add(59) / 60; + match mailer .send_mail( - &self.email, + email, "Account registration received", "register", context! { - username => self.username.clone(), - email => self.email.clone(), + username => username.to_owned(), + email => email.to_owned(), domain => config.domain.clone(), site_name => config.site.name.clone(), + verification_token => token.token, + expires_minutes => expires_minutes, }, ) .await diff --git a/crates/assets/assets/templates/mails/register.html b/crates/assets/assets/templates/mails/register.html index 9237382..11d0aee 100644 --- a/crates/assets/assets/templates/mails/register.html +++ b/crates/assets/assets/templates/mails/register.html @@ -70,12 +70,18 @@ website.

- Service URL: - {{ domain }} + Verification link: + + {{ domain + }}/actions/verify_email?token={{ + verification_token }} + +

+

+ This token expires in + {{ expires_minutes }} minutes.

If you did not create this account, you can diff --git a/crates/token/src/services/mod.rs b/crates/token/src/services/mod.rs index e50f467..bf6ae85 100644 --- a/crates/token/src/services/mod.rs +++ b/crates/token/src/services/mod.rs @@ -3,5 +3,5 @@ pub mod register_token; pub mod session; pub use password_reset::{PasswordResetToken, PasswordResetTokenService}; -pub use register_token::{RegisterToken, RegisterTokenService}; +pub use register_token::{RegisterToken, RegisterTokenLease, RegisterTokenService}; pub use session::{Session, SessionLookup, SessionService}; diff --git a/crates/token/src/services/register_token.rs b/crates/token/src/services/register_token.rs index 80a2c22..5992d38 100644 --- a/crates/token/src/services/register_token.rs +++ b/crates/token/src/services/register_token.rs @@ -1,8 +1,78 @@ +use std::sync::OnceLock; + use redis::aio::MultiplexedConnection; use serde::{Deserialize, Serialize}; use crate::{TokenError, TokenStore}; +const ISSUE_OR_REUSE_SCRIPT: &str = r#" +local index_key = KEYS[1] +local token_prefix = ARGV[1] +local uid = tonumber(ARGV[2]) +local email = ARGV[3] +local ttl_secs = tonumber(ARGV[4]) +local min_reuse_ttl_secs = tonumber(ARGV[5]) +local new_token = ARGV[6] + +local existing = redis.call('GET', index_key) +if existing then + local existing_key = token_prefix .. '::' .. existing + local payload = redis.call('GET', existing_key) + if payload then + local ok, decoded = pcall(cjson.decode, payload) + if ok and decoded and tonumber(decoded.uid) == uid and decoded.email == email then + local ttl = redis.call('TTL', existing_key) + if ttl > min_reuse_ttl_secs then + local index_ttl = redis.call('TTL', index_key) + if index_ttl < ttl then + redis.call('EXPIRE', index_key, ttl) + end + return {existing, ttl} + end + end + end + redis.call('DEL', existing_key) +end + +local payload = cjson.encode({ uid = uid, email = email }) +local new_key = token_prefix .. '::' .. new_token +redis.call('SETEX', new_key, ttl_secs, payload) +redis.call('SETEX', index_key, ttl_secs, new_token) +return {new_token, ttl_secs} +"#; + +const CONSUME_AND_CLEAR_INDEX_SCRIPT: &str = r#" +local token_key = KEYS[1] +local index_prefix = ARGV[1] +local token = ARGV[2] + +local payload = redis.call('GETDEL', token_key) +if not payload then + return nil +end + +local ok, decoded = pcall(cjson.decode, payload) +if ok and decoded and decoded.uid ~= nil then + local index_key = index_prefix .. '::' .. tostring(decoded.uid) + local indexed_token = redis.call('GET', index_key) + if indexed_token == token then + redis.call('DEL', index_key) + end +end + +return payload +"#; + +fn issue_or_reuse_script() -> &'static redis::Script { + static SCRIPT: OnceLock = OnceLock::new(); + SCRIPT.get_or_init(|| redis::Script::new(ISSUE_OR_REUSE_SCRIPT)) +} + +fn consume_and_clear_index_script() -> &'static redis::Script { + static SCRIPT: OnceLock = OnceLock::new(); + SCRIPT.get_or_init(|| redis::Script::new(CONSUME_AND_CLEAR_INDEX_SCRIPT)) +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RegisterToken { pub uid: i32, @@ -12,6 +82,12 @@ pub struct RegisterToken { #[derive(Debug, Clone, Copy, Default)] pub struct RegisterTokenService; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RegisterTokenLease { + pub token: String, + pub ttl_secs: u64, +} + #[async_trait::async_trait] impl TokenStore for RegisterTokenService { type Payload = RegisterToken; @@ -20,6 +96,16 @@ impl TokenStore for RegisterTokenService { } impl RegisterTokenService { + const INDEX_PREFIX: &'static str = "register-token-index"; + + fn index_key(uid: i32) -> String { + format!("{}::{uid}", Self::INDEX_PREFIX) + } + + fn token_key(token: &str) -> String { + format!("{}::{token}", Self::PREFIX) + } + pub async fn issue_for_user( redis: &mut MultiplexedConnection, uid: i32, @@ -33,10 +119,47 @@ impl RegisterTokenService { ::issue(redis, &token, ttl_secs).await } + pub async fn issue_or_reuse_for_user( + redis: &mut MultiplexedConnection, + uid: i32, + email: impl Into, + ttl_secs: u64, + min_reuse_ttl_secs: u64, + ) -> Result { + let email = email.into(); + let new_token = uuid::Uuid::new_v4().to_string(); + let index_key = Self::index_key(uid); + let mut invocation = issue_or_reuse_script().prepare_invoke(); + invocation + .key(index_key) + .arg(Self::PREFIX) + .arg(uid) + .arg(email) + .arg(ttl_secs) + .arg(min_reuse_ttl_secs) + .arg(new_token); + let (token, ttl_secs): (String, i64) = invocation.invoke_async(redis).await?; + + Ok(RegisterTokenLease { + token, + ttl_secs: ttl_secs.max(0) as u64, + }) + } + pub async fn consume( redis: &mut MultiplexedConnection, token: &str, ) -> Result, TokenError> { - ::consume(redis, token).await + let mut invocation = consume_and_clear_index_script().prepare_invoke(); + invocation + .key(Self::token_key(token)) + .arg(Self::INDEX_PREFIX) + .arg(token); + let payload: Option = invocation.invoke_async(redis).await?; + + payload + .map(|payload| serde_json::from_str(&payload)) + .transpose() + .map_err(Into::into) } } From a4c7d2149715515c6dbe328c11f4a210b530fa6b Mon Sep 17 00:00:00 2001 From: yuanzui-cf Date: Thu, 19 Mar 2026 11:58:02 +0800 Subject: [PATCH 04/10] feat(model): Add update_status method for User model --- auth/src/models/users.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/auth/src/models/users.rs b/auth/src/models/users.rs index 59d8688..eedbb93 100644 --- a/auth/src/models/users.rs +++ b/auth/src/models/users.rs @@ -348,6 +348,18 @@ impl Model { user.update(conn).await.map_err(ModelError::DBError) } + + pub async fn update_status( + &self, + conn: &impl ConnectionTrait, + status: AccountStatus, + ) -> Result { + let mut user = self.clone().into_active_model(); + + user.status = Set(status); + + user.update(conn).await.map_err(ModelError::DBError) + } } impl AccountStatus { From abf662a185abf119fdd58808a0d6ae07ce0a9264 Mon Sep 17 00:00:00 2001 From: yuanzui-cf Date: Thu, 19 Mar 2026 11:59:11 +0800 Subject: [PATCH 05/10] refactor(error): Add with_op for AppError --- auth/src/internal/error.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/auth/src/internal/error.rs b/auth/src/internal/error.rs index f04a1e9..ba244e2 100644 --- a/auth/src/internal/error.rs +++ b/auth/src/internal/error.rs @@ -31,21 +31,21 @@ pub struct AppError { } impl AppError { - pub fn new(kind: AppErrorKind, op: &'static str) -> Self { + pub fn new(kind: AppErrorKind) -> Self { Self { kind, - op, + op: "", detail: None, source: None, } } pub fn biz(kind: AppErrorKind, op: &'static str) -> Self { - Self::new(kind, op) + Self::new(kind).with_op(op) } pub fn infra(kind: AppErrorKind, op: &'static str, source: impl Into) -> Self { - Self::new(kind, op).with_source(source) + Self::new(kind).with_op(op).with_source(source) } pub fn with_detail(mut self, detail: impl Into) -> Self { @@ -58,6 +58,11 @@ impl AppError { self } + pub fn with_op(mut self, op: &'static str) -> Self { + self.op = op; + self + } + pub fn source_ref(&self) -> Option<&(dyn StdError + 'static)> { self.source.as_ref().map(|err| err.as_ref()) } From 454f3ab1d4c6d5a188d7c75b7b5f64b3c306dc30 Mon Sep 17 00:00:00 2001 From: yuanzui-cf Date: Thu, 19 Mar 2026 11:59:20 +0800 Subject: [PATCH 06/10] feat(model): Add From trait for AppError --- auth/src/models/common.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/auth/src/models/common.rs b/auth/src/models/common.rs index d195004..f077ff1 100644 --- a/auth/src/models/common.rs +++ b/auth/src/models/common.rs @@ -2,6 +2,8 @@ use crypto::password::PasswordError; use sea_orm::DbErr; use thiserror::Error; +use crate::internal::error::{AppError, AppErrorKind}; + #[derive(Debug, Error)] pub enum ModelError { #[error("Database error: {0}")] @@ -15,3 +17,9 @@ pub enum ModelError { #[error("Model error: {0}")] Custom(String), } + +impl From for AppError { + fn from(value: ModelError) -> Self { + AppError::new(AppErrorKind::InternalError).with_source(value) + } +} From 9cc1187060257721e317420580bdcfea4b01b85b Mon Sep 17 00:00:00 2001 From: yuanzui-cf Date: Thu, 19 Mar 2026 12:00:06 +0800 Subject: [PATCH 07/10] feat(error): Add TokenInvalid error for AppError & Serializer --- auth/src/internal/error.rs | 17 ++++++++++++----- auth/src/models/common.rs | 14 +++++++++++++- auth/src/routers/response.rs | 2 ++ auth/src/routers/serializer/common.rs | 19 +++++++++++++++---- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/auth/src/internal/error.rs b/auth/src/internal/error.rs index ba244e2..457362c 100644 --- a/auth/src/internal/error.rs +++ b/auth/src/internal/error.rs @@ -2,6 +2,7 @@ use std::{error::Error as StdError, fmt::Display}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AppErrorKind { + Undefined, BadRequest, Unauthorized, Forbidden, @@ -20,6 +21,7 @@ pub enum AppErrorKind { UserDeleted, DuplicatePassword, VerificationEmailSendFailed, + TokenInvalid, } #[derive(Debug)] @@ -31,21 +33,26 @@ pub struct AppError { } impl AppError { - pub fn new(kind: AppErrorKind) -> Self { + pub fn new() -> Self { Self { - kind, - op: "", + kind: AppErrorKind::Undefined, + op: "", detail: None, source: None, } } pub fn biz(kind: AppErrorKind, op: &'static str) -> Self { - Self::new(kind).with_op(op) + Self::new().with_kind(kind).with_op(op) } pub fn infra(kind: AppErrorKind, op: &'static str, source: impl Into) -> Self { - Self::new(kind).with_op(op).with_source(source) + Self::new().with_kind(kind).with_op(op).with_source(source) + } + + pub fn with_kind(mut self, kind: AppErrorKind) -> Self { + self.kind = kind; + self } pub fn with_detail(mut self, detail: impl Into) -> Self { diff --git a/auth/src/models/common.rs b/auth/src/models/common.rs index f077ff1..0d16e07 100644 --- a/auth/src/models/common.rs +++ b/auth/src/models/common.rs @@ -20,6 +20,18 @@ pub enum ModelError { impl From for AppError { fn from(value: ModelError) -> Self { - AppError::new(AppErrorKind::InternalError).with_source(value) + match value { + ModelError::DBError(err) => AppError::new() + .with_kind(AppErrorKind::InternalError) + .with_source(err), + ModelError::PasswordError(err) => AppError::new() + .with_kind(AppErrorKind::ParamError) + .with_detail(err.to_string()), + ModelError::ParamsError => AppError::new().with_kind(AppErrorKind::ParamError), + ModelError::Empty => AppError::new().with_kind(AppErrorKind::NotFound), + ModelError::Custom(msg) => AppError::new() + .with_kind(AppErrorKind::InternalError) + .with_detail(msg), + } } } diff --git a/auth/src/routers/response.rs b/auth/src/routers/response.rs index b0320b8..67b2261 100644 --- a/auth/src/routers/response.rs +++ b/auth/src/routers/response.rs @@ -6,6 +6,7 @@ use crate::{ impl From for ResponseCode { fn from(value: AppErrorKind) -> Self { match value { + AppErrorKind::Undefined => ResponseCode::InternalError, AppErrorKind::BadRequest => ResponseCode::BadRequest, AppErrorKind::Unauthorized => ResponseCode::Unauthorized, AppErrorKind::Forbidden => ResponseCode::Forbidden, @@ -24,6 +25,7 @@ impl From for ResponseCode { AppErrorKind::UserDeleted => ResponseCode::UserDeleted, AppErrorKind::DuplicatePassword => ResponseCode::DuplicatePassword, AppErrorKind::VerificationEmailSendFailed => ResponseCode::VerificationEmailSendFailed, + AppErrorKind::TokenInvalid => ResponseCode::TokenInvalid, } } } diff --git a/auth/src/routers/serializer/common.rs b/auth/src/routers/serializer/common.rs index 9eae144..b0144bf 100644 --- a/auth/src/routers/serializer/common.rs +++ b/auth/src/routers/serializer/common.rs @@ -48,7 +48,8 @@ pub enum ResponseCode { EmailExists, // 4016 UserDeleted, // 4017 DuplicatePassword, // 4018 - VerificationEmailSendFailed, // 4020 + VerificationEmailSendFailed, // 4019 + TokenInvalid, // 4020 } impl ResponseCode { @@ -74,6 +75,7 @@ impl ResponseCode { ResponseCode::DuplicatePassword => StatusCode::CONFLICT, // Registration already succeeded, but sending verification email failed. ResponseCode::VerificationEmailSendFailed => StatusCode::OK, + ResponseCode::TokenInvalid => StatusCode::UNAUTHORIZED, } } } @@ -92,7 +94,8 @@ fn status_from_code(code: u16) -> StatusCode { 4016 => StatusCode::CONFLICT, 4017 => StatusCode::NOT_FOUND, 4018 => StatusCode::CONFLICT, - 4020 => StatusCode::OK, + 4019 => StatusCode::OK, + 4020 => StatusCode::UNAUTHORIZED, _ => StatusCode::from_u16(code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), } } @@ -119,7 +122,8 @@ impl From for u16 { ResponseCode::EmailExists => 4016, ResponseCode::UserDeleted => 4017, ResponseCode::DuplicatePassword => 4018, - ResponseCode::VerificationEmailSendFailed => 4020, + ResponseCode::VerificationEmailSendFailed => 4019, + ResponseCode::TokenInvalid => 4020, } } } @@ -149,6 +153,7 @@ impl From for String { ResponseCode::VerificationEmailSendFailed => { "Account created, but verification email could not be sent".into() }, + ResponseCode::TokenInvalid => "Token is invalid".into(), } } } @@ -249,11 +254,17 @@ mod tests { ); let partial_ok = Response::<()>::new( - 4020, + 4019, "Account created, but verification email could not be sent".into(), None, ); assert_eq!(partial_ok.into_response().status(), StatusCode::OK); + + let token_invalid = Response::<()>::new(4020, "Token is invalid".into(), None); + assert_eq!( + token_invalid.into_response().status(), + StatusCode::UNAUTHORIZED + ); } #[test] From c2371c8514a37b3c49e6b55692cf3746a6486984 Mon Sep 17 00:00:00 2001 From: yuanzui-cf Date: Thu, 19 Mar 2026 12:00:29 +0800 Subject: [PATCH 08/10] feat(service): Add verify_email service --- auth/src/routers.rs | 1 + auth/src/routers/controllers/auth.rs | 24 +++++++++++++ auth/src/services/auth/mod.rs | 2 ++ auth/src/services/auth/verify_email.rs | 49 ++++++++++++++++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 auth/src/services/auth/verify_email.rs diff --git a/auth/src/routers.rs b/auth/src/routers.rs index 5564c7e..a027e90 100644 --- a/auth/src/routers.rs +++ b/auth/src/routers.rs @@ -65,6 +65,7 @@ pub fn get_router(app: Router, config: &Config) -> Router { .route("/login", post(auth::login)) .route("/logout", any(auth::logout)) .route("/register", post(auth::register)) + .route("/verify-email", post(auth::verify_email)) .route("/forget-password", post(auth::forget_password)) .route("/reset-password", get(auth::reset_password)) .route( diff --git a/auth/src/routers/controllers/auth.rs b/auth/src/routers/controllers/auth.rs index c7c81e1..a230a11 100644 --- a/auth/src/routers/controllers/auth.rs +++ b/auth/src/routers/controllers/auth.rs @@ -239,3 +239,27 @@ pub async fn forget_password( Err(err) => app_error_to_response(err), } } + +pub async fn verify_email( + State(state): State, + Json(req): Json, +) -> Response { + let mut redis = match state.redis.get_multiplexed_tokio_connection().await { + Ok(redis) => redis, + Err(err) => { + return app_error_to_response( + AppError::infra( + AppErrorKind::InternalError, + "auth.controller.verify_email.redis", + err, + ) + .with_detail("Unable to connect to redis"), + ); + }, + }; + + match req.verify_email(&state.db, &mut redis).await { + Ok(_) => Response::new(ResponseCode::OK.into(), ResponseCode::OK.into(), None), + Err(err) => app_error_to_response(err), + } +} diff --git a/auth/src/services/auth/mod.rs b/auth/src/services/auth/mod.rs index 4216b16..3c3cf1b 100644 --- a/auth/src/services/auth/mod.rs +++ b/auth/src/services/auth/mod.rs @@ -2,8 +2,10 @@ mod forget_password; mod login; mod register; mod reset_password; +mod verify_email; pub use forget_password::*; pub use login::*; pub use register::*; pub use reset_password::*; +pub use verify_email::*; diff --git a/auth/src/services/auth/verify_email.rs b/auth/src/services/auth/verify_email.rs new file mode 100644 index 0000000..3e7afa0 --- /dev/null +++ b/auth/src/services/auth/verify_email.rs @@ -0,0 +1,49 @@ +use redis::aio::MultiplexedConnection; +use sea_orm::DatabaseConnection; +use serde::Deserialize; +use token::services::RegisterTokenService; + +use crate::{ + internal::error::{AppError, AppErrorKind}, + models::users, +}; + +#[derive(Deserialize)] +pub struct VerifyEmailService { + pub token: String, +} + +impl VerifyEmailService { + pub async fn verify_email( + &self, + conn: &DatabaseConnection, + redis: &mut MultiplexedConnection, + ) -> Result<(), AppError> { + let res = RegisterTokenService::consume(redis, &self.token).await; + + match res { + Ok(Some(token)) => { + let (user, _, _) = users::get_user_by_id(conn, token.uid) + .await + .map_err(|err| AppError::from(err).with_op("auth.verify_email.find_user"))?; + + user.update_status(conn, users::AccountStatus::Active) + .await + .map_err(|err| { + AppError::from(err).with_op("auth.verify_email.update_status") + })?; + + Ok(()) + }, + Ok(None) => Err(AppError::biz( + AppErrorKind::TokenInvalid, + "auth.verify_email.consume", + )), + Err(err) => Err(AppError::infra( + AppErrorKind::InternalError, + "auth.verify_email.consume", + err, + )), + } + } +} From 38a10ab1661f47e61dc4ecfbc2d583cb91164698 Mon Sep 17 00:00:00 2001 From: yuanzui-cf Date: Fri, 20 Mar 2026 01:44:43 +0800 Subject: [PATCH 09/10] fix(auth): harden register/verify-email redis-token flow --- auth/src/routers/controllers/auth.rs | 18 +++++-- auth/src/services/auth/register.rs | 9 ++++ auth/src/services/auth/verify_email.rs | 67 +++++++++++++++++--------- 3 files changed, 67 insertions(+), 27 deletions(-) diff --git a/auth/src/routers/controllers/auth.rs b/auth/src/routers/controllers/auth.rs index a230a11..509afa1 100644 --- a/auth/src/routers/controllers/auth.rs +++ b/auth/src/routers/controllers/auth.rs @@ -24,10 +24,20 @@ pub async fn register( State(state): State, Json(req): Json, ) -> Response { - let mut redis: Option = if state.mail.is_some() - && let Ok(redis) = state.redis.get_multiplexed_tokio_connection().await - { - Some(redis) + let mut redis: Option = if state.mail.is_some() { + match state.redis.get_multiplexed_tokio_connection().await { + Ok(redis) => Some(redis), + Err(err) => { + return app_error_to_response( + AppError::infra( + AppErrorKind::InternalError, + "auth.controller.register.redis", + err, + ) + .with_detail("Unable to connect to redis"), + ); + }, + } } else { None }; diff --git a/auth/src/services/auth/register.rs b/auth/src/services/auth/register.rs index 7e5202d..ed127f8 100644 --- a/auth/src/services/auth/register.rs +++ b/auth/src/services/auth/register.rs @@ -45,6 +45,15 @@ impl RegisterService { )); } + if mailer.is_some() && redis.is_none() { + return Err(AppError::infra( + AppErrorKind::InternalError, + "auth.register.precheck_redis", + anyhow::anyhow!("Redis connection not available"), + ) + .with_detail("Unable to connect to redis")); + } + self.validate()?; if let Ok((user, _, _)) = users::get_user_by_email(conn, &self.email).await { diff --git a/auth/src/services/auth/verify_email.rs b/auth/src/services/auth/verify_email.rs index 3e7afa0..f849fda 100644 --- a/auth/src/services/auth/verify_email.rs +++ b/auth/src/services/auth/verify_email.rs @@ -1,7 +1,7 @@ use redis::aio::MultiplexedConnection; use sea_orm::DatabaseConnection; use serde::Deserialize; -use token::services::RegisterTokenService; +use token::{TokenStore, services::RegisterTokenService}; use crate::{ internal::error::{AppError, AppErrorKind}, @@ -19,31 +19,52 @@ impl VerifyEmailService { conn: &DatabaseConnection, redis: &mut MultiplexedConnection, ) -> Result<(), AppError> { - let res = RegisterTokenService::consume(redis, &self.token).await; - - match res { - Ok(Some(token)) => { - let (user, _, _) = users::get_user_by_id(conn, token.uid) - .await - .map_err(|err| AppError::from(err).with_op("auth.verify_email.find_user"))?; + let token = ::get(redis, &self.token) + .await + .map_err(|err| { + AppError::infra( + AppErrorKind::InternalError, + "auth.verify_email.get_token", + err, + ) + })?; + let Some(token) = token else { + return Err(AppError::biz( + AppErrorKind::TokenInvalid, + "auth.verify_email.get_token", + )); + }; - user.update_status(conn, users::AccountStatus::Active) - .await - .map_err(|err| { - AppError::from(err).with_op("auth.verify_email.update_status") - })?; + let (user, _, _) = users::get_user_by_id(conn, token.uid) + .await + .map_err(|err| AppError::from(err).with_op("auth.verify_email.find_user"))?; + if user.email != token.email { + return Err(AppError::biz( + AppErrorKind::TokenInvalid, + "auth.verify_email.validate_email", + )); + } - Ok(()) - }, - Ok(None) => Err(AppError::biz( + if user.status.is_inactive() { + user.update_status(conn, users::AccountStatus::Active) + .await + .map_err(|err| AppError::from(err).with_op("auth.verify_email.update_status"))?; + } else if !matches!(user.status, users::AccountStatus::Active) { + return Err(AppError::biz( AppErrorKind::TokenInvalid, - "auth.verify_email.consume", - )), - Err(err) => Err(AppError::infra( - AppErrorKind::InternalError, - "auth.verify_email.consume", - err, - )), + "auth.verify_email.validate_status", + )); } + + if let Err(err) = RegisterTokenService::consume(redis, &self.token).await { + tracing::warn!( + uid = token.uid, + token = %self.token, + error = %err, + "verify-email token cleanup failed after state update" + ); + } + + Ok(()) } } From d413b759475eb149869761124c9752dbaca78e58 Mon Sep 17 00:00:00 2001 From: yuanzui-cf Date: Fri, 27 Mar 2026 00:31:09 +0800 Subject: [PATCH 10/10] feat(auth): Add verify-email action page --- auth/src/routers.rs | 17 +- auth/src/routers/controllers.rs | 1 + auth/src/routers/controllers/actions.rs | 35 ++++ auth/src/services/actions/mod.rs | 3 + auth/src/services/actions/verify_email.rs | 81 ++++++++ auth/src/services/mod.rs | 1 + .../templates/actions/verify_email.html | 178 ++++++++++++++++++ .../assets/templates/mails/register.html | 2 +- 8 files changed, 315 insertions(+), 3 deletions(-) create mode 100644 auth/src/routers/controllers/actions.rs create mode 100644 auth/src/services/actions/mod.rs create mode 100644 auth/src/services/actions/verify_email.rs create mode 100644 crates/assets/assets/templates/actions/verify_email.html diff --git a/auth/src/routers.rs b/auth/src/routers.rs index a027e90..b50ebac 100644 --- a/auth/src/routers.rs +++ b/auth/src/routers.rs @@ -20,7 +20,7 @@ use tower_http::{cors, cors::CorsLayer}; use crate::{ internal::config::Config, routers::{ - controllers::{auth, common, users}, + controllers::{actions, auth, common, users}, middleware::permission::PermissionAccess, utils::content_type, }, @@ -58,6 +58,16 @@ pub fn get_router(app: Router, config: &Config) -> Router { Router::new().nest("/oauth", oauth) }; + let action = { + let route = Router::new().route("/verify-email", get(actions::verify_email)); + let route = Router::new().nest("/actions", route); + if config.dev_mode { + route.layer(public_cors.clone()) + } else { + route.layer(internal_cors.clone()) + } + }; + let api_v1 = { // Auth let auth = { @@ -143,7 +153,10 @@ pub fn get_router(app: Router, config: &Config) -> Router { Router::new().nest("/api", route) }; - app.merge(api).merge(oauth).fallback(static_asset_fallback) + app.merge(api) + .merge(oauth) + .merge(action) + .fallback(static_asset_fallback) } async fn static_asset_fallback(request: Request) -> impl IntoResponse { diff --git a/auth/src/routers/controllers.rs b/auth/src/routers/controllers.rs index 8e61fd9..8283a8e 100644 --- a/auth/src/routers/controllers.rs +++ b/auth/src/routers/controllers.rs @@ -1,3 +1,4 @@ +pub mod actions; pub mod auth; pub mod common; pub mod users; diff --git a/auth/src/routers/controllers/actions.rs b/auth/src/routers/controllers/actions.rs new file mode 100644 index 0000000..ca26806 --- /dev/null +++ b/auth/src/routers/controllers/actions.rs @@ -0,0 +1,35 @@ +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::{Html, IntoResponse, Response as AxumResponse}, +}; + +use crate::{services::actions::ActionsVerifyEmailService, state::AppState}; + +pub async fn verify_email( + State(state): State, + Query(req): Query, +) -> AxumResponse { + match req.render_verify_email_page(&state.config) { + Ok(html) => Html(html).into_response(), + Err(err) => { + let source = err.source_ref().map(ToString::to_string); + tracing::error!( + op = err.op, + kind = ?err.kind, + detail = ?err.detail, + source = ?source, + "failed to render verify-email action page" + ); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Html( + "Verification \ + Error

Unable to load verification page. Please try \ + again later.

", + ), + ) + .into_response() + }, + } +} diff --git a/auth/src/services/actions/mod.rs b/auth/src/services/actions/mod.rs new file mode 100644 index 0000000..7b3c454 --- /dev/null +++ b/auth/src/services/actions/mod.rs @@ -0,0 +1,3 @@ +pub mod verify_email; + +pub use verify_email::*; diff --git a/auth/src/services/actions/verify_email.rs b/auth/src/services/actions/verify_email.rs new file mode 100644 index 0000000..75a6d2f --- /dev/null +++ b/auth/src/services/actions/verify_email.rs @@ -0,0 +1,81 @@ +use anyhow::anyhow; +use assets::AssetManager; +use minijinja::{AutoEscape, Environment, context}; +use serde::Deserialize; + +use crate::internal::{ + config::Config, + error::{AppError, AppErrorKind}, +}; + +#[derive(Deserialize)] +pub struct ActionsVerifyEmailService { + #[serde(default)] + pub email: String, + #[serde(default)] + pub token: String, +} + +impl ActionsVerifyEmailService { + pub fn render_verify_email_page(&self, config: &Config) -> Result { + let token = self.token.trim().to_owned(); + let initial_error = if token.is_empty() { + String::from("Invalid verification link.") + } else { + String::new() + }; + let verify_api = format!( + "{}/api/v1/auth/verify-email", + config.domain.trim_end_matches('/') + ); + + let file = AssetManager::get("templates/actions/verify_email.html").ok_or_else(|| { + AppError::infra( + AppErrorKind::InternalError, + "actions.verify_email.load_template", + anyhow!("templates/actions/verify_email.html not found"), + ) + })?; + let source = String::from_utf8(file.data.into_owned()).map_err(|err| { + AppError::infra( + AppErrorKind::InternalError, + "actions.verify_email.read_template", + err, + ) + })?; + + let mut env = Environment::new(); + env.set_auto_escape_callback(|_| AutoEscape::Html); + env.add_template("actions.verify-email", &source) + .map_err(|err| { + AppError::infra( + AppErrorKind::InternalError, + "actions.verify_email.parse_template", + err, + ) + })?; + + env.get_template("actions.verify-email") + .map_err(|err| { + AppError::infra( + AppErrorKind::InternalError, + "actions.verify_email.get_template", + err, + ) + })? + .render(context! { + token => token, + email => self.email.clone(), + verify_api => verify_api, + site_name => config.site.name.clone(), + initial_error => initial_error, + }) + .map_err(|err| { + AppError::infra( + AppErrorKind::InternalError, + "actions.verify_email.render_template", + err, + ) + }) + } +} diff --git a/auth/src/services/mod.rs b/auth/src/services/mod.rs index 5995b7b..c935ec5 100644 --- a/auth/src/services/mod.rs +++ b/auth/src/services/mod.rs @@ -1,2 +1,3 @@ +pub mod actions; pub mod auth; pub mod users; diff --git a/crates/assets/assets/templates/actions/verify_email.html b/crates/assets/assets/templates/actions/verify_email.html new file mode 100644 index 0000000..9dfc58d --- /dev/null +++ b/crates/assets/assets/templates/actions/verify_email.html @@ -0,0 +1,178 @@ + + + + + + Verify Email - {{ site_name }} + + + +
+

Verify your email

+ {% if email %} +

Confirm verification for {{ email }}.

+ {% else %} +

Confirm verification for your account.

+ {% endif %} +

+ This page does not auto-verify. Click the button below to + continue. +

+ +
+ +
+ +
+

+
+ + + + diff --git a/crates/assets/assets/templates/mails/register.html b/crates/assets/assets/templates/mails/register.html index 11d0aee..a3242f5 100644 --- a/crates/assets/assets/templates/mails/register.html +++ b/crates/assets/assets/templates/mails/register.html @@ -73,7 +73,7 @@ Verification link: {{ domain - }}/actions/verify_email?token={{ + }}/actions/verify-email?token={{ verification_token }}