diff --git a/Cargo.lock b/Cargo.lock index 9caffb1..bd83c2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5337,12 +5337,21 @@ version = "0.0.4" [[package]] name = "shield-dioxus" version = "0.0.4" +dependencies = [ + "async-trait", + "dioxus", + "shield", +] [[package]] name = "shield-dioxus-axum" version = "0.0.4" dependencies = [ + "async-trait", + "dioxus", + "shield", "shield-axum", + "shield-dioxus", ] [[package]] @@ -5374,6 +5383,7 @@ dependencies = [ "axum", "dioxus", "shield", + "shield-dioxus", "shield-dioxus-axum", "shield-memory", "shield-oidc", diff --git a/examples/dioxus-axum/Cargo.toml b/examples/dioxus-axum/Cargo.toml index b2892d6..a697e58 100644 --- a/examples/dioxus-axum/Cargo.toml +++ b/examples/dioxus-axum/Cargo.toml @@ -30,6 +30,7 @@ web = ["dioxus/web"] axum = { workspace = true, optional = true } dioxus = { workspace = true, features = ["router", "fullstack"] } shield.workspace = true +shield-dioxus.workspace = true shield-dioxus-axum = { workspace = true, optional = true } shield-memory = { workspace = true, optional = true } shield-oidc = { workspace = true, features = ["native-tls"], optional = true } diff --git a/examples/dioxus-axum/src/app.rs b/examples/dioxus-axum/src/app.rs index 42c969b..edecc7f 100644 --- a/examples/dioxus-axum/src/app.rs +++ b/examples/dioxus-axum/src/app.rs @@ -1,12 +1,17 @@ use dioxus::prelude::*; +use shield_dioxus::ShieldRouter; use crate::home::Home; -#[derive(Debug, Clone, Routable, PartialEq)] +#[derive(Clone, Debug, PartialEq, Routable)] #[rustfmt::skip] enum Route { #[route("/")] Home {}, + #[child("/auth")] + Auth { + child: ShieldRouter + }, } #[component] diff --git a/examples/dioxus-axum/src/main.rs b/examples/dioxus-axum/src/main.rs index b99a94d..f34f878 100644 --- a/examples/dioxus-axum/src/main.rs +++ b/examples/dioxus-axum/src/main.rs @@ -19,8 +19,8 @@ async fn main() { prelude::{DioxusRouterExt, *}, }; use shield::{Shield, ShieldOptions}; - use shield_dioxus_axum::ShieldLayer; - use shield_memory::MemoryStorage; + use shield_dioxus_axum::{ShieldLayer, provide_axum_integration}; + use shield_memory::{MemoryStorage, User}; use shield_oidc::{Keycloak, OidcMethod}; use tokio::net::TcpListener; use tower_sessions::{Expiry, MemoryStore, SessionManagerLayer, cookie::time::Duration}; @@ -59,7 +59,13 @@ async fn main() { // Initialize router let router = Router::new() - .serve_dioxus_application(ServeConfig::new().unwrap(), App) + .serve_dioxus_application( + ServeConfigBuilder::new() + .context_provider(provide_axum_integration::) + .build() + .unwrap(), + App, + ) .layer(shield_layer) .layer(session_layer); diff --git a/packages/core/shield/src/form.rs b/packages/core/shield/src/form.rs index 24a4bfc..8ea6932 100644 --- a/packages/core/shield/src/form.rs +++ b/packages/core/shield/src/form.rs @@ -1,21 +1,23 @@ use std::collections::HashMap; +use serde::{Deserialize, Serialize}; + /// HTML [attribute](https://html.spec.whatwg.org/multipage/syntax.html#attributes-2). -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub enum Attribute { Boolean(bool), String(String), } /// HTML [form](https://html.spec.whatwg.org/multipage/forms.html#the-form-element). -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Form { pub inputs: Vec, pub attributes: Option>, } /// HTML [input](https://html.spec.whatwg.org/multipage/input.html#the-input-element). -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Input { pub name: String, pub label: Option, @@ -25,195 +27,261 @@ pub struct Input { } /// HTML input [type](https://html.spec.whatwg.org/multipage/input.html#attr-input-type) and [attributes](https://html.spec.whatwg.org/multipage/input.html#input-type-attr-summary). -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub enum InputType { - Button { - popovertarget: Option, - popovertargetaction: Option, - }, - Checkbox { - checked: Option, - required: Option, - }, - Color { - alpha: Option, - autocomplete: Option, - colorspace: Option, - list: Option, - }, - Date { - autocomplete: Option, - list: Option, - max: Option, - min: Option, - readonly: Option, - required: Option, - step: Option, - }, - DatetimeLocal { - autocomplete: Option, - list: Option, - max: Option, - min: Option, - readonly: Option, - required: Option, - step: Option, - }, - Email { - autocomplete: Option, - dirname: Option, - list: Option, - maxlength: Option, - minlength: Option, - multiple: Option, - pattern: Option, - placeholder: Option, - readonly: Option, - required: Option, - size: Option, - }, - File { - accept: Option, - multiple: Option, - required: Option, - }, - Hidden { - autocomplete: Option, - dirname: Option, - required: Option, - }, - Image { - alt: Option, - formaction: Option, - formenctype: Option, - formmethod: Option, - formnovalidate: Option, - formtarget: Option, - height: Option, - popovertarget: Option, - popovertargetaction: Option, - src: Option, - width: Option, - }, - Month { - autocomplete: Option, - list: Option, - max: Option, - min: Option, - readonly: Option, - required: Option, - step: Option, - }, - Number { - autocomplete: Option, - list: Option, - max: Option, - min: Option, - placeholder: Option, - readonly: Option, - required: Option, - step: Option, - }, - Password { - autocomplete: Option, - dirname: Option, - maxlength: Option, - minlength: Option, - pattern: Option, - placeholder: Option, - readonly: Option, - required: Option, - size: Option, - }, - Radio { - checked: Option, - required: Option, - }, - Range { - autocomplete: Option, - list: Option, - max: Option, - min: Option, - step: Option, - }, - Reset { - popovertarget: Option, - popovertargetaction: Option, - }, - Search { - autocomplete: Option, - dirname: Option, - list: Option, - maxlength: Option, - minlength: Option, - pattern: Option, - placeholder: Option, - readonly: Option, - required: Option, - size: Option, - }, - Submit { - dirname: Option, - formaction: Option, - formenctype: Option, - formmethod: Option, - formnovalidate: Option, - formtarget: Option, - popovertarget: Option, - popovertargetaction: Option, - }, - Tel { - autocomplete: Option, - dirname: Option, - list: Option, - maxlength: Option, - minlength: Option, - pattern: Option, - placeholder: Option, - readonly: Option, - required: Option, - size: Option, - }, - Text { - autocomplete: Option, - dirname: Option, - list: Option, - maxlength: Option, - minlength: Option, - pattern: Option, - placeholder: Option, - readonly: Option, - required: Option, - size: Option, - }, - Time { - autocomplete: Option, - list: Option, - max: Option, - min: Option, - readonly: Option, - required: Option, - step: Option, - }, - Url { - autocomplete: Option, - dirname: Option, - list: Option, - maxlength: Option, - minlength: Option, - pattern: Option, - placeholder: Option, - readonly: Option, - required: Option, - size: Option, - }, - Week { - autocomplete: Option, - list: Option, - max: Option, - min: Option, - readonly: Option, - required: Option, - step: Option, - }, + Button(InputTypeButton), + Checkbox(InputTypeCheckbox), + Color(InputTypeColor), + Date(InputTypeDate), + DatetimeLocal(InputTypeDatetimeLocal), + Email(InputTypeEmail), + File(InputTypeFile), + Hidden(InputTypeHidden), + Image(InputTypeImage), + Month(InputTypeMonth), + Number(InputTypeNumber), + Password(InputTypePassword), + Radio(InputTypeRadio), + Range(InputTypeRange), + Reset(InputTypeReset), + Search(InputTypeSearch), + Submit(InputTypeSubmit), + Tel(InputTypeTel), + Text(InputTypeText), + Time(InputTypeTime), + Url(InputTypeUrl), + Week(InputTypeWeek), +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeButton { + pub popovertarget: Option, + pub popovertargetaction: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeCheckbox { + pub checked: Option, + pub required: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeColor { + pub alpha: Option, + pub autocomplete: Option, + pub colorspace: Option, + pub list: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeDate { + pub autocomplete: Option, + pub list: Option, + pub max: Option, + pub min: Option, + pub readonly: Option, + pub required: Option, + pub step: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeDatetimeLocal { + pub autocomplete: Option, + pub list: Option, + pub max: Option, + pub min: Option, + pub readonly: Option, + pub required: Option, + pub step: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeEmail { + pub autocomplete: Option, + pub dirname: Option, + pub list: Option, + pub maxlength: Option, + pub minlength: Option, + pub multiple: Option, + pub pattern: Option, + pub placeholder: Option, + pub readonly: Option, + pub required: Option, + pub size: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeFile { + pub accept: Option, + pub multiple: Option, + pub required: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeHidden { + pub autocomplete: Option, + pub dirname: Option, + pub required: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeImage { + pub alt: Option, + pub formaction: Option, + pub formenctype: Option, + pub formmethod: Option, + pub formnovalidate: Option, + pub formtarget: Option, + pub height: Option, + pub popovertarget: Option, + pub popovertargetaction: Option, + pub src: Option, + pub width: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeMonth { + pub autocomplete: Option, + pub list: Option, + pub max: Option, + pub min: Option, + pub readonly: Option, + pub required: Option, + pub step: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeNumber { + pub autocomplete: Option, + pub list: Option, + pub max: Option, + pub min: Option, + pub placeholder: Option, + pub readonly: Option, + pub required: Option, + pub step: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypePassword { + pub autocomplete: Option, + pub dirname: Option, + pub maxlength: Option, + pub minlength: Option, + pub pattern: Option, + pub placeholder: Option, + pub readonly: Option, + pub required: Option, + pub size: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeRadio { + pub checked: Option, + pub required: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeRange { + pub autocomplete: Option, + pub list: Option, + pub max: Option, + pub min: Option, + pub step: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeReset { + pub popovertarget: Option, + pub popovertargetaction: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeSearch { + pub autocomplete: Option, + pub dirname: Option, + pub list: Option, + pub maxlength: Option, + pub minlength: Option, + pub pattern: Option, + pub placeholder: Option, + pub readonly: Option, + pub required: Option, + pub size: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeSubmit { + pub dirname: Option, + pub formaction: Option, + pub formenctype: Option, + pub formmethod: Option, + pub formnovalidate: Option, + pub formtarget: Option, + pub popovertarget: Option, + pub popovertargetaction: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeTel { + pub autocomplete: Option, + pub dirname: Option, + pub list: Option, + pub maxlength: Option, + pub minlength: Option, + pub pattern: Option, + pub placeholder: Option, + pub readonly: Option, + pub required: Option, + pub size: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeText { + pub autocomplete: Option, + pub dirname: Option, + pub list: Option, + pub maxlength: Option, + pub minlength: Option, + pub pattern: Option, + pub placeholder: Option, + pub readonly: Option, + pub required: Option, + pub size: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeTime { + pub autocomplete: Option, + pub list: Option, + pub max: Option, + pub min: Option, + pub readonly: Option, + pub required: Option, + pub step: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeUrl { + pub autocomplete: Option, + pub dirname: Option, + pub list: Option, + pub maxlength: Option, + pub minlength: Option, + pub pattern: Option, + pub placeholder: Option, + pub readonly: Option, + pub required: Option, + pub size: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InputTypeWeek { + pub autocomplete: Option, + pub list: Option, + pub max: Option, + pub min: Option, + pub readonly: Option, + pub required: Option, + pub step: Option, } diff --git a/packages/core/shield/src/shield.rs b/packages/core/shield/src/shield.rs index 361fce6..91f4ea5 100644 --- a/packages/core/shield/src/shield.rs +++ b/packages/core/shield/src/shield.rs @@ -3,7 +3,8 @@ use std::{any::Any, collections::HashMap, sync::Arc}; use futures::future::try_join_all; use crate::{ - error::ShieldError, method::ErasedMethod, options::ShieldOptions, storage::Storage, user::User, + Form, error::ShieldError, method::ErasedMethod, options::ShieldOptions, storage::Storage, + user::User, }; #[derive(Clone)] @@ -62,6 +63,23 @@ impl Shield { None => Ok(None), } } + + pub async fn action_forms(&self, action_id: &str) -> Result, ShieldError> { + let mut forms = vec![]; + + for (_, method) in self.methods.iter() { + let Some(action) = method.erased_action_by_id(action_id) else { + continue; + }; + + for provider in method.erased_providers().await? { + let form = action.erased_render(provider); + forms.push(form); + } + } + + Ok(forms) + } } #[cfg(test)] diff --git a/packages/core/shield/src/shield_dyn.rs b/packages/core/shield/src/shield_dyn.rs index cccd81a..a8cd474 100644 --- a/packages/core/shield/src/shield_dyn.rs +++ b/packages/core/shield/src/shield_dyn.rs @@ -2,11 +2,13 @@ use std::{any::Any, sync::Arc}; use async_trait::async_trait; -use crate::{error::ShieldError, shield::Shield, user::User}; +use crate::{Form, error::ShieldError, shield::Shield, user::User}; #[async_trait] pub trait DynShield: Send + Sync { async fn providers(&self) -> Result>, ShieldError>; + + async fn action_forms(&self, action_id: &str) -> Result, ShieldError>; } #[async_trait] @@ -14,6 +16,10 @@ impl DynShield for Shield { async fn providers(&self) -> Result>, ShieldError> { self.providers().await } + + async fn action_forms(&self, action_id: &str) -> Result, ShieldError> { + self.action_forms(action_id).await + } } pub struct ShieldDyn(Arc); @@ -26,4 +32,8 @@ impl ShieldDyn { pub async fn providers(&self) -> Result>, ShieldError> { self.0.providers().await } + + pub async fn action_forms(&self, action_id: &str) -> Result, ShieldError> { + self.0.action_forms(action_id).await + } } diff --git a/packages/integrations/shield-dioxus-axum/Cargo.toml b/packages/integrations/shield-dioxus-axum/Cargo.toml index abd80c6..efaf4c6 100644 --- a/packages/integrations/shield-dioxus-axum/Cargo.toml +++ b/packages/integrations/shield-dioxus-axum/Cargo.toml @@ -13,4 +13,8 @@ default = [] utoipa = ["shield-axum/utoipa"] [dependencies] +async-trait.workspace = true +dioxus = { workspace = true, features = ["server"] } +shield.workspace = true shield-axum.workspace = true +shield-dioxus.workspace = true diff --git a/packages/integrations/shield-dioxus-axum/src/integration.rs b/packages/integrations/shield-dioxus-axum/src/integration.rs new file mode 100644 index 0000000..b1d3980 --- /dev/null +++ b/packages/integrations/shield-dioxus-axum/src/integration.rs @@ -0,0 +1,35 @@ +use std::marker::PhantomData; + +use async_trait::async_trait; +use dioxus::prelude::extract; + +use shield::{Session, ShieldDyn, User}; +use shield_axum::{ExtractSession, ExtractShield}; +use shield_dioxus::{DioxusIntegration, DioxusIntegrationDyn}; + +pub struct DioxusAxumIntegration(PhantomData); + +impl Default for DioxusAxumIntegration { + fn default() -> Self { + Self(Default::default()) + } +} + +#[async_trait] +impl DioxusIntegration for DioxusAxumIntegration { + async fn extract_shield(&self) -> ShieldDyn { + let ExtractShield(shield) = extract::, _>().await.expect("TODO"); + + ShieldDyn::new(shield) + } + + async fn extract_session(&self) -> Session { + let ExtractSession(session) = extract().await.expect("TODO"); + + session + } +} + +pub fn provide_axum_integration() -> DioxusIntegrationDyn { + DioxusIntegrationDyn::new(DioxusAxumIntegration::::default()) +} diff --git a/packages/integrations/shield-dioxus-axum/src/lib.rs b/packages/integrations/shield-dioxus-axum/src/lib.rs index 94cc1b6..01c518a 100644 --- a/packages/integrations/shield-dioxus-axum/src/lib.rs +++ b/packages/integrations/shield-dioxus-axum/src/lib.rs @@ -1 +1,5 @@ +mod integration; + pub use shield_axum::*; + +pub use integration::*; diff --git a/packages/integrations/shield-dioxus/Cargo.toml b/packages/integrations/shield-dioxus/Cargo.toml index 11450c4..c8750a2 100644 --- a/packages/integrations/shield-dioxus/Cargo.toml +++ b/packages/integrations/shield-dioxus/Cargo.toml @@ -9,3 +9,6 @@ repository.workspace = true version.workspace = true [dependencies] +async-trait.workspace = true +dioxus = { workspace = true, features = ["router"] } +shield.workspace = true diff --git a/packages/integrations/shield-dioxus/src/form.rs b/packages/integrations/shield-dioxus/src/form.rs new file mode 100644 index 0000000..6bd830f --- /dev/null +++ b/packages/integrations/shield-dioxus/src/form.rs @@ -0,0 +1,332 @@ +use dioxus::prelude::*; +use shield::{Form, Input, InputType}; + +pub trait ToRsx { + fn to_rsx(&self) -> Element; +} + +impl ToRsx for Form { + fn to_rsx(&self) -> Element { + rsx! { + form { + {self.inputs.iter().map(ToRsx::to_rsx)} + } + } + } +} + +impl ToRsx for Input { + fn to_rsx(&self) -> Element { + let input = match &self.r#type { + InputType::Button(button) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "button", + popovertarget: button.popovertarget.clone(), + popovertargetaction: button.popovertargetaction.clone(), + } + }, + InputType::Checkbox(checkbox) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "checkbox", + checked: checkbox.checked, + required: checkbox.required, + } + }, + InputType::Color(color) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "color", + "alpha": color.alpha, + autocomplete: color.autocomplete.clone(), + "colorspace": color.colorspace.clone(), + list: color.list.clone(), + } + }, + InputType::Date(date) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "date", + autocomplete: date.autocomplete.clone(), + list: date.list.clone(), + max: date.max.clone(), + min: date.min.clone(), + readonly: date.readonly, + required: date.required, + step: date.step.clone(), + } + }, + InputType::DatetimeLocal(datetime_local) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "datetime-local", + autocomplete: datetime_local.autocomplete.clone(), + list: datetime_local.list.clone(), + max: datetime_local.max.clone(), + min: datetime_local.min.clone(), + readonly: datetime_local.readonly, + required: datetime_local.required, + step: datetime_local.step.clone(), + } + }, + InputType::Email(email) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "email", + autocomplete: email.autocomplete.clone(), + "dirname": email.dirname.clone(), + list: email.list.clone(), + maxlength: email.maxlength.clone(), + minlength: email.minlength.clone(), + multiple: email.multiple, + pattern: email.pattern.clone(), + placeholder: email.placeholder.clone(), + readonly: email.readonly, + required: email.required, + size: email.size.clone(), + } + }, + InputType::File(file) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "file", + accept: file.accept.clone(), + multiple: file.multiple, + required: file.required, + } + }, + InputType::Hidden(hidden) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "hidden", + autocomplete: hidden.autocomplete.clone(), + "dirname": hidden.dirname.clone(), + required: hidden.required, + } + }, + InputType::Image(image) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "image", + alt: image.alt.clone(), + formaction: image.formaction.clone(), + formenctype: image.formenctype.clone(), + formmethod: image.formmethod.clone(), + formnovalidate: image.formnovalidate, + formtarget: image.formtarget.clone(), + height: image.height.clone(), + popovertarget: image.popovertarget.clone(), + popovertargetaction: image.popovertargetaction.clone(), + src: image.src.clone(), + width: image.width.clone(), + } + }, + InputType::Month(month) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "month", + autocomplete: month.autocomplete.clone(), + list: month.list.clone(), + max: month.max.clone(), + min: month.min.clone(), + readonly: month.readonly, + required: month.required, + step: month.step.clone(), + } + }, + InputType::Number(number) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "number", + autocomplete: number.autocomplete.clone(), + list: number.list.clone(), + max: number.max.clone(), + min: number.min.clone(), + placeholder: number.placeholder.clone(), + readonly: number.readonly, + required: number.required, + step: number.step.clone(), + } + }, + InputType::Password(password) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "password", + autocomplete: password.autocomplete.clone(), + "dirname": password.dirname.clone(), + maxlength: password.maxlength.clone(), + minlength: password.minlength.clone(), + pattern: password.pattern.clone(), + placeholder: password.placeholder.clone(), + readonly: password.readonly, + required: password.required, + size: password.size.clone(), + } + }, + InputType::Radio(radio) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "radio", + checked: radio.checked, + required: radio.required, + } + }, + InputType::Range(range) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "range", + autocomplete: range.autocomplete.clone(), + list: range.list.clone(), + max: range.max.clone(), + min: range.min.clone(), + step: range.step.clone(), + } + }, + InputType::Reset(reset) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "reset", + popovertarget: reset.popovertarget.clone(), + popovertargetaction: reset.popovertargetaction.clone(), + } + }, + InputType::Search(search) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "search", + autocomplete: search.autocomplete.clone(), + "dirname": search.dirname.clone(), + list: search.list.clone(), + maxlength: search.maxlength.clone(), + minlength: search.minlength.clone(), + pattern: search.pattern.clone(), + placeholder: search.placeholder.clone(), + readonly: search.readonly, + required: search.required, + size: search.size.clone(), + } + }, + InputType::Submit(submit) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "submit", + "dirname": submit.dirname.clone(), + formaction: submit.formaction.clone(), + formenctype: submit.formenctype.clone(), + formmethod: submit.formmethod.clone(), + formnovalidate: submit.formnovalidate, + formtarget: submit.formtarget.clone(), + popovertarget: submit.popovertarget.clone(), + popovertargetaction: submit.popovertargetaction.clone(), + } + }, + InputType::Tel(tel) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "tel", + autocomplete: tel.autocomplete.clone(), + "dirname": tel.dirname.clone(), + list: tel.list.clone(), + maxlength: tel.maxlength.clone(), + minlength: tel.minlength.clone(), + pattern: tel.pattern.clone(), + placeholder: tel.placeholder.clone(), + readonly: tel.readonly, + required: tel.required, + size: tel.size.clone(), + } + }, + InputType::Text(text) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "text", + autocomplete: text.autocomplete.clone(), + "dirname": text.dirname.clone(), + list: text.list.clone(), + maxlength: text.maxlength.clone(), + minlength: text.minlength.clone(), + pattern: text.pattern.clone(), + placeholder: text.placeholder.clone(), + readonly: text.readonly, + required: text.required, + size: text.size.clone(), + } + }, + InputType::Time(time) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "time", + autocomplete: time.autocomplete.clone(), + list: time.list.clone(), + max: time.max.clone(), + min: time.min.clone(), + readonly: time.readonly, + required: time.required, + step: time.step.clone(), + } + }, + InputType::Url(url) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "url", + autocomplete: url.autocomplete.clone(), + "dirname": url.dirname.clone(), + list: url.list.clone(), + maxlength: url.maxlength.clone(), + minlength: url.minlength.clone(), + pattern: url.pattern.clone(), + placeholder: url.placeholder.clone(), + readonly: url.readonly, + required: url.required, + size: url.size.clone(), + } + }, + InputType::Week(week) => rsx! { + input { + name: self.name.clone(), + value: self.value.clone(), + r#type: "week", + autocomplete: week.autocomplete.clone(), + list: week.list.clone(), + max: week.max.clone(), + min: week.min.clone(), + readonly: week.readonly, + required: week.required, + step: week.step.clone(), + } + }, + }; + + rsx! { + div { + if let Some(label) = &self.label { + label { "{label}" } + } + + {input} + } + } + } +} diff --git a/packages/integrations/shield-dioxus/src/integration.rs b/packages/integrations/shield-dioxus/src/integration.rs new file mode 100644 index 0000000..4a89740 --- /dev/null +++ b/packages/integrations/shield-dioxus/src/integration.rs @@ -0,0 +1,24 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use shield::{Session, ShieldDyn}; + +#[async_trait] +pub trait DioxusIntegration: Send + Sync { + async fn extract_shield(&self) -> ShieldDyn; + + async fn extract_session(&self) -> Session; +} + +#[derive(Clone)] +pub struct DioxusIntegrationDyn(Arc); + +impl DioxusIntegrationDyn { + pub fn new(integration: I) -> Self { + Self(Arc::new(integration)) + } + + pub async fn extract_shield(&self) -> ShieldDyn { + self.0.extract_shield().await + } +} diff --git a/packages/integrations/shield-dioxus/src/lib.rs b/packages/integrations/shield-dioxus/src/lib.rs index 8b13789..ac97ca9 100644 --- a/packages/integrations/shield-dioxus/src/lib.rs +++ b/packages/integrations/shield-dioxus/src/lib.rs @@ -1 +1,7 @@ +mod form; +mod integration; +mod router; +mod routes; +pub use integration::*; +pub use router::*; diff --git a/packages/integrations/shield-dioxus/src/router.rs b/packages/integrations/shield-dioxus/src/router.rs new file mode 100644 index 0000000..3021a7d --- /dev/null +++ b/packages/integrations/shield-dioxus/src/router.rs @@ -0,0 +1,9 @@ +use dioxus::prelude::*; + +use crate::routes::Action; + +#[derive(Clone, Debug, PartialEq, Routable)] +pub enum ShieldRouter { + #[route("/:action_id")] + Action { action_id: String }, +} diff --git a/packages/integrations/shield-dioxus/src/routes.rs b/packages/integrations/shield-dioxus/src/routes.rs new file mode 100644 index 0000000..8bd911f --- /dev/null +++ b/packages/integrations/shield-dioxus/src/routes.rs @@ -0,0 +1,3 @@ +mod action; + +pub use action::*; diff --git a/packages/integrations/shield-dioxus/src/routes/action.rs b/packages/integrations/shield-dioxus/src/routes/action.rs new file mode 100644 index 0000000..778fb83 --- /dev/null +++ b/packages/integrations/shield-dioxus/src/routes/action.rs @@ -0,0 +1,38 @@ +use dioxus::prelude::*; +use shield::Form; + +use crate::{DioxusIntegrationDyn, form::ToRsx}; + +#[derive(Clone, PartialEq, Props)] +pub struct ActionProps { + action_id: String, +} + +#[component] +pub fn Action(props: ActionProps) -> Element { + let response = use_server_future({ + let action_id = props.action_id.clone(); + + move || forms(action_id.clone()) + })?; + + let response_read = response.read(); + let response = response_read.as_ref().unwrap(); + + match response { + Ok(forms) => rsx! { + {forms.iter().map(ToRsx::to_rsx)} + }, + Err(err) => rsx! { "{err}" }, + } +} + +#[server] +async fn forms(action_id: String) -> Result, ServerFnError> { + let FromContext(integration): FromContext = extract().await?; + let shield = integration.extract_shield().await; + + let forms = shield.action_forms(&action_id).await?; + + Ok(forms) +} diff --git a/packages/methods/shield-credentials/src/email_password.rs b/packages/methods/shield-credentials/src/email_password.rs index ae2d59d..011ae1e 100644 --- a/packages/methods/shield-credentials/src/email_password.rs +++ b/packages/methods/shield-credentials/src/email_password.rs @@ -2,7 +2,7 @@ use std::{pin::Pin, sync::Arc}; use async_trait::async_trait; use serde::Deserialize; -use shield::{Form, Input, InputType, ShieldError, User}; +use shield::{Form, Input, InputType, InputTypeEmail, InputTypePassword, ShieldError, User}; use crate::Credentials; @@ -44,36 +44,24 @@ impl Credentials for EmailPasswordCredentials Input { name: "email".to_owned(), label: Some("Email address".to_owned()), - r#type: InputType::Email { + r#type: InputType::Email(InputTypeEmail { autocomplete: Some("email".to_owned()), - dirname: None, - list: None, - maxlength: None, - minlength: None, - multiple: None, - pattern: None, placeholder: Some("Email address".to_owned()), - readonly: None, required: Some(true), - size: None, - }, + ..Default::default() + }), value: None, attributes: None, }, Input { name: "password".to_owned(), label: Some("Password".to_owned()), - r#type: InputType::Password { + r#type: InputType::Password(InputTypePassword { autocomplete: Some("current-password".to_owned()), - dirname: None, - maxlength: None, - minlength: None, - pattern: None, placeholder: Some("Password".to_owned()), - readonly: None, required: Some(true), - size: None, - }, + ..Default::default() + }), value: None, attributes: None, }, diff --git a/packages/methods/shield-credentials/src/username_password.rs b/packages/methods/shield-credentials/src/username_password.rs index 89df58d..5e502e2 100644 --- a/packages/methods/shield-credentials/src/username_password.rs +++ b/packages/methods/shield-credentials/src/username_password.rs @@ -2,7 +2,7 @@ use std::{pin::Pin, sync::Arc}; use async_trait::async_trait; use serde::Deserialize; -use shield::{Form, Input, InputType, ShieldError, User}; +use shield::{Form, Input, InputType, InputTypePassword, InputTypeText, ShieldError, User}; use crate::Credentials; @@ -44,35 +44,24 @@ impl Credentials for UsernamePasswordCredentia Input { name: "username".to_owned(), label: Some("Username".to_owned()), - r#type: InputType::Text { + r#type: InputType::Text(InputTypeText { autocomplete: Some("username".to_owned()), - dirname: None, - list: None, - maxlength: None, - minlength: None, - pattern: None, placeholder: Some("Username".to_owned()), - readonly: None, required: Some(true), - size: None, - }, + ..Default::default() + }), value: None, attributes: None, }, Input { name: "password".to_owned(), label: Some("Password".to_owned()), - r#type: InputType::Password { + r#type: InputType::Password(InputTypePassword { autocomplete: Some("current-password".to_owned()), - dirname: None, - maxlength: None, - minlength: None, - pattern: None, placeholder: Some("Password".to_owned()), - readonly: None, required: Some(true), - size: None, - }, + ..Default::default() + }), value: None, attributes: None, }, diff --git a/packages/methods/shield-oidc/src/actions/sign_in.rs b/packages/methods/shield-oidc/src/actions/sign_in.rs index d3b1602..e53afac 100644 --- a/packages/methods/shield-oidc/src/actions/sign_in.rs +++ b/packages/methods/shield-oidc/src/actions/sign_in.rs @@ -4,8 +4,8 @@ use openidconnect::{ url::form_urlencoded::parse, }; use shield::{ - Action, Form, Request, Response, SIGN_IN_ACTION_ID, Session, SessionError, ShieldError, - erased_action, + Action, Form, Input, InputType, InputTypeSubmit, Provider, Request, Response, + SIGN_IN_ACTION_ID, Session, SessionError, ShieldError, erased_action, }; use crate::{ @@ -22,9 +22,15 @@ impl Action for OidcSignInAction { SIGN_IN_ACTION_ID.to_owned() } - fn render(&self, _provider: OidcProvider) -> Form { + fn render(&self, provider: OidcProvider) -> Form { Form { - inputs: vec![], + inputs: vec![Input { + name: "submit".to_owned(), + label: None, + r#type: InputType::Submit(InputTypeSubmit::default()), + value: Some(format!("Sign in with {}", provider.name())), + attributes: None, + }], attributes: None, } }