diff --git a/Cargo.lock b/Cargo.lock index 6f90fb2..3910489 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1727,7 +1727,7 @@ dependencies = [ "send_wrapper", "serde", "serde_json", - "serde_qs", + "serde_qs 0.15.0", "serde_urlencoded", "thiserror 2.0.18", "tokio", @@ -2006,7 +2006,7 @@ dependencies = [ "rustc-hash 2.1.1", "serde", "serde_json", - "serde_qs", + "serde_qs 0.15.0", "subsecond", "thiserror 2.0.18", "tokio", @@ -3406,7 +3406,7 @@ dependencies = [ "send_wrapper", "serde", "serde_json", - "serde_qs", + "serde_qs 0.15.0", "server_fn", "slotmap", "tachys", @@ -5584,6 +5584,18 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "serde_qs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac22439301a0b6f45a037681518e3169e8db1db76080e2e9600a08d1027df037" +dependencies = [ + "itoa", + "percent-encoding", + "ryu", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -5674,7 +5686,7 @@ dependencies = [ "send_wrapper", "serde", "serde_json", - "serde_qs", + "serde_qs 0.15.0", "server_fn_macro_default", "thiserror 2.0.18", "throw_error", @@ -5828,6 +5840,7 @@ dependencies = [ "anyhow", "dioxus", "serde_json", + "serde_qs 1.0.0", "shield", "tracing", ] diff --git a/packages/core/shield/src/shield.rs b/packages/core/shield/src/shield.rs index f14f015..56bd16b 100644 --- a/packages/core/shield/src/shield.rs +++ b/packages/core/shield/src/shield.rs @@ -139,12 +139,16 @@ impl Shield { } } + provider_forms.sort_by(|a, b| a.id.cmp(&b.id)); + method_forms.push(ActionMethodForm { id: method_id.clone(), provider_forms, }); } + method_forms.sort_by(|a, b| a.id.cmp(&b.id)); + Ok(ActionForms { id: action_id.to_owned(), name: action_name.unwrap_or(action_id.to_owned()), diff --git a/packages/integrations/shield-dioxus/Cargo.toml b/packages/integrations/shield-dioxus/Cargo.toml index 911592b..367bdd4 100644 --- a/packages/integrations/shield-dioxus/Cargo.toml +++ b/packages/integrations/shield-dioxus/Cargo.toml @@ -15,5 +15,6 @@ server = ["dioxus/server"] anyhow.workspace = true dioxus = { workspace = true, features = ["fullstack", "router"] } serde_json.workspace = true +serde_qs = "1.0.0" shield.workspace = true tracing.workspace = true diff --git a/packages/integrations/shield-dioxus/src/lib.rs b/packages/integrations/shield-dioxus/src/lib.rs index e97cbd7..6ce5936 100644 --- a/packages/integrations/shield-dioxus/src/lib.rs +++ b/packages/integrations/shield-dioxus/src/lib.rs @@ -1,9 +1,11 @@ mod integration; +mod query; mod router; mod routes; mod style; pub use integration::*; +pub use query::*; pub use router::*; pub use routes::call; pub use style::*; diff --git a/packages/integrations/shield-dioxus/src/query.rs b/packages/integrations/shield-dioxus/src/query.rs new file mode 100644 index 0000000..883c5f1 --- /dev/null +++ b/packages/integrations/shield-dioxus/src/query.rs @@ -0,0 +1,18 @@ +use std::{collections::HashMap, ops::Deref}; + +#[derive(Clone, Debug)] +pub struct Query(HashMap); + +impl Query { + pub fn parse(query: &str) -> Self { + Self(serde_qs::from_str::>(query).unwrap_or_default()) + } +} + +impl Deref for Query { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/packages/integrations/shield-dioxus/src/router.rs b/packages/integrations/shield-dioxus/src/router.rs index e4db693..8d070cc 100644 --- a/packages/integrations/shield-dioxus/src/router.rs +++ b/packages/integrations/shield-dioxus/src/router.rs @@ -4,8 +4,8 @@ use crate::routes::Action; #[derive(Clone, Debug, PartialEq, Routable)] pub enum ShieldRouter { - #[route("", Action)] - ActionIndex, - #[route("/:action_id")] - Action { action_id: String }, + #[route("?:..query", Action)] + ActionIndex { query: String }, + #[route("/:action_id?:..query")] + Action { action_id: String, query: String }, } diff --git a/packages/integrations/shield-dioxus/src/routes/action.rs b/packages/integrations/shield-dioxus/src/routes/action.rs index c80cae1..5b043d5 100644 --- a/packages/integrations/shield-dioxus/src/routes/action.rs +++ b/packages/integrations/shield-dioxus/src/routes/action.rs @@ -2,12 +2,13 @@ use dioxus::prelude::*; use serde_json::Value; use shield::{ActionForms, ResponseType}; -use crate::ErasedDioxusStyle; +use crate::{query::Query, style::ErasedDioxusStyle}; #[derive(Clone, PartialEq, Props)] pub struct ActionProps { #[props(default = "index".to_owned())] action_id: String, + query: String, } #[component] @@ -22,6 +23,8 @@ pub fn Action(props: ActionProps) -> Element { let response_read = response.read(); let response = response_read.as_ref().unwrap(); + use_context_provider(|| Query::parse(&props.query)); + match response { Ok(forms) => style.render(forms), Err(err) => rsx! { "{err}" }, diff --git a/packages/methods/shield-email/src/actions/sign_in.rs b/packages/methods/shield-email/src/actions/sign_in.rs index 5e83030..a129399 100644 --- a/packages/methods/shield-email/src/actions/sign_in.rs +++ b/packages/methods/shield-email/src/actions/sign_in.rs @@ -97,24 +97,19 @@ impl Action for EmailSignInAction { .map_err(|err| ShieldError::Validation(err.to_string()))?; let token = Alphanumeric.sample_string(&mut rand::rng(), 32); - let expires_at = Utc::now() + self.options.expires_in; let email_auth_token = self .storage .create_email_auth_token(CreateEmailAuthToken { email: data.email.to_lowercase(), token: hash_token(&token, &self.options.secret), - expired_at: expires_at.into(), + expired_at: (Utc::now() + self.options.expires_in).into(), }) .await?; self.options .sender - .send( - &email_auth_token.email, - &email_auth_token.token, - email_auth_token.expired_at, - ) + .send(&email_auth_token.email, &token, email_auth_token.expired_at) .await?; Ok(Response::new(ResponseType::Default).session_action(SessionAction::unauthenticate())) diff --git a/packages/methods/shield-email/src/actions/sign_in_callback.rs b/packages/methods/shield-email/src/actions/sign_in_callback.rs index cb97772..b20ae7e 100644 --- a/packages/methods/shield-email/src/actions/sign_in_callback.rs +++ b/packages/methods/shield-email/src/actions/sign_in_callback.rs @@ -5,8 +5,8 @@ use chrono::Utc; use serde::Deserialize; use shield::{ Action, ActionMethod, CreateEmailAddress, CreateUser, Form, Input, InputType, InputTypeEmail, - InputTypeSubmit, InputValue, MethodSession, Request, Response, ResponseType, SessionAction, - ShieldError, SignInCallbackAction, User, erased_action, + InputTypeSubmit, InputTypeText, InputValue, MethodSession, Request, Response, ResponseType, + SessionAction, ShieldError, SignInCallbackAction, User, erased_action, }; use crate::{ @@ -51,7 +51,7 @@ impl Action for EmailSignInCallbackAction< } fn method(&self) -> ActionMethod { - ActionMethod::Get + ActionMethod::Post } fn condition( @@ -74,19 +74,23 @@ impl Action for EmailSignInCallbackAction< required: Some(true), ..Default::default() }), - value: None, + value: Some(InputValue::Query { + key: "email".to_owned(), + }), addon_start: None, addon_end: None, }, Input { name: "token".to_owned(), label: Some("Token".to_owned()), - r#type: InputType::Email(InputTypeEmail { + r#type: InputType::Text(InputTypeText { placeholder: Some("Token".to_owned()), required: Some(true), ..Default::default() }), - value: None, + value: Some(InputValue::Query { + key: "token".to_owned(), + }), addon_start: None, addon_end: None, }, @@ -147,7 +151,10 @@ impl Action for EmailSignInCallbackAction< } }; - Ok(Response::new(ResponseType::Default).session_action(SessionAction::authenticate(user))) + Ok(Response::new(ResponseType::Redirect( + self.options.sign_in_redirect.clone(), + )) + .session_action(SessionAction::authenticate(user))) } } diff --git a/packages/methods/shield-email/src/options.rs b/packages/methods/shield-email/src/options.rs index c4b66f3..bf3e032 100644 --- a/packages/methods/shield-email/src/options.rs +++ b/packages/methods/shield-email/src/options.rs @@ -7,7 +7,7 @@ use secrecy::SecretString; use crate::sender::Sender; #[derive(Builder, Clone)] -#[builder(state_mod(vis = "pub(crate)"))] +#[builder(on(String, into), state_mod(vis = "pub(crate)"))] pub struct EmailOptions { #[builder(into)] pub(crate) secret: SecretString, @@ -17,4 +17,7 @@ pub struct EmailOptions { #[builder(default = TimeDelta::minutes(10))] pub(crate) expires_in: TimeDelta, + + #[builder(default = "/")] + pub(crate) sign_in_redirect: String, } diff --git a/packages/storage/shield-memory/src/methods/email.rs b/packages/storage/shield-memory/src/methods/email.rs index 0991737..740f714 100644 --- a/packages/storage/shield-memory/src/methods/email.rs +++ b/packages/storage/shield-memory/src/methods/email.rs @@ -29,7 +29,7 @@ impl EmailStorage for MemoryStorage { .find(|email_auth_token| { email_auth_token.email == email && email_auth_token.token == token - && email_auth_token.expired_at < Utc::now() + && email_auth_token.expired_at > Utc::now() }) .cloned()) } diff --git a/packages/storage/shield-sea-orm/src/methods/email.rs b/packages/storage/shield-sea-orm/src/methods/email.rs index f607666..00d33fc 100644 --- a/packages/storage/shield-sea-orm/src/methods/email.rs +++ b/packages/storage/shield-sea-orm/src/methods/email.rs @@ -16,7 +16,7 @@ impl EmailStorage for SeaOrmStorage { email_auth_token::Entity::find() .filter(email_auth_token::Column::Email.eq(email)) .filter(email_auth_token::Column::Token.eq(token)) - .filter(email_auth_token::Column::ExpiredAt.lte(Utc::now())) + .filter(email_auth_token::Column::ExpiredAt.gt(Utc::now())) .one(&self.database) .await .map_err(|err| StorageError::Engine(err.to_string())) diff --git a/packages/styles/shield-bootstrap/src/dioxus/form.rs b/packages/styles/shield-bootstrap/src/dioxus/form.rs index ea9a86c..18cdc0a 100644 --- a/packages/styles/shield-bootstrap/src/dioxus/form.rs +++ b/packages/styles/shield-bootstrap/src/dioxus/form.rs @@ -44,19 +44,25 @@ pub fn Form(props: FormProps) -> Element { ).expect("TODO: handle error"); let result = call(action_id, method_id, provider_id, data).await; - info!("{:?}", result); - // TODO: Handle error. - if let Ok(response) = result { - match response { - ResponseType::Default => {}, - ResponseType::Redirect(to) => { - navigator.push(to); - }, - ResponseType::RedirectToAction { action_id } => { - navigator.push(ShieldRouter::Action { action_id }); + match result { + Ok(response) => { + info!("{:?}", response); + + match response { + ResponseType::Default => {}, + ResponseType::Redirect(to) => { + navigator.push(to); + }, + ResponseType::RedirectToAction { action_id } => { + navigator.push(ShieldRouter::Action { action_id, query: "".to_owned() }); + } } } + Err(err) => { + // TODO: Handle error. + error!("{err}"); + } } } } @@ -64,7 +70,7 @@ pub fn Form(props: FormProps) -> Element { for input in props.form.inputs { FormInput { - input: input + input: input, } } } diff --git a/packages/styles/shield-bootstrap/src/dioxus/input.rs b/packages/styles/shield-bootstrap/src/dioxus/input.rs index 6c800f1..c8ce1cf 100644 --- a/packages/styles/shield-bootstrap/src/dioxus/input.rs +++ b/packages/styles/shield-bootstrap/src/dioxus/input.rs @@ -1,5 +1,6 @@ use dioxus::prelude::*; use shield::{Input, InputValue}; +use shield_dioxus::Query; #[derive(Clone, PartialEq, Props)] pub struct FormInputProps { @@ -8,6 +9,8 @@ pub struct FormInputProps { #[component] pub fn FormInput(props: FormInputProps) -> Element { + let query = use_context::(); + rsx! { div { class: "mb-3", @@ -28,7 +31,9 @@ pub fn FormInput(props: FormInputProps) -> Element { type: props.input.r#type.as_str(), value: props.input.value.map(|value| match value { InputValue::Origin => "TODO: origin".to_owned(), - InputValue::Query {key} => format!("TODO: query param {key}"), + InputValue::Query { key } => { + query.get(&key).cloned().unwrap_or_default() + }, InputValue::String { value } => value.clone(), }), placeholder: props.input.label,