From 43f3867630214bf05c66b6fbb863f6581335a70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E5=A4=A9?= Date: Wed, 7 Feb 2024 17:34:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0jwt=E3=80=81validator?= =?UTF-8?q?=E3=80=81bcrypto=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=8E=A5=E5=8F=A3=E3=80=81=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E5=99=A8=E4=BB=A5=E5=8F=8A=E4=B8=AD=E9=97=B4?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 250 +++++++++++++++++++++++----- Cargo.toml | 7 +- config/default.toml | 3 + config/dev.toml | 6 - src/common/err.rs | 31 +++- src/common/extract/authorization.rs | 37 ++++ src/common/extract/mod.rs | 1 + src/common/jwt/mod.rs | 65 ++++++++ src/common/mod.rs | 4 + src/common/pagination.rs | 15 ++ src/common/response.rs | 49 +++++- src/common/util/datetime_format.rs | 28 ++++ src/common/util/mod.rs | 1 + src/config/app.rs | 1 + src/entity/mod.rs | 3 +- src/entity/users.rs | 22 +++ src/form.rs | 11 ++ src/handler/article.rs | 10 +- src/handler/category.rs | 19 +-- src/handler/mod.rs | 1 + src/handler/user.rs | 22 +++ src/lib.rs | 2 +- src/middleware/mod.rs | 0 src/router/article.rs | 11 ++ src/router/category.rs | 20 +++ src/router/mod.rs | 11 +- src/router/user.rs | 24 +++ src/service/mod.rs | 3 +- src/service/user.rs | 61 +++++++ src/state.rs | 6 - 30 files changed, 628 insertions(+), 96 deletions(-) create mode 100644 src/common/extract/authorization.rs create mode 100644 src/common/extract/mod.rs create mode 100644 src/common/jwt/mod.rs create mode 100644 src/common/pagination.rs create mode 100644 src/common/util/datetime_format.rs create mode 100644 src/common/util/mod.rs create mode 100644 src/entity/users.rs create mode 100644 src/handler/user.rs create mode 100644 src/middleware/mod.rs create mode 100644 src/router/article.rs create mode 100644 src/router/category.rs create mode 100644 src/router/user.rs create mode 100644 src/service/user.rs delete mode 100644 src/state.rs diff --git a/Cargo.lock b/Cargo.lock index fbc45a1..90eb7e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,15 +240,42 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "895ff42f72016617773af68fb90da2a9677d89c62338ec09162d4909d86fdd8f" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "axum-with-seaorm" version = "0.1.0" dependencies = [ "askama", + "async-trait", "axum", + "axum-extra", + "bcrypt", "chrono", "config", "dotenvy", + "jsonwebtoken", + "lazy_static", "sea-orm", "serde", "serde_json", @@ -301,6 +328,19 @@ dependencies = [ "serde", ] +[[package]] +name = "bcrypt" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d1c9c15093eb224f0baa400f38fcd713fc1391a6f1c389d886beef146d60a3" +dependencies = [ + "base64 0.21.7", + "blowfish", + "getrandom", + "subtle", + "zeroize", +] + [[package]] name = "bigdecimal" version = "0.3.1" @@ -348,6 +388,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "borsh" version = "1.3.1" @@ -380,9 +430,9 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytecheck" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" dependencies = [ "bytecheck_derive", "ptr_meta", @@ -391,9 +441,9 @@ dependencies = [ [[package]] name = "bytecheck_derive" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" dependencies = [ "proc-macro2", "quote", @@ -448,6 +498,16 @@ dependencies = [ "windows-targets 0.52.0", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "config" version = "0.13.4" @@ -787,8 +847,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -844,6 +906,30 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -855,9 +941,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" +checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3" [[package]] name = "hex" @@ -1025,6 +1111,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "if_chain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" + [[package]] name = "indexmap" version = "2.2.2" @@ -1046,6 +1138,15 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "itertools" version = "0.12.1" @@ -1063,9 +1164,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" dependencies = [ "wasm-bindgen", ] @@ -1081,6 +1182,21 @@ dependencies = [ "serde", ] +[[package]] +name = "jsonwebtoken" +version = "9.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ea04a7c5c055c175f189b6dc6ba036fd62306b58c66c9f6389036c503a3f4" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1443,6 +1559,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pem" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" +dependencies = [ + "base64 0.21.7", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1460,9 +1586,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.6" +version = "2.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f200d8d83c44a45b21764d1916299752ca035d15ecd46faca3e9a2a2bf6ad06" +checksum = "219c0dcc30b6a27553f9cc242972b67f75b60eb0db71f0b5462f38b058c41546" dependencies = [ "memchr", "thiserror", @@ -1471,9 +1597,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.6" +version = "2.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcd6ab1236bbdb3a49027e920e693192ebfe8913f6d60e294de57463a493cfde" +checksum = "22e1288dbd7786462961e69bfd4df7848c1e37e8b74303dbdab82c3a9cdd2809" dependencies = [ "pest", "pest_generator", @@ -1481,9 +1607,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.6" +version = "2.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a31940305ffc96863a735bef7c7994a00b325a7138fdbc5bda0f1a0476d3275" +checksum = "1381c29a877c6d34b8c176e734f35d7f7f5b3adaefe940cb4d1bb7af94678e2e" dependencies = [ "pest", "pest_meta", @@ -1494,9 +1620,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.6" +version = "2.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7ff62f5259e53b78d1af898941cdcdccfae7385cf7d793a6e55de5d05bb4b7d" +checksum = "d0934d6907f148c22a3acbda520c7eed243ad7487a30f51f6ce52b58b7077a8a" dependencies = [ "once_cell", "pest", @@ -1736,9 +1862,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rend" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" dependencies = [ "bytecheck", ] @@ -1759,9 +1885,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.43" +version = "0.7.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527a97cdfef66f65998b5f3b637c26f5a5ec09cc52a3f9932313ac645f4190f5" +checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" dependencies = [ "bitvec", "bytecheck", @@ -1777,9 +1903,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.43" +version = "0.7.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5c462a1328c8e67e4d6dbad1eb0355dd43e8ab432c6e227a43657f16ade5033" +checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65" dependencies = [ "proc-macro2", "quote", @@ -1941,9 +2067,9 @@ dependencies = [ [[package]] name = "sea-orm" -version = "0.12.12" +version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cbf88748872fa54192476d6d49d0775e208566a72656e267e45f6980b926c8d" +checksum = "6632f499b80cc6aaa781b302e4c9fae663e0e3dcf2640e9d80034d5b10731efe" dependencies = [ "async-stream", "async-trait", @@ -1969,9 +2095,9 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "0.12.12" +version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0dbc880d47aa53c6a572e39c99402c7fad59b50766e51e0b0fc1306510b0555" +checksum = "ec13bfb4c4aef208f68dbea970dd40d13830c868aa8dcb4e106b956e6bb4f2fa" dependencies = [ "heck", "proc-macro2", @@ -2129,6 +2255,18 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -2495,13 +2633,12 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.9.0" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", "windows-sys 0.52.0", ] @@ -2800,9 +2937,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode_categories" @@ -2855,6 +2992,33 @@ dependencies = [ "serde_derive", "serde_json", "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af" +dependencies = [ + "if_chain", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" +dependencies = [ + "proc-macro2", + "syn 1.0.109", ] [[package]] @@ -2883,9 +3047,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2893,9 +3057,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" dependencies = [ "bumpalo", "log", @@ -2908,9 +3072,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2918,9 +3082,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", @@ -2931,9 +3095,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" [[package]] name = "webpki-roots" @@ -3121,9 +3285,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.37" +version = "0.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cad8365489051ae9f054164e459304af2e7e9bb407c958076c8bf4aef52da5" +checksum = "5389a154b01683d28c77f8f68f49dea75f0a4da32557a58f68ee51ebba472d29" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 1b0ed08..868ffb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,4 +18,9 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "time", "lo serde_json = "1.0.113" chrono = "0.4.33" state = "0.6.0" -validator = "0.16.1" +lazy_static = "1.4.0" +validator = { version = "0.16.1", features = ["derive"] } +jsonwebtoken = "9.2.0" +async-trait = "0.1.77" +bcrypt = "0.15.0" +axum-extra = { version = "0.9.2", features = ["typed-header"] } diff --git a/config/default.toml b/config/default.toml index 535bbd6..6fd82ee 100644 --- a/config/default.toml +++ b/config/default.toml @@ -1,3 +1,5 @@ +jwt_secret = "123456" + [server] port = 9527 host = "0.0.0.0" @@ -5,3 +7,4 @@ host = "0.0.0.0" [mysql] max_cons = 5 dsn = "mysql://root:mysql123!%40%23@127.0.0.1:3306/study" + diff --git a/config/dev.toml b/config/dev.toml index b6f9739..e69de29 100644 --- a/config/dev.toml +++ b/config/dev.toml @@ -1,6 +0,0 @@ -[web] -addr = "0.0.0.0:9527" - -[mysql] -max_cons = 5 -dns = "mysql://root:mysql123!%40%23@127.0.0.1:3306/study" diff --git a/src/common/err.rs b/src/common/err.rs index 6068622..406973c 100644 --- a/src/common/err.rs +++ b/src/common/err.rs @@ -1,9 +1,15 @@ +use crate::common::response::AppResponse; + #[derive(Debug)] pub enum AppErrorType { + System, Config, Database, Notfound, + Validate, Template, + Authorization, + Forbidden, } type Cause = Box; @@ -38,7 +44,11 @@ impl AppError { impl std::fmt::Display for AppError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) + let msg = match &self.error { + AppErrorItem::Cause(err) => err.to_string(), + AppErrorItem::Message(msg) => msg.to_string(), + }; + write!(f, "{}", msg) } } @@ -62,13 +72,22 @@ impl From for AppError { } } +impl From for AppError { + fn from(err: jsonwebtoken::errors::Error) -> Self { + Self::from_err(Box::new(err), AppErrorType::Authorization) + } +} + +impl From for AppError { + fn from(err: validator::ValidationErrors) -> Self { + Self::from_err(Box::new(err), AppErrorType::Validate) + } +} + + impl axum::response::IntoResponse for AppError { fn into_response(self) -> axum::response::Response { - let msg = match self.error { - AppErrorItem::Cause(err) => err.to_string(), - AppErrorItem::Message(msg) => msg.to_string(), - }; - msg.into_response() + AppResponse::<()>::from_error(self).into_response() } } diff --git a/src/common/extract/authorization.rs b/src/common/extract/authorization.rs new file mode 100644 index 0000000..177c11c --- /dev/null +++ b/src/common/extract/authorization.rs @@ -0,0 +1,37 @@ +use async_trait::async_trait; +use axum::extract::FromRequestParts; +use axum::http::request::Parts; +use axum::http::StatusCode; +use axum::RequestPartsExt; +use axum_extra::headers::authorization::{Authorization as ExtraAuthorization, Bearer}; +use axum_extra::TypedHeader; +use tracing::info; + +use crate::common::err::{AppError, AppErrorType}; +use crate::common::jwt::JwtClaims; + +#[derive(Debug, Clone)] +pub struct Authorization(pub JwtClaims); + + +#[async_trait] +impl FromRequestParts for Authorization { + type Rejection = (StatusCode, AppError); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let TypedHeader(ExtraAuthorization(bearer)) = parts + .extract::>>() + .await + .map_err(|error| { + (StatusCode::UNAUTHORIZED, AppError::from_err(Box::new(error), AppErrorType::Authorization)) + })?; + + info!("Authorization from_request_parts"); + + let claims = JwtClaims::verify(bearer.token()) + .map_err(|error| (StatusCode::UNAUTHORIZED, error))?; + + Ok(Self(claims)) + } +} + diff --git a/src/common/extract/mod.rs b/src/common/extract/mod.rs new file mode 100644 index 0000000..3e8f8bd --- /dev/null +++ b/src/common/extract/mod.rs @@ -0,0 +1 @@ +pub mod authorization; \ No newline at end of file diff --git a/src/common/jwt/mod.rs b/src/common/jwt/mod.rs new file mode 100644 index 0000000..08c072a --- /dev/null +++ b/src/common/jwt/mod.rs @@ -0,0 +1,65 @@ +use chrono::{Duration, Utc}; +use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; +use jsonwebtoken::errors::ErrorKind; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; + +use crate::APPLICATION_CONTEXT; +use crate::common::err::{AppError, AppErrorType}; +use crate::common::Result; +use crate::config::app::AppConfig; + +lazy_static! { + pub static ref JWT_SECRET: String = { + let app_cfg = APPLICATION_CONTEXT.get::(); + app_cfg.jwt_secret.clone() + }; +} + + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct JwtClaims { + // 账号id + pub id: u32, + // 账号 + pub email: String, + // 过期时间 + pub exp: i64, + // 签名时间 + pub iat: i64, +} + +impl JwtClaims { + pub fn new(id: u32, email: String) -> Self { + let iat = Utc::now(); + let exp = iat + Duration::hours(24); + + Self { + id, + email, + exp: exp.timestamp(), + iat: iat.timestamp(), + } + } + + pub fn sign(&self) -> Result { + let encoding_key = EncodingKey::from_secret(JWT_SECRET.as_bytes()); + jsonwebtoken::encode(&Header::default(), self, &encoding_key) + .map_err(AppError::from) + } + + pub fn verify(token: &str) -> Result { + let decode_key = DecodingKey::from_secret(JWT_SECRET.as_bytes()); + + let validation = Validation::default(); + + match jsonwebtoken::decode::(token, &decode_key, &validation) { + Ok(c) => Ok(c.claims), + Err(e) => match e.kind() { + ErrorKind::InvalidToken => Err(AppError::from_msg("Token失效", AppErrorType::Authorization)), + ErrorKind::InvalidIssuer => Err(AppError::from_msg("InvalidIssuer", AppErrorType::Authorization)), + _ => Err(AppError::from_msg("InvalidToken other errors", AppErrorType::Authorization)) + }, + } + } +} \ No newline at end of file diff --git a/src/common/mod.rs b/src/common/mod.rs index 255e740..45f6ff7 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,6 +1,10 @@ pub mod template; pub mod err; pub mod response; +pub mod jwt; +pub mod extract; +mod pagination; +pub mod util; pub type Result = std::result::Result; diff --git a/src/common/pagination.rs b/src/common/pagination.rs new file mode 100644 index 0000000..4e631ab --- /dev/null +++ b/src/common/pagination.rs @@ -0,0 +1,15 @@ +use serde::Serialize; + +#[derive(Serialize, Debug)] +pub struct Paginate { + pub total: u32, + pub page: u32, + pub page_size: u32, + pub data: T, +} + +impl Paginate { + pub fn new(total: u32, page: u32, page_size: u32, data: T) -> Self { + Self { total, page, page_size, data } + } +} \ No newline at end of file diff --git a/src/common/response.rs b/src/common/response.rs index 9ee8b3f..6947fef 100644 --- a/src/common/response.rs +++ b/src/common/response.rs @@ -1,7 +1,14 @@ use axum::http::{header, HeaderMap, StatusCode}; -use axum::response::Html; +use axum::response::{Html, IntoResponse, Json, Response}; +use serde::Serialize; + +use crate::common::err::AppError; + use super::Result; +pub const CODE_SUCCESS: i8 = 0; +pub const CODE_FAIL: i8 = -1; + pub type HtmlResponse = Html; pub type RedirectResponse = (StatusCode, HeaderMap, ()); @@ -11,5 +18,45 @@ pub fn redirect(url: &str) -> Result { Ok((StatusCode::FOUND, header, ())) } +#[derive(Debug, Serialize, Clone)] +pub struct AppResponse { + pub code: Option, + pub msg: Option, + pub data: Option, +} + +impl IntoResponse for AppResponse { + fn into_response(self) -> Response { + Json(self).into_response() + } +} + +impl AppResponse { + pub fn from_data(data: T) -> Self { + Self { + code: Some(CODE_SUCCESS), + msg: Some("success".to_string()), + data: Some(data), + } + } + + pub fn from_error(err: AppError) -> Self { + Self { + code: Some(CODE_FAIL), + msg: Some(err.to_string()), + data: None, + } + } + + pub fn from_result(result: Result) -> Self { + match result { + Ok(data) => Self::from_data(data), + Err(err) => Self::from_error(err), + } + } +} + + + diff --git a/src/common/util/datetime_format.rs b/src/common/util/datetime_format.rs new file mode 100644 index 0000000..4c161ac --- /dev/null +++ b/src/common/util/datetime_format.rs @@ -0,0 +1,28 @@ +pub mod default_datetime_format { + use chrono::{DateTime, Local, NaiveDateTime}; + use serde::{Deserialize, Deserializer, Serializer}; + + const FORMAT: &'static str = "%Y-%m-%d %H:%M:%S"; + + pub fn serialize( + date: &DateTime, + serializer: S, + ) -> Result + where + S: Serializer, + { + let s = format!("{}", date.format(FORMAT)); + serializer.serialize_str(&s) + } + + pub fn deserialize<'de, D>( + deserializer: D, + ) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let datetime_str = String::deserialize(deserializer)?; + let dt = NaiveDateTime::parse_from_str(&datetime_str, FORMAT).map_err(serde::de::Error::custom)?; + Ok(dt.and_local_timezone(Local).unwrap()) + } +} \ No newline at end of file diff --git a/src/common/util/mod.rs b/src/common/util/mod.rs new file mode 100644 index 0000000..a7e0887 --- /dev/null +++ b/src/common/util/mod.rs @@ -0,0 +1 @@ +pub mod datetime_format; \ No newline at end of file diff --git a/src/config/app.rs b/src/config/app.rs index 9b4a57d..7b122a6 100644 --- a/src/config/app.rs +++ b/src/config/app.rs @@ -7,4 +7,5 @@ use crate::config::server::ServerConfig; pub struct AppConfig { pub mysql: MysqlConfig, pub server: ServerConfig, + pub jwt_secret: String, } diff --git a/src/entity/mod.rs b/src/entity/mod.rs index 6694a54..d9499c1 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -1,4 +1,5 @@ pub mod category; pub mod article; pub mod article_tag; -pub mod tag; \ No newline at end of file +pub mod tag; +pub mod users; \ No newline at end of file diff --git a/src/entity/users.rs b/src/entity/users.rs new file mode 100644 index 0000000..aaf7393 --- /dev/null +++ b/src/entity/users.rs @@ -0,0 +1,22 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "users")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: u32, + pub email: String, + #[serde(skip_serializing)] + pub password_hash: String, + pub create_time: DateTimeLocal, + #[serde(skip_serializing)] + pub is_del: bool, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/form.rs b/src/form.rs index 69a66a3..c986904 100644 --- a/src/form.rs +++ b/src/form.rs @@ -1,12 +1,23 @@ use serde::Deserialize; +use validator::{Validate}; #[derive(Deserialize)] pub struct CategoryForm { pub name: String, } + #[derive(Deserialize)] pub struct ArticleForm { pub title: String, pub category_id: u32, pub content: String, +} + + +#[derive(Validate, Deserialize)] +pub struct UserSignForm { + #[validate(email(message = "请输入正确格式的邮箱"))] + pub email: String, + #[validate(length(min = 6, message = "密码长度至少为 6"))] + pub password: String, } \ No newline at end of file diff --git a/src/handler/article.rs b/src/handler/article.rs index bcfcfb8..a03035c 100644 --- a/src/handler/article.rs +++ b/src/handler/article.rs @@ -1,6 +1,5 @@ -use axum::{Form, Router}; use axum::extract::Query; -use axum::routing::get; +use axum::Form; use crate::common::{response::HtmlResponse, Result}; use crate::common::response::{redirect, RedirectResponse}; @@ -9,13 +8,6 @@ use crate::form::ArticleForm; use crate::param::ArticleParams; use crate::service::article::ArticleService; -pub fn init_router() -> Router { - Router::new() - .route("/", get(index)) - .route("/add", get(add_ui).post(add)) - .route("/tags", get(list_with_tags)) -} - pub async fn index(Query(params): Query) -> Result { let handler_name = "article/index"; let tpl = ArticleService::index(handler_name, params).await?; diff --git a/src/handler/category.rs b/src/handler/category.rs index 9f87290..142406a 100644 --- a/src/handler/category.rs +++ b/src/handler/category.rs @@ -1,6 +1,5 @@ -use axum::{Form, Router}; use axum::extract::{Path, Query}; -use axum::routing::get; +use axum::Form; use crate::{form, view}; use crate::common::{response::HtmlResponse, Result}; @@ -9,22 +8,6 @@ use crate::common::template::render; use crate::param::{CategoryParams, DelParams}; use crate::service::category::CategoryService; -pub fn init_router() -> Router { - Router::new() - .route("/", get(index)) - .route( - "/add", - get(add_ui).post(add), - ) - .route( - "/edit/:id", - get(edit_ui).post(edit), - ) - .route("/del/:id", get(del)) - .route("/del/:id/:real", get(del)) - .route("/articles/:id", get(articles)) -} - pub async fn index(Query(params): Query) -> Result { let handler_name = "category/index"; let tpl = CategoryService::index(handler_name, params).await?; diff --git a/src/handler/mod.rs b/src/handler/mod.rs index 360ba42..0bc4293 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -4,4 +4,5 @@ pub mod category; mod index; pub mod article; +pub mod user; diff --git a/src/handler/user.rs b/src/handler/user.rs new file mode 100644 index 0000000..ae57800 --- /dev/null +++ b/src/handler/user.rs @@ -0,0 +1,22 @@ +use axum::Json; + +use crate::common::extract::authorization::Authorization; +use crate::common::response::AppResponse; +use crate::common::Result; +use crate::entity::users; +use crate::form::UserSignForm; +use crate::service::user::UserService; + +pub async fn userinfo(Authorization(jwt_claims): Authorization) -> Result> { + let user = UserService::userinfo(jwt_claims.id).await?; + Ok(AppResponse::from_data(user)) +} + +pub async fn sign_in(Json(jsn): Json) -> Result { + UserService::sign_in(jsn).await +} + +pub async fn list() -> Result>> { + let user = UserService::list().await?; + Ok(AppResponse::from_data(user)) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index e256cd1..ef1be38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,6 @@ use ::state::type_map::TypeMapSendSync; use tracing::info; -pub mod state; pub mod entity; // pub mod state; pub mod handler; @@ -13,6 +12,7 @@ pub mod config; pub mod router; pub mod initialize; pub mod common; +pub mod middleware; /// 整个项目上下文 ApplicationContext diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/router/article.rs b/src/router/article.rs new file mode 100644 index 0000000..470ef8c --- /dev/null +++ b/src/router/article.rs @@ -0,0 +1,11 @@ +use axum::Router; +use axum::routing::get; + +use crate::handler::article::{add, add_ui, index, list_with_tags}; + +pub fn init_router() -> Router { + Router::new() + .route("/", get(index)) + .route("/add", get(add_ui).post(add)) + .route("/tags", get(list_with_tags)) +} \ No newline at end of file diff --git a/src/router/category.rs b/src/router/category.rs new file mode 100644 index 0000000..5978afe --- /dev/null +++ b/src/router/category.rs @@ -0,0 +1,20 @@ +use axum::Router; +use axum::routing::get; + +use crate::handler::category::{add, add_ui, articles, del, edit, edit_ui, index}; + +pub fn init_router() -> Router { + Router::new() + .route("/", get(index)) + .route( + "/add", + get(add_ui).post(add), + ) + .route( + "/edit/:id", + get(edit_ui).post(edit), + ) + .route("/del/:id", get(del)) + .route("/del/:id/:real", get(del)) + .route("/articles/:id", get(articles)) +} diff --git a/src/router/mod.rs b/src/router/mod.rs index 3960d3f..d655f8f 100644 --- a/src/router/mod.rs +++ b/src/router/mod.rs @@ -1,13 +1,18 @@ +mod article; +mod category; +mod user; + use axum::routing::get; -use crate::handler; -use crate::handler::{category, article}; +use crate::handler::{index}; pub fn init() -> axum::Router { let category_router = category::init_router(); let article_router = article::init_router(); + let user_router = user::init_router(); axum::Router::new() - .route("/", get(handler::index)) + .route("/", get(index)) .nest("/category", category_router) .nest("/article", article_router) + .nest("/user", user_router) } diff --git a/src/router/user.rs b/src/router/user.rs new file mode 100644 index 0000000..890f8c4 --- /dev/null +++ b/src/router/user.rs @@ -0,0 +1,24 @@ +use axum::middleware::from_extractor; +use axum::Router; +use axum::routing::{get, post}; + +use crate::common::extract::authorization::Authorization; +use crate::handler::user::{list, sign_in, userinfo}; + +pub fn init_router() -> Router { + need_auth_routers().merge(not_need_auth_routers()) +} + +pub fn need_auth_routers() -> Router { + Router::new() + // 在此之前的利用中间价鉴权 + .layer(from_extractor::()) + // 之后的利用提取器鉴权 + .route("/userinfo", get(userinfo)) +} + +pub fn not_need_auth_routers() -> Router { + Router::new() + .route("/sign_in", post(sign_in)) + .route("/list", get(list)) +} diff --git a/src/service/mod.rs b/src/service/mod.rs index 3e7e78f..4dc8bdf 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -1,2 +1,3 @@ pub mod article; -pub mod category; \ No newline at end of file +pub mod category; +pub mod user; \ No newline at end of file diff --git a/src/service/user.rs b/src/service/user.rs new file mode 100644 index 0000000..83c8726 --- /dev/null +++ b/src/service/user.rs @@ -0,0 +1,61 @@ +use bcrypt::DEFAULT_COST; +use chrono::Local; +use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, DatabaseConnection, EntityTrait, QueryFilter}; +use sea_orm::ActiveValue::Set; +use validator::{Validate}; + +use crate::APPLICATION_CONTEXT; +use crate::common::{err::AppError, Result}; +use crate::common::err::AppErrorType; +use crate::common::jwt::JwtClaims; +use crate::entity::users; +use crate::form::UserSignForm; + +pub struct UserService; + + +impl UserService { + pub async fn list() -> Result> { + let conn = APPLICATION_CONTEXT.get::(); + users::Entity::find().all(conn).await.map_err(AppError::from) + } + + pub async fn userinfo(id: u32) -> Result { + let conn = APPLICATION_CONTEXT.get::(); + let user = users::Entity::find_by_id(id).one(conn).await.map_err(AppError::from)?; + match user { + None => Err(AppError::notfound()), + Some(user) => Ok(user), + } + } + + pub async fn sign_in(jsn: UserSignForm) -> Result { + jsn.validate().map_err(AppError::from)?; + + let conn = APPLICATION_CONTEXT.get::(); + + let condition = Condition::all().add(users::Column::Email.eq(&jsn.email)); + + let user = users::Entity::find().filter(condition).one(conn).await?; + + if user.is_some() { + return Err(AppError::from_msg("用户已存在", AppErrorType::Database)); + } + + let hashed_password = bcrypt::hash(&jsn.password, DEFAULT_COST).unwrap(); + + let user = users::ActiveModel { + email: Set(jsn.email), + password_hash: Set(hashed_password), + create_time: Set(Local::now()), + ..Default::default() + }; + + let user = user.save(conn).await.map_err(AppError::from)?; + + let jwt_token = JwtClaims::new(user.id.unwrap(), user.email.unwrap()).sign(); + + jwt_token + } +} + diff --git a/src/state.rs b/src/state.rs deleted file mode 100644 index f95a965..0000000 --- a/src/state.rs +++ /dev/null @@ -1,6 +0,0 @@ - -use sea_orm::DatabaseConnection; - -pub struct AppState { - pub conn: DatabaseConnection, -} \ No newline at end of file