diff --git a/Cargo.lock b/Cargo.lock index f8c1ade..62d09fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2829,9 +2829,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.5.10", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -3133,6 +3135,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "keyboard-types" version = "0.7.0" @@ -4113,6 +4130,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -4359,6 +4386,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "querystring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9318ead08c799aad12a55a3e78b82e0b6167271ffd1f627b758891282f739187" + [[package]] name = "quinn" version = "0.11.8" @@ -4670,8 +4703,10 @@ checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-core", "futures-util", + "h2 0.4.10", "http 1.3.1", "http-body", "http-body-util", @@ -4681,6 +4716,7 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime", "mime_guess", "native-tls", "percent-encoding", @@ -5497,7 +5533,9 @@ name = "shield-bootstrap" version = "0.0.4" dependencies = [ "dioxus", + "dioxus-html", "leptos", + "serde_json", "shield", "shield-dioxus", "shield-leptos", @@ -5526,6 +5564,7 @@ dependencies = [ "dioxus", "serde_json", "shield", + "tracing", ] [[package]] @@ -5648,6 +5687,7 @@ dependencies = [ "serde", "serde_json", "shield", + "tracing", ] [[package]] @@ -5751,6 +5791,19 @@ dependencies = [ name = "shield-webauthn" version = "0.0.4" +[[package]] +name = "shield-workos" +version = "0.0.4" +dependencies = [ + "async-trait", + "bon", + "serde", + "serde_json", + "shield", + "tracing", + "workos-sdk", +] + [[package]] name = "shlex" version = "1.3.0" @@ -5788,6 +5841,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.15", + "time", +] + [[package]] name = "slab" version = "0.4.11" @@ -6224,6 +6289,27 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tachys" version = "0.2.6" @@ -7263,6 +7349,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-registry" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -7522,6 +7619,24 @@ dependencies = [ "bitflags", ] +[[package]] +name = "workos-sdk" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd0bf083e05d68ad10b7b85b8a9a21735c9d509bcb32b6d38687afb9014872d" +dependencies = [ + "async-trait", + "chrono", + "derive_more 2.0.1", + "jsonwebtoken", + "querystring", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.15", + "url", +] + [[package]] name = "writeable" version = "0.6.1" diff --git a/Cargo.toml b/Cargo.toml index 85aade0..fe581ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,8 @@ bon = "3.3.2" chrono = "0.4.39" console_error_panic_hook = "0.1.2" dioxus = "0.7.0-rc.0" -dioxus-server = { version = "0.7.0-rc.0" } +dioxus-html = "0.7.0-rc.0" +dioxus-server = "0.7.0-rc.0" futures = "0.3.31" http = "1.2.0" leptos = "0.8.3" @@ -53,6 +54,7 @@ shield-sea-orm = { path = "./packages/storage/shield-sea-orm", version = "0.0.4" shield-sqlx = { path = "./packages/storage/shield-sqlx", version = "0.0.4" } shield-tower = { path = "./packages/integrations/shield-tower", version = "0.0.4" } shield-webauthn = { path = "./packages/methods/shield-webauthn", version = "0.0.4" } +shield-workos = { path = "./packages/methods/shield-workos", version = "0.0.4" } thiserror = "2.0.7" tokio = "1.42.0" tower-layer = "0.3.3" diff --git a/packages/core/shield/src/action.rs b/packages/core/shield/src/action.rs index 406e84f..8e9f36a 100644 --- a/packages/core/shield/src/action.rs +++ b/packages/core/shield/src/action.rs @@ -13,14 +13,20 @@ use crate::{ pub struct ActionForms { pub id: String, pub name: String, - pub forms: Vec, + pub method_forms: Vec, +} + +// TODO: Think of a better name. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct ActionMethodForm { + pub id: String, + pub provider_forms: Vec, } // TODO: Think of a better name. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct ActionProviderForm { - pub method_id: String, - pub provider_id: Option, + pub id: Option, pub form: Form, } @@ -34,7 +40,7 @@ pub trait Action: ErasedAction + Send + Sync { Ok(true) } - fn form(&self, provider: P) -> Form; + fn forms(&self, provider: P) -> Vec
; async fn call( &self, @@ -56,7 +62,7 @@ pub trait ErasedAction: Send + Sync { session: Session, ) -> Result; - fn erased_form(&self, provider: Box) -> Form; + fn erased_forms(&self, provider: Box) -> Vec; async fn erased_call( &self, @@ -83,8 +89,8 @@ macro_rules! erased_action { self.condition(provider.downcast_ref().expect("TODO"), session) } - fn erased_form(&self, provider: Box) -> $crate::Form { - self.form(*provider.downcast().expect("TODO")) + fn erased_forms(&self, provider: Box) -> Vec<$crate::Form> { + self.forms(*provider.downcast().expect("TODO")) } async fn erased_call( diff --git a/packages/core/shield/src/actions.rs b/packages/core/shield/src/actions.rs index 47d587d..e49ad2e 100644 --- a/packages/core/shield/src/actions.rs +++ b/packages/core/shield/src/actions.rs @@ -1,7 +1,9 @@ mod sign_in; mod sign_in_callback; mod sign_out; +mod sign_up; pub use sign_in::*; pub use sign_in_callback::*; pub use sign_out::*; +pub use sign_up::*; diff --git a/packages/core/shield/src/actions/sign_out.rs b/packages/core/shield/src/actions/sign_out.rs index 09445e0..b956511 100644 --- a/packages/core/shield/src/actions/sign_out.rs +++ b/packages/core/shield/src/actions/sign_out.rs @@ -31,14 +31,14 @@ impl SignOutAction { })) } - pub fn form(_provider: P) -> Form { - Form { + pub fn forms(_provider: P) -> Vec { + vec![Form { inputs: vec![Input { name: "submit".to_owned(), label: None, r#type: InputType::Submit(InputTypeSubmit {}), value: Some(Self::name()), }], - } + }] } } diff --git a/packages/core/shield/src/actions/sign_up.rs b/packages/core/shield/src/actions/sign_up.rs new file mode 100644 index 0000000..f962548 --- /dev/null +++ b/packages/core/shield/src/actions/sign_up.rs @@ -0,0 +1,14 @@ +const ACTION_ID: &str = "sign-up"; +const ACTION_NAME: &str = "Sign up"; + +pub struct SignUpAction; + +impl SignUpAction { + pub fn id() -> String { + ACTION_ID.to_owned() + } + + pub fn name() -> String { + ACTION_NAME.to_owned() + } +} diff --git a/packages/core/shield/src/shield.rs b/packages/core/shield/src/shield.rs index a1ef50e..3ed2436 100644 --- a/packages/core/shield/src/shield.rs +++ b/packages/core/shield/src/shield.rs @@ -4,6 +4,7 @@ use futures::future::try_join_all; use tracing::warn; use crate::{ + ActionMethodForm, action::{ActionForms, ActionProviderForm}, error::{ActionError, MethodError, ProviderError, ShieldError}, method::ErasedMethod, @@ -27,6 +28,8 @@ impl Shield { where S: Storage + 'static, { + // TOOD: Check for duplicate method IDs. + Self { storage: Arc::new(storage), methods: Arc::new( @@ -83,9 +86,9 @@ impl Shield { session: Session, ) -> Result { let mut action_name = None::; - let mut forms = vec![]; + let mut method_forms = vec![]; - for (_, method) in self.methods.iter() { + for (method_id, method) in self.methods.iter() { let Some(action) = method.erased_action_by_id(action_id) else { continue; }; @@ -98,25 +101,31 @@ impl Shield { } action_name = Some(name); + let mut provider_forms = vec![]; for (provider_id, provider) in method.erased_providers().await? { if !action.erased_condition(&*provider, session.clone())? { continue; } - let form = action.erased_form(provider); - - forms.push(ActionProviderForm { - method_id: method.erased_id(), - provider_id, - form, - }); + let forms = action.erased_forms(provider); + for form in forms { + provider_forms.push(ActionProviderForm { + id: provider_id.clone(), + form, + }); + } } + + method_forms.push(ActionMethodForm { + id: method_id.clone(), + provider_forms, + }); } Ok(ActionForms { id: action_id.to_owned(), name: action_name.unwrap_or(action_id.to_owned()), - forms, + method_forms, }) } diff --git a/packages/integrations/shield-dioxus/Cargo.toml b/packages/integrations/shield-dioxus/Cargo.toml index 559f535..b1ff1b4 100644 --- a/packages/integrations/shield-dioxus/Cargo.toml +++ b/packages/integrations/shield-dioxus/Cargo.toml @@ -16,3 +16,4 @@ async-trait.workspace = true dioxus = { workspace = true, features = ["fullstack", "router"] } serde_json.workspace = true shield.workspace = true +tracing.workspace = true diff --git a/packages/integrations/shield-dioxus/src/routes/action.rs b/packages/integrations/shield-dioxus/src/routes/action.rs index 0912cef..59f4e84 100644 --- a/packages/integrations/shield-dioxus/src/routes/action.rs +++ b/packages/integrations/shield-dioxus/src/routes/action.rs @@ -1,4 +1,5 @@ use dioxus::prelude::*; +use serde_json::Value; use shield::{ActionForms, Response}; use crate::ErasedDioxusStyle; @@ -56,6 +57,8 @@ pub async fn call( action_id: String, method_id: String, provider_id: Option, + // TODO: Would be nice if this argument could fill up with all unknown keys instead of setting name to `data[...]`. + data: Value, ) -> Result { #[cfg(feature = "server")] { @@ -65,6 +68,8 @@ pub async fn call( use crate::integration::DioxusIntegrationDyn; + tracing::info!("call data {data:#?}"); + let FromContext(integration): FromContext = extract().await?; let shield = integration.extract_shield().await; let session = integration.extract_session().await; @@ -75,10 +80,9 @@ pub async fn call( &method_id, provider_id.as_deref(), session, - // TODO: Support request input. Request { query: Value::Null, - form_data: Value::Null, + form_data: data, }, ) .await?; diff --git a/packages/integrations/shield-leptos/Cargo.toml b/packages/integrations/shield-leptos/Cargo.toml index a6cc38b..613483e 100644 --- a/packages/integrations/shield-leptos/Cargo.toml +++ b/packages/integrations/shield-leptos/Cargo.toml @@ -15,3 +15,4 @@ leptos_router.workspace = true serde.workspace = true serde_json.workspace = true shield.workspace = true +tracing.workspace = true diff --git a/packages/integrations/shield-leptos/src/routes/action.rs b/packages/integrations/shield-leptos/src/routes/action.rs index c2f34d7..884dab9 100644 --- a/packages/integrations/shield-leptos/src/routes/action.rs +++ b/packages/integrations/shield-leptos/src/routes/action.rs @@ -1,6 +1,7 @@ use leptos::prelude::*; use leptos_router::{hooks::use_params, params::Params}; -use shield::{ActionForms, Response}; +use serde_json::Value; +use shield::ActionForms; use crate::ErasedLeptosStyle; @@ -52,9 +53,11 @@ pub async fn call( action_id: String, method_id: String, provider_id: Option, + // TODO: Would be nice if this argument could fill up with all unknown keys instead of setting name to `data[...]`. + data: Value, ) -> Result<(), ServerFnError> { use serde_json::Value; - use shield::Request; + use shield::{Request, Response}; use crate::expect_server_integration; @@ -62,16 +65,17 @@ pub async fn call( let shield = integration.extract_shield().await; let session = integration.extract_session().await; + tracing::info!("call data {data:#?}"); + let response = shield .call( &action_id, &method_id, provider_id.as_deref(), session, - // TODO: Support request input. Request { query: Value::Null, - form_data: Value::Null, + form_data: data, }, ) .await?; diff --git a/packages/methods/shield-credentials/src/actions/sign_in.rs b/packages/methods/shield-credentials/src/actions/sign_in.rs index 079a61e..14ef4f3 100644 --- a/packages/methods/shield-credentials/src/actions/sign_in.rs +++ b/packages/methods/shield-credentials/src/actions/sign_in.rs @@ -31,8 +31,8 @@ impl Action Form { - self.credentials.form() + fn forms(&self, _provider: CredentialsProvider) -> Vec { + vec![self.credentials.form()] } async fn call( diff --git a/packages/methods/shield-credentials/src/actions/sign_out.rs b/packages/methods/shield-credentials/src/actions/sign_out.rs index 89b2336..b7003ff 100644 --- a/packages/methods/shield-credentials/src/actions/sign_out.rs +++ b/packages/methods/shield-credentials/src/actions/sign_out.rs @@ -23,8 +23,8 @@ impl Action for CredentialsSignOutAction { SignOutAction::condition(provider, session) } - fn form(&self, provider: CredentialsProvider) -> Form { - SignOutAction::form(provider) + fn forms(&self, provider: CredentialsProvider) -> Vec { + SignOutAction::forms(provider) } async fn call( diff --git a/packages/methods/shield-oauth/src/actions/sign_in.rs b/packages/methods/shield-oauth/src/actions/sign_in.rs index 0f88b40..816e361 100644 --- a/packages/methods/shield-oauth/src/actions/sign_in.rs +++ b/packages/methods/shield-oauth/src/actions/sign_in.rs @@ -23,8 +23,8 @@ impl Action for OauthSignInAction { SignInAction::name() } - fn form(&self, _provider: OauthProvider) -> Form { - Form { inputs: vec![] } + fn forms(&self, _provider: OauthProvider) -> Vec { + vec![Form { inputs: vec![] }] } async fn call( diff --git a/packages/methods/shield-oauth/src/actions/sign_in_callback.rs b/packages/methods/shield-oauth/src/actions/sign_in_callback.rs index 0cc7fe2..5fec982 100644 --- a/packages/methods/shield-oauth/src/actions/sign_in_callback.rs +++ b/packages/methods/shield-oauth/src/actions/sign_in_callback.rs @@ -143,8 +143,8 @@ impl Action for OauthSignInCallbackAction { SignInCallbackAction::condition(provider, session) } - fn form(&self, _provider: OauthProvider) -> Form { - Form { inputs: vec![] } + fn forms(&self, _provider: OauthProvider) -> Vec { + vec![Form { inputs: vec![] }] } async fn call( diff --git a/packages/methods/shield-oauth/src/actions/sign_out.rs b/packages/methods/shield-oauth/src/actions/sign_out.rs index 94a9c8d..117916c 100644 --- a/packages/methods/shield-oauth/src/actions/sign_out.rs +++ b/packages/methods/shield-oauth/src/actions/sign_out.rs @@ -19,8 +19,8 @@ impl Action for OauthSignOutAction { SignOutAction::condition(provider, session) } - fn form(&self, provider: OauthProvider) -> Form { - SignOutAction::form(provider) + fn forms(&self, provider: OauthProvider) -> Vec { + SignOutAction::forms(provider) } async fn call( diff --git a/packages/methods/shield-oidc/src/actions/sign_in.rs b/packages/methods/shield-oidc/src/actions/sign_in.rs index dc1d57f..f2bfa6c 100644 --- a/packages/methods/shield-oidc/src/actions/sign_in.rs +++ b/packages/methods/shield-oidc/src/actions/sign_in.rs @@ -26,15 +26,15 @@ impl Action for OidcSignInAction { SignInAction::name() } - fn form(&self, provider: OidcProvider) -> Form { - Form { + fn forms(&self, provider: OidcProvider) -> Vec { + vec![Form { inputs: vec![Input { name: "submit".to_owned(), label: None, r#type: InputType::Submit(InputTypeSubmit::default()), value: Some(format!("Sign in with {}", provider.name())), }], - } + }] } async fn call( diff --git a/packages/methods/shield-oidc/src/actions/sign_in_callback.rs b/packages/methods/shield-oidc/src/actions/sign_in_callback.rs index 804c5fb..a0dd488 100644 --- a/packages/methods/shield-oidc/src/actions/sign_in_callback.rs +++ b/packages/methods/shield-oidc/src/actions/sign_in_callback.rs @@ -154,8 +154,8 @@ impl Action for OidcSignInCallbackAction { SignInCallbackAction::condition(provider, session) } - fn form(&self, _provider: OidcProvider) -> Form { - Form { inputs: vec![] } + fn forms(&self, _provider: OidcProvider) -> Vec { + vec![Form { inputs: vec![] }] } async fn call( diff --git a/packages/methods/shield-oidc/src/actions/sign_out.rs b/packages/methods/shield-oidc/src/actions/sign_out.rs index e1639a6..ff57851 100644 --- a/packages/methods/shield-oidc/src/actions/sign_out.rs +++ b/packages/methods/shield-oidc/src/actions/sign_out.rs @@ -19,8 +19,8 @@ impl Action for OidcSignOutAction { SignOutAction::condition(provider, session) } - fn form(&self, provider: OidcProvider) -> Form { - SignOutAction::form(provider) + fn forms(&self, provider: OidcProvider) -> Vec { + SignOutAction::forms(provider) } async fn call( diff --git a/packages/methods/shield-workos/Cargo.toml b/packages/methods/shield-workos/Cargo.toml new file mode 100644 index 0000000..03b33c4 --- /dev/null +++ b/packages/methods/shield-workos/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "shield-workos" +description = "WorkOS method for Shield." + +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[dependencies] +async-trait.workspace = true +bon.workspace = true +serde.workspace = true +serde_json.workspace = true +shield.workspace = true +tracing.workspace = true +workos-sdk = "0.3.0" diff --git a/packages/methods/shield-workos/README.md b/packages/methods/shield-workos/README.md new file mode 100644 index 0000000..340f623 --- /dev/null +++ b/packages/methods/shield-workos/README.md @@ -0,0 +1,13 @@ +

Shield WorkOS

+ +WorkOS method for Shield. + +## Documentation + +See [the Shield book](https://shield.rustforweb.org/) for documentation. + +## Rust for Web + +The Shield project is part of [Rust for Web](https://github.com/RustForWeb). + +[Rust for Web](https://github.com/RustForWeb) creates and ports web libraries for Rust. All projects are free and open source. diff --git a/packages/methods/shield-workos/src/actions.rs b/packages/methods/shield-workos/src/actions.rs new file mode 100644 index 0000000..37fd419 --- /dev/null +++ b/packages/methods/shield-workos/src/actions.rs @@ -0,0 +1,29 @@ +mod index; +mod sign_in; +mod sign_out; +mod sign_up; + +pub use index::*; +pub use sign_in::*; +pub use sign_out::*; +pub use sign_up::*; + +// TODO: +// - Index action +// - Email form +// - SSO button forms +// - Sign in action +// - Password form +// - Magic auth button +// - SSO button forms (if enabled in options only show the ones the user has connected) +// - Sign up action +// - Password form +// - Magic auth button +// - SSO button forms +// - SSO callback action +// - Email verification action +// - Magic auth action +// - Forgot password action +// - Reset password action +// - MFA challenge action +// - MFA enrollment action diff --git a/packages/methods/shield-workos/src/actions/index.rs b/packages/methods/shield-workos/src/actions/index.rs new file mode 100644 index 0000000..74b6f7a --- /dev/null +++ b/packages/methods/shield-workos/src/actions/index.rs @@ -0,0 +1,96 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use serde::Deserialize; +use shield::{ + Action, Form, Input, InputType, InputTypeEmail, Request, Response, Session, ShieldError, + erased_action, +}; +use tracing::info; +use workos_sdk::{ + PaginationParams, WorkOs, + user_management::{ListUsers, ListUsersParams}, +}; + +use crate::provider::WorkosProvider; + +// TODO: Make a special case for an index action reachable at the `/auth` root URL. + +const ACTION_ID: &str = "index"; +const ACTION_NAME: &str = "Index"; + +#[derive(Debug, Deserialize)] +pub struct EmailData { + pub email: String, +} + +pub struct WorkosIndexAction { + client: Arc, +} + +impl WorkosIndexAction { + pub fn new(client: Arc) -> Self { + Self { client } + } +} + +#[async_trait] +impl Action for WorkosIndexAction { + fn id(&self) -> String { + ACTION_ID.to_owned() + } + + fn name(&self) -> String { + ACTION_NAME.to_owned() + } + + fn forms(&self, _provider: WorkosProvider) -> Vec { + // TODO: SSO buttons. + + vec![Form { + inputs: vec![Input { + name: "email".to_owned(), + label: Some("Email address".to_owned()), + r#type: InputType::Email(InputTypeEmail { + autocomplete: Some("email".to_owned()), + placeholder: Some("Email address".to_owned()), + required: Some(true), + ..Default::default() + }), + value: None, + }], + }] + } + + async fn call( + &self, + _provider: WorkosProvider, + _session: Session, + request: Request, + ) -> Result { + // TODO: Check email address and redirect to sign-in/sign-up action with prefilled email address. + // TODO: Only check if enabled in options. + + let data = serde_json::from_value::(request.form_data) + .map_err(|err| ShieldError::Validation(err.to_string()))?; + + let result = self + .client + .user_management() + .list_users(&ListUsersParams { + pagination: PaginationParams { + limit: Some(1), + ..Default::default() + }, + email: Some(&data.email), + ..Default::default() + }) + .await; + + info!("{result:?}"); + + Ok(Response::Default) + } +} + +erased_action!(WorkosIndexAction); diff --git a/packages/methods/shield-workos/src/actions/sign_in.rs b/packages/methods/shield-workos/src/actions/sign_in.rs new file mode 100644 index 0000000..e5692a2 --- /dev/null +++ b/packages/methods/shield-workos/src/actions/sign_in.rs @@ -0,0 +1,104 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use shield::{ + Action, Form, Input, InputType, InputTypeEmail, InputTypeHidden, InputTypePassword, + InputTypeSubmit, Request, Response, Session, ShieldError, SignInAction, erased_action, +}; +use workos_sdk::WorkOs; + +use crate::provider::WorkosProvider; + +pub struct WorkosSignInAction { + // TODO: Remove expect. + #[expect(unused)] + client: Arc, +} + +impl WorkosSignInAction { + pub fn new(client: Arc) -> Self { + Self { client } + } +} + +#[async_trait] +impl Action for WorkosSignInAction { + fn id(&self) -> String { + SignInAction::id() + } + + fn name(&self) -> String { + SignInAction::name() + } + + fn forms(&self, _provider: WorkosProvider) -> Vec { + // TODO: Magic auth and SSO buttons. + // TODO: Prefill email address. + + vec![ + Form { + inputs: vec![ + Input { + name: "email".to_owned(), + label: Some("Email address".to_owned()), + r#type: InputType::Email(InputTypeEmail { + autocomplete: Some("email".to_owned()), + placeholder: Some("Email address".to_owned()), + required: Some(true), + ..Default::default() + }), + value: None, + }, + Input { + name: "password".to_owned(), + label: Some("Password".to_owned()), + r#type: InputType::Password(InputTypePassword { + autocomplete: Some("current-password".to_owned()), + placeholder: Some("Password".to_owned()), + required: Some(true), + ..Default::default() + }), + value: None, + }, + Input { + name: "submit".to_owned(), + label: None, + r#type: InputType::Submit(InputTypeSubmit::default()), + value: Some("Sign in".to_owned()), + }, + ], + }, + Form { + inputs: vec![ + Input { + name: "email".to_owned(), + label: None, + r#type: InputType::Hidden(InputTypeHidden { + autocomplete: Some("email".to_owned()), + required: Some(true), + }), + value: None, + }, + Input { + name: "submit".to_owned(), + label: None, + r#type: InputType::Submit(InputTypeSubmit::default()), + value: Some("Email sign-in code".to_owned()), + }, + ], + }, + ] + } + + async fn call( + &self, + _provider: WorkosProvider, + _session: Session, + _request: Request, + ) -> Result { + // TODO: sign in + Ok(Response::Default) + } +} + +erased_action!(WorkosSignInAction); diff --git a/packages/methods/shield-workos/src/actions/sign_out.rs b/packages/methods/shield-workos/src/actions/sign_out.rs new file mode 100644 index 0000000..42abe06 --- /dev/null +++ b/packages/methods/shield-workos/src/actions/sign_out.rs @@ -0,0 +1,50 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use shield::{Action, Form, Request, Response, Session, ShieldError, SignOutAction, erased_action}; +use workos_sdk::WorkOs; + +use crate::provider::WorkosProvider; + +pub struct WorkosSignOutAction { + // TODO: Remove expect. + #[expect(unused)] + client: Arc, +} + +impl WorkosSignOutAction { + pub fn new(client: Arc) -> Self { + Self { client } + } +} + +#[async_trait] +impl Action for WorkosSignOutAction { + fn id(&self) -> String { + SignOutAction::id() + } + + fn name(&self) -> String { + SignOutAction::name() + } + + fn condition(&self, provider: &WorkosProvider, session: Session) -> Result { + SignOutAction::condition(provider, session) + } + + fn forms(&self, provider: WorkosProvider) -> Vec { + SignOutAction::forms(provider) + } + + async fn call( + &self, + _provider: WorkosProvider, + _session: Session, + _request: Request, + ) -> Result { + // TODO: sign out + Ok(Response::Default) + } +} + +erased_action!(WorkosSignOutAction); diff --git a/packages/methods/shield-workos/src/actions/sign_up.rs b/packages/methods/shield-workos/src/actions/sign_up.rs new file mode 100644 index 0000000..fe709b7 --- /dev/null +++ b/packages/methods/shield-workos/src/actions/sign_up.rs @@ -0,0 +1,104 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use shield::{ + Action, Form, Input, InputType, InputTypeEmail, InputTypeHidden, InputTypePassword, + InputTypeSubmit, Request, Response, Session, ShieldError, SignUpAction, erased_action, +}; +use workos_sdk::WorkOs; + +use crate::provider::WorkosProvider; + +pub struct WorkosSignUpAction { + // TODO: Remove expect. + #[expect(unused)] + client: Arc, +} + +impl WorkosSignUpAction { + pub fn new(client: Arc) -> Self { + Self { client } + } +} + +#[async_trait] +impl Action for WorkosSignUpAction { + fn id(&self) -> String { + SignUpAction::id() + } + + fn name(&self) -> String { + SignUpAction::name() + } + + fn forms(&self, _provider: WorkosProvider) -> Vec { + // TODO: Magic auth and SSO buttons. + // TODO: Prefill email address. + + vec![ + Form { + inputs: vec![ + Input { + name: "email".to_owned(), + label: Some("Email address".to_owned()), + r#type: InputType::Email(InputTypeEmail { + autocomplete: Some("email".to_owned()), + placeholder: Some("Email address".to_owned()), + required: Some(true), + ..Default::default() + }), + value: None, + }, + Input { + name: "password".to_owned(), + label: Some("Password".to_owned()), + r#type: InputType::Password(InputTypePassword { + autocomplete: Some("new-password".to_owned()), + placeholder: Some("Password".to_owned()), + required: Some(true), + ..Default::default() + }), + value: None, + }, + Input { + name: "submit".to_owned(), + label: None, + r#type: InputType::Submit(InputTypeSubmit::default()), + value: Some("Sign up".to_owned()), + }, + ], + }, + Form { + inputs: vec![ + Input { + name: "email".to_owned(), + label: None, + r#type: InputType::Hidden(InputTypeHidden { + autocomplete: Some("email".to_owned()), + required: Some(true), + }), + value: None, + }, + Input { + name: "submit".to_owned(), + label: None, + r#type: InputType::Submit(InputTypeSubmit::default()), + value: Some("Email sign-up code".to_owned()), + }, + ], + }, + ] + } + + async fn call( + &self, + _provider: WorkosProvider, + _session: Session, + _request: Request, + ) -> Result { + // TODO: sign in + Ok(Response::Default) + } +} + +erased_action!(WorkosSignUpAction); diff --git a/packages/methods/shield-workos/src/lib.rs b/packages/methods/shield-workos/src/lib.rs new file mode 100644 index 0000000..011cc55 --- /dev/null +++ b/packages/methods/shield-workos/src/lib.rs @@ -0,0 +1,9 @@ +mod actions; +mod method; +mod options; +mod provider; + +pub use method::*; +pub use options::*; + +// TODO: Support both AuthKit method and self hosted method. diff --git a/packages/methods/shield-workos/src/method.rs b/packages/methods/shield-workos/src/method.rs new file mode 100644 index 0000000..e8c66eb --- /dev/null +++ b/packages/methods/shield-workos/src/method.rs @@ -0,0 +1,54 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use shield::{Action, Method, ShieldError, erased_method}; +use workos_sdk::WorkOs; + +use crate::{ + actions::{WorkosIndexAction, WorkosSignInAction, WorkosSignOutAction, WorkosSignUpAction}, + options::WorkosOptions, + provider::WorkosProvider, +}; + +pub const WORKOS_METHOD_ID: &str = "workos"; + +pub struct WorkosMethod { + options: WorkosOptions, + client: Arc, +} + +impl WorkosMethod { + pub fn new(client: WorkOs) -> Self { + Self { + options: WorkosOptions::default(), + client: Arc::new(client), + } + } + + pub fn with_options(mut self, options: WorkosOptions) -> Self { + self.options = options; + self + } +} + +#[async_trait] +impl Method for WorkosMethod { + fn id(&self) -> String { + WORKOS_METHOD_ID.to_owned() + } + + fn actions(&self) -> Vec>> { + vec![ + Box::new(WorkosIndexAction::new(self.client.clone())), + Box::new(WorkosSignInAction::new(self.client.clone())), + Box::new(WorkosSignUpAction::new(self.client.clone())), + Box::new(WorkosSignOutAction::new(self.client.clone())), + ] + } + + async fn providers(&self) -> Result, ShieldError> { + Ok(vec![WorkosProvider]) + } +} + +erased_method!(WorkosMethod); diff --git a/packages/methods/shield-workos/src/options.rs b/packages/methods/shield-workos/src/options.rs new file mode 100644 index 0000000..43fd6a2 --- /dev/null +++ b/packages/methods/shield-workos/src/options.rs @@ -0,0 +1,11 @@ +use bon::Builder; + +#[derive(Builder, Clone, Debug)] +#[builder(on(String, into), state_mod(vis = "pub(crate)"))] +pub struct WorkosOptions {} + +impl Default for WorkosOptions { + fn default() -> Self { + Self::builder().build() + } +} diff --git a/packages/methods/shield-workos/src/provider.rs b/packages/methods/shield-workos/src/provider.rs new file mode 100644 index 0000000..9aa5add --- /dev/null +++ b/packages/methods/shield-workos/src/provider.rs @@ -0,0 +1,19 @@ +use shield::Provider; + +use crate::method::WORKOS_METHOD_ID; + +pub struct WorkosProvider; + +impl Provider for WorkosProvider { + fn method_id(&self) -> String { + WORKOS_METHOD_ID.to_owned() + } + + fn id(&self) -> Option { + None + } + + fn name(&self) -> String { + "WorkOS".to_owned() + } +} diff --git a/packages/styles/shield-bootstrap/Cargo.toml b/packages/styles/shield-bootstrap/Cargo.toml index 6ab4c09..bce4338 100644 --- a/packages/styles/shield-bootstrap/Cargo.toml +++ b/packages/styles/shield-bootstrap/Cargo.toml @@ -8,13 +8,23 @@ license.workspace = true repository.workspace = true version.workspace = true +[package.metadata.cargo-machete] +ignored = ["dioxus-html"] + [features] -dioxus = ["dep:dioxus", "dep:shield-dioxus"] +dioxus = [ + "dep:dioxus", + "dep:dioxus-html", + "dep:serde_json", + "dep:shield-dioxus", +] leptos = ["dep:leptos", "dep:shield-leptos"] [dependencies] dioxus = { workspace = true, optional = true } +dioxus-html = { workspace = true, features = ["serialize"], optional = true } leptos = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } shield.workspace = true shield-dioxus = { workspace = true, optional = true } shield-leptos = { workspace = true, optional = true } diff --git a/packages/styles/shield-bootstrap/src/dioxus.rs b/packages/styles/shield-bootstrap/src/dioxus.rs index 8cc2087..587e173 100644 --- a/packages/styles/shield-bootstrap/src/dioxus.rs +++ b/packages/styles/shield-bootstrap/src/dioxus.rs @@ -26,10 +26,14 @@ impl DioxusStyle for BootstrapDioxusStyle { "{action.name}" } - for form in &action.forms { - Form { - action_id: action.id.clone(), - form: form.clone(), + for method_form in &action.method_forms { + for provider_form in &method_form.provider_forms { + Form { + action_id: action.id.clone(), + method_id: method_form.id.clone(), + provider_id: provider_form.id.clone(), + form: provider_form.form.clone(), + } } } } diff --git a/packages/styles/shield-bootstrap/src/dioxus/form.rs b/packages/styles/shield-bootstrap/src/dioxus/form.rs index b0dae1b..c133599 100644 --- a/packages/styles/shield-bootstrap/src/dioxus/form.rs +++ b/packages/styles/shield-bootstrap/src/dioxus/form.rs @@ -1,5 +1,5 @@ use dioxus::{logger::tracing::info, prelude::*}; -use shield::{ActionProviderForm, Response}; +use shield::Response; use shield_dioxus::call; use crate::dioxus::input::FormInput; @@ -7,7 +7,9 @@ use crate::dioxus::input::FormInput; #[derive(Clone, PartialEq, Props)] pub struct FormProps { action_id: String, - form: ActionProviderForm, + method_id: String, + provider_id: Option, + form: shield::Form, } #[component] @@ -19,15 +21,17 @@ pub fn Form(props: FormProps) -> Element { onsubmit: { move |event| { let action_id = props.action_id.clone(); - let method_id = props.form.method_id.clone(); - let provider_id = props.form.provider_id.clone(); + let method_id = props.method_id.clone(); + let provider_id = props.provider_id.clone(); event.prevent_default(); async move { info!("{:?}", event); + // TODO: Replace `expect` with proper error handling. + let data = serde_json::to_value(event.data().values()).expect("Valid JSON."); - let result = call(action_id, method_id, provider_id).await; + let result = call(action_id, method_id, provider_id, data).await; info!("{:?}", result); // TODO: Handle error. @@ -43,7 +47,7 @@ pub fn Form(props: FormProps) -> Element { } }, - for input in props.form.form.inputs { + for input in props.form.inputs { FormInput { input: input } diff --git a/packages/styles/shield-bootstrap/src/leptos.rs b/packages/styles/shield-bootstrap/src/leptos.rs index ce598af..d935c6e 100644 --- a/packages/styles/shield-bootstrap/src/leptos.rs +++ b/packages/styles/shield-bootstrap/src/leptos.rs @@ -22,9 +22,14 @@ impl LeptosStyle for BootstrapLeptosStyle {

{action.name.clone()}

- {action.forms.iter().map(|form| view! { - - }).collect_view()} + {action.method_forms.iter().flat_map(|method_form| method_form.provider_forms.iter().map(|provider_form| view! { + + })).collect_view()}
} .into_any() diff --git a/packages/styles/shield-bootstrap/src/leptos/form.rs b/packages/styles/shield-bootstrap/src/leptos/form.rs index ca41597..19324ef 100644 --- a/packages/styles/shield-bootstrap/src/leptos/form.rs +++ b/packages/styles/shield-bootstrap/src/leptos/form.rs @@ -1,20 +1,24 @@ use leptos::prelude::*; -use shield::ActionProviderForm; use shield_leptos::Call; use crate::leptos::input::FormInput; #[component] -pub fn Form(action_id: String, form: ActionProviderForm) -> impl IntoView { +pub fn Form( + action_id: String, + method_id: String, + provider_id: Option, + form: shield::Form, +) -> impl IntoView { let call = ServerAction::::new(); view! { - - + + - {form.form.inputs.into_iter().map(|input| view! { + {form.inputs.into_iter().map(|input| view! { }).collect_view()} diff --git a/packages/styles/shield-bootstrap/src/leptos/input.rs b/packages/styles/shield-bootstrap/src/leptos/input.rs index 5306b6e..642f74d 100644 --- a/packages/styles/shield-bootstrap/src/leptos/input.rs +++ b/packages/styles/shield-bootstrap/src/leptos/input.rs @@ -25,7 +25,8 @@ fn Control(input: Input) -> impl IntoView { view! {