diff --git a/examples/actix-app/config/config.dev.toml b/examples/actix-app/config/config.dev.toml index 994c2844..2f56ee17 100644 --- a/examples/actix-app/config/config.dev.toml +++ b/examples/actix-app/config/config.dev.toml @@ -74,4 +74,4 @@ span = { "http.method" = "string", "http.target" = "string", "http.status_code" app-name = "data-cube" [openapi] -custom-html = "assets/docs/rapidoc.html" +custom-html = "local/docs/rapidoc.html" diff --git a/examples/actix-app/config/openapi/auth.toml b/examples/actix-app/config/openapi/auth.toml index ef2d564a..c0a6f070 100644 --- a/examples/actix-app/config/openapi/auth.toml +++ b/examples/actix-app/config/openapi/auth.toml @@ -3,7 +3,7 @@ name = "Authentication" [[endpoints]] path = "/auth/login" method = "POST" -summary = "Account login" +summary = "Logins the user account" securities = [] [endpoints.body] @@ -14,9 +14,9 @@ password = { type = "string", format = "password", description = "User password" [[endpoints]] path = "/auth/refresh" method = "GET" -summary = "Refreshes access token" +summary = "Refreshes the access token" [[endpoints]] path = "/auth/logout" method = "POST" -summary = "Account logout" +summary = "Logouts the user account" diff --git a/examples/actix-app/assets/docs/rapidoc.html b/examples/actix-app/local/docs/rapidoc.html similarity index 100% rename from examples/actix-app/assets/docs/rapidoc.html rename to examples/actix-app/local/docs/rapidoc.html diff --git a/examples/actix-app/src/main.rs b/examples/actix-app/src/main.rs index 07c6d059..d16613f2 100644 --- a/examples/actix-app/src/main.rs +++ b/examples/actix-app/src/main.rs @@ -1,6 +1,5 @@ #![feature(async_fn_in_trait)] #![feature(lazy_cell)] -#![feature(let_chains)] mod controller; mod domain; diff --git a/examples/actix-app/src/middleware/access.rs b/examples/actix-app/src/middleware/access.rs index 49c1c882..5906a6e2 100644 --- a/examples/actix-app/src/middleware/access.rs +++ b/examples/actix-app/src/middleware/access.rs @@ -44,20 +44,28 @@ where fn call(&self, req: ServiceRequest) -> Self::Future { let mut req = Request::from(req); - if let Ok(claims) = req.parse_jwt_claims(JwtClaims::shared_key()) && - let Ok(mut user_session) = UserSession::::try_from_jwt_claims(claims) - { - if let Ok(session_id) = req.parse_session_id() { - user_session.set_session_id(session_id); + match req.parse_jwt_claims(JwtClaims::shared_key()) { + Ok(claims) => { + if let Ok(mut user_session) = UserSession::::try_from_jwt_claims(claims) { + if let Ok(session_id) = req.parse_session_id() { + user_session.set_session_id(session_id); + } + req.set_data(user_session); + } else { + return Box::pin(async move { + let message = "401 Unauthorized: invalid JWT claims"; + let rejection = Rejection::with_message(message).context(&req).into(); + let result: zino::Result = Err(rejection); + result.map_err(|err| err.into()) + }); + } + } + Err(rejection) => { + return Box::pin(async move { + let result: zino::Result = Err(rejection.into()); + result.map_err(|err| err.into()) + }); } - req.set_data(user_session); - } else { - return Box::pin(async move { - let message = "401 Unauthorized: login is required"; - let rejection = Rejection::with_message(message).context(&req).into(); - let result: zino::Result = Err(rejection); - return result.map_err(|err| err.into()); - }); } let req = ServiceRequest::from(req); diff --git a/examples/axum-app/Cargo.toml b/examples/axum-app/Cargo.toml index b3cb21e6..a98375c5 100644 --- a/examples/axum-app/Cargo.toml +++ b/examples/axum-app/Cargo.toml @@ -18,12 +18,12 @@ features = ["derive"] [dependencies.zino] path = "../../zino" -version = "0.11.3" +version = "0.11.4" features = ["axum", "export-pdf"] [dependencies.zino-core] path = "../../zino-core" -version = "0.12.3" +version = "0.12.4" features = [ "connector", "connector-arrow", @@ -33,8 +33,8 @@ features = [ [dependencies.zino-derive] path = "../../zino-derive" -version = "0.9.3" +version = "0.9.4" [dependencies.zino-model] path = "../../zino-model" -version = "0.9.3" +version = "0.9.4" diff --git a/examples/axum-app/config/config.dev.toml b/examples/axum-app/config/config.dev.toml index 563199ec..2b437840 100644 --- a/examples/axum-app/config/config.dev.toml +++ b/examples/axum-app/config/config.dev.toml @@ -74,4 +74,4 @@ span = { "http.method" = "string", "http.target" = "string", "http.status_code" app-name = "data-cube" [openapi] -custom-html = "assets/docs/rapidoc.html" +custom-html = "local/docs/rapidoc.html" diff --git a/examples/axum-app/config/openapi/auth.toml b/examples/axum-app/config/openapi/auth.toml index ef2d564a..c0a6f070 100644 --- a/examples/axum-app/config/openapi/auth.toml +++ b/examples/axum-app/config/openapi/auth.toml @@ -3,7 +3,7 @@ name = "Authentication" [[endpoints]] path = "/auth/login" method = "POST" -summary = "Account login" +summary = "Logins the user account" securities = [] [endpoints.body] @@ -14,9 +14,9 @@ password = { type = "string", format = "password", description = "User password" [[endpoints]] path = "/auth/refresh" method = "GET" -summary = "Refreshes access token" +summary = "Refreshes the access token" [[endpoints]] path = "/auth/logout" method = "POST" -summary = "Account logout" +summary = "Logouts the user account" diff --git a/examples/axum-app/assets/docs/rapidoc.html b/examples/axum-app/local/docs/rapidoc.html similarity index 100% rename from examples/axum-app/assets/docs/rapidoc.html rename to examples/axum-app/local/docs/rapidoc.html diff --git a/examples/axum-app/src/main.rs b/examples/axum-app/src/main.rs index 07c6d059..d16613f2 100644 --- a/examples/axum-app/src/main.rs +++ b/examples/axum-app/src/main.rs @@ -1,6 +1,5 @@ #![feature(async_fn_in_trait)] #![feature(lazy_cell)] -#![feature(let_chains)] mod controller; mod domain; diff --git a/examples/axum-app/src/middleware/access.rs b/examples/axum-app/src/middleware/access.rs index 204a1c59..eda21e8e 100644 --- a/examples/axum-app/src/middleware/access.rs +++ b/examples/axum-app/src/middleware/access.rs @@ -3,24 +3,23 @@ use zino::{prelude::*, Request, Result}; use zino_model::user::{JwtAuthService, User}; pub async fn init_user_session(mut req: Request, next: Next) -> Result { - if let Ok(claims) = req.parse_jwt_claims(JwtClaims::shared_key()) { - match User::verify_jwt_claims(&claims).await { - Ok(verified) => { - if verified && - let Ok(mut user_session) = UserSession::::try_from_jwt_claims(claims) - { - if let Ok(session_id) = req.parse_session_id() { - user_session.set_session_id(session_id); - } - req.set_data(user_session); - } else { - reject!(req, unauthorized, "invalid JWT claims"); + let claims = req + .parse_jwt_claims(JwtClaims::shared_key()) + .map_err(|rejection| rejection.context(&req))?; + match User::verify_jwt_claims(&claims).await { + Ok(verified) => { + if verified { + let mut user_session = UserSession::::try_from_jwt_claims(claims) + .extract(&req)?; + if let Ok(session_id) = req.parse_session_id() { + user_session.set_session_id(session_id); } + req.set_data(user_session); + } else { + reject!(req, unauthorized, "invalid JWT claims"); } - Err(err) => reject!(req, unauthorized, err), } - } else { - reject!(req, unauthorized, "login is required"); + Err(err) => reject!(req, unauthorized, err), } Ok(next.run(req.into()).await) } diff --git a/examples/dioxus-desktop/Cargo.toml b/examples/dioxus-desktop/Cargo.toml index aab0c348..fac54538 100644 --- a/examples/dioxus-desktop/Cargo.toml +++ b/examples/dioxus-desktop/Cargo.toml @@ -17,14 +17,14 @@ features = ["derive"] [dependencies.zino] path = "../../zino" -version = "0.11.3" +version = "0.11.4" features = ["dioxus"] [dependencies.zino-core] path = "../../zino-core" -version = "0.12.3" +version = "0.12.4" features = ["orm-sqlite"] [dependencies.zino-model] path = "../../zino-model" -version = "0.9.3" +version = "0.9.4" diff --git a/zino-core/Cargo.toml b/zino-core/Cargo.toml index 28223c90..a876a7fb 100644 --- a/zino-core/Cargo.toml +++ b/zino-core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zino-core" description = "Core types and traits for zino." -version = "0.12.3" +version = "0.12.4" rust-version = "1.72" edition = "2021" license = "MIT" @@ -104,7 +104,7 @@ mime_guess = "2.0.4" multer = "2.1.0" parking_lot = "0.12.1" rand = "0.8.5" -regex = "1.9.4" +regex = "1.9.5" reqwest-middleware = "0.2.3" reqwest-retry = "0.2.3" reqwest-tracing = "0.4.6" @@ -181,7 +181,7 @@ features = [ ] [dependencies.tera] -version = "1.19.0" +version = "1.19.1" optional = true [dependencies.tracing-subscriber] diff --git a/zino-core/src/request/mod.rs b/zino-core/src/request/mod.rs index 37961065..9ea1f80f 100644 --- a/zino-core/src/request/mod.rs +++ b/zino-core/src/request/mod.rs @@ -535,7 +535,6 @@ pub trait RequestContext { T: Default + Serialize + DeserializeOwned, K: MACLike, { - let mut validation = Validation::new(); let (param, mut token) = match self.get_query("access_token") { Some(access_token) => ("access_token", access_token), None => ("authorization", ""), @@ -545,6 +544,11 @@ pub trait RequestContext { .strip_prefix("Bearer ") .unwrap_or(authorization); } + if token.is_empty() { + let mut validation = Validation::new(); + validation.record(param, "the JWT token is absent"); + return Err(Rejection::bad_request(validation).context(self)); + } let mut options = auth::default_verification_options(); options.reject_before = self @@ -554,12 +558,12 @@ pub trait RequestContext { options.required_nonce = self.get_query("nonce").map(|s| s.to_owned()); match key.verify_token(token, Some(options)) { - Ok(claims) => return Ok(JwtClaims(claims)), + Ok(claims) => Ok(JwtClaims(claims)), Err(err) => { - validation.record(param, err.to_string()); + let message = format!("401 Unauthorized: {err}"); + Err(Rejection::with_message(message).context(self)) } } - Err(Rejection::bad_request(validation).context(self)) } /// Returns a `Response` or `Rejection` from an SQL query validation. diff --git a/zino-derive/Cargo.toml b/zino-derive/Cargo.toml index c4827dca..81959607 100644 --- a/zino-derive/Cargo.toml +++ b/zino-derive/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zino-derive" description = "Derived traits for zino." -version = "0.9.3" +version = "0.9.4" rust-version = "1.72" edition = "2021" license = "MIT" @@ -17,9 +17,9 @@ proc-macro = true convert_case = "0.6.0" proc-macro2 = "1.0.66" quote = "1.0.33" -syn = "2.0.29" +syn = "2.0.31" [dependencies.zino-core] path = "../zino-core" -version = "0.12.3" +version = "0.12.4" features = ["orm"] diff --git a/zino-model/Cargo.toml b/zino-model/Cargo.toml index 55433752..100b72a5 100644 --- a/zino-model/Cargo.toml +++ b/zino-model/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zino-model" description = "Domain models for zino." -version = "0.9.3" +version = "0.9.4" rust-version = "1.72" edition = "2021" license = "MIT" @@ -29,7 +29,7 @@ maintainer-id = [] edition = [] [dependencies] -regex = "1.9.4" +regex = "1.9.5" strum_macros = "0.25.2" [dependencies.serde] @@ -38,9 +38,9 @@ features = ["derive"] [dependencies.zino-core] path = "../zino-core" -version = "0.12.3" +version = "0.12.4" features = ["orm"] [dependencies.zino-derive] path = "../zino-derive" -version = "0.9.3" +version = "0.9.4" diff --git a/zino-model/src/user/jwt_auth.rs b/zino-model/src/user/jwt_auth.rs index 22ecdd98..d8dd80f4 100644 --- a/zino-model/src/user/jwt_auth.rs +++ b/zino-model/src/user/jwt_auth.rs @@ -85,10 +85,10 @@ where async fn generate_token(body: Map) -> Result<(K, Map), Error> { let account = body .get_str("account") - .ok_or_else(|| Error::new("403 Forbidden: the user `account` shoud be specified"))?; - let passowrd = body - .get_str("password") - .ok_or_else(|| Error::new("403 Forbidden: the user `password` shoud be specified"))?; + .ok_or_else(|| Error::new("401 Unauthorized: the user `account` shoud be specified"))?; + let passowrd = body.get_str("password").ok_or_else(|| { + Error::new("401 Unauthorized: the user `password` shoud be specified") + })?; let mut query = Query::default(); let mut fields = vec![Self::PRIMARY_KEY_NAME, Self::PASSWORD_FIELD]; if let Some(role_field) = Self::ROLE_FIELD { @@ -144,11 +144,15 @@ where /// Refreshes the access token. async fn refresh_token(claims: &JwtClaims) -> Result { if !claims.data().is_empty() { - return Err(Error::new("the JWT token is not a refresh token")); + return Err(Error::new( + "401 Unauthorized: the JWT token is not a refresh token", + )); } let Some(user_id) = claims.subject() else { - return Err(Error::new("the JWT token does not have a subject")); + return Err(Error::new( + "401 Unauthorized: the JWT token does not have a subject", + )); }; let mut query = Query::default(); @@ -167,7 +171,7 @@ where ); let mut user: Map = Self::find_one(&query).await?.ok_or_else(|| { - let message = format!("403 Forbidden: cannot get the user `{user_id}`"); + let message = format!("404 Not Found: cannot get the user `{user_id}`"); Error::new(message) })?; let mut claims = JwtClaims::new(user_id); @@ -189,7 +193,9 @@ where /// Verfifies the JWT claims. async fn verify_jwt_claims(claims: &JwtClaims) -> Result { let Some(user_id) = claims.subject() else { - return Err(Error::new("the JWT token does not have a subject")); + return Err(Error::new( + "401 Unauthorized: the JWT token does not have a subject", + )); }; let mut query = Query::default(); @@ -211,7 +217,7 @@ where ); let user: Map = Self::find_one(&query).await?.ok_or_else(|| { - let message = format!("403 Forbidden: cannot get the user `{user_id}`"); + let message = format!("404 Not Found: cannot get the user `{user_id}`"); Error::new(message) })?; let data = claims.data(); @@ -219,14 +225,14 @@ where let Some(roles) = data.get("roles") && user.get(role_field) != Some(roles) { - let message = format!("403 Forbidden: invalid for the `{role_field}` field"); + let message = format!("401 Unauthorized: invalid for the `{role_field}` field"); return Err(Error::new(message)); } if let Some(tenant_id_field) = Self::TENANT_ID_FIELD && let Some(tenant_id) = data.get("tenant_id") && user.get(tenant_id_field) != Some(tenant_id) { - let message = format!("403 Forbidden: invalid for the `{tenant_id_field}` field"); + let message = format!("401 Unauthorized: invalid for the `{tenant_id_field}` field"); return Err(Error::new(message)); } if let Some(login_at_field) = Self::LOGIN_AT_FIELD && @@ -234,7 +240,7 @@ where let Ok(login_at) = login_at_str.parse::() && claims.issued_at().timestamp() < login_at.timestamp() { - let message = format!("403 Forbidden: invalid before the `{login_at_field}` time"); + let message = format!("401 Unauthorized: invalid before the `{login_at_field}` time"); return Err(Error::new(message)); } Ok(true) @@ -261,7 +267,7 @@ where query.add_filter("status", Map::from_entry("$nin", vec!["Locked", "Deleted"])); let user: Map = Self::find_one(&query).await?.ok_or_else(|| { - let message = format!("403 Forbidden: cannot get the user `{user_id}`"); + let message = format!("404 Not Found: cannot get the user `{user_id}`"); Error::new(message) })?; diff --git a/zino/Cargo.toml b/zino/Cargo.toml index 6b1ff691..6b11a4fd 100644 --- a/zino/Cargo.toml +++ b/zino/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zino" description = "Next-generation framework for composable applications in Rust." -version = "0.11.3" +version = "0.11.4" rust-version = "1.72" edition = "2021" license = "MIT" @@ -143,4 +143,4 @@ optional = true [dependencies.zino-core] path = "../zino-core" -version = "0.12.3" +version = "0.12.4"