diff --git a/Cargo.lock b/Cargo.lock index 1960946..d161233 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5612,7 +5612,6 @@ dependencies = [ "shield-dioxus-axum", "shield-memory", "shield-oidc", - "shield-workos", "tokio", "tower-sessions", "tracing", @@ -5678,6 +5677,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "shield-examples-workos" +version = "0.0.4" +dependencies = [ + "axum", + "dioxus", + "shield", + "shield-bootstrap", + "shield-dioxus", + "shield-dioxus-axum", + "shield-memory", + "shield-workos", + "tokio", + "tower-sessions", + "tracing", +] + [[package]] name = "shield-leptos" version = "0.0.4" @@ -5801,7 +5817,6 @@ dependencies = [ "serde", "serde_json", "shield", - "tracing", "workos-sdk", ] diff --git a/examples/axum/README.md b/examples/axum/README.md new file mode 100644 index 0000000..3cfca8c --- /dev/null +++ b/examples/axum/README.md @@ -0,0 +1,9 @@ +# Shield Axum Example + +1. Start the server: + +```shell +cargo run +``` + +2. Go to http://localhost:8080. diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 6010c59..656f7d8 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -23,7 +23,7 @@ async fn main() { .init(); // Configuration - let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 3000); + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); // Initialize sessions let session_store = MemoryStore::default(); diff --git a/examples/dioxus-axum/.env.example b/examples/dioxus-axum/.env.example deleted file mode 100644 index babe14d..0000000 --- a/examples/dioxus-axum/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -# WorkOS (optional) -# WORKOS_API_KEY = diff --git a/examples/dioxus-axum/Cargo.toml b/examples/dioxus-axum/Cargo.toml index 78f7a33..6ef10ff 100644 --- a/examples/dioxus-axum/Cargo.toml +++ b/examples/dioxus-axum/Cargo.toml @@ -18,7 +18,6 @@ server = [ "dep:shield-dioxus-axum", "dep:shield-memory", "dep:shield-oidc", - "dep:shield-workos", "dep:tokio", "dep:tower-sessions", "dioxus/server", @@ -36,7 +35,6 @@ 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 } -shield-workos = { workspace = true, optional = true } tokio = { workspace = true, features = ["rt-multi-thread"], optional = true } tower-sessions = { workspace = true, optional = true } tracing.workspace = true diff --git a/examples/dioxus-axum/README.md b/examples/dioxus-axum/README.md new file mode 100644 index 0000000..1c46ff6 --- /dev/null +++ b/examples/dioxus-axum/README.md @@ -0,0 +1,15 @@ +# Shield Dioxus Axum Example + +1. Install binaries: + +```shell +cargo install --locked dioxus-cli +``` + +2. Start the development server: + +```shell +dx serve --platform web +``` + +3. Go to http://localhost:8080. diff --git a/examples/dioxus-axum/src/main.rs b/examples/dioxus-axum/src/main.rs index 8a75e20..50658da 100644 --- a/examples/dioxus-axum/src/main.rs +++ b/examples/dioxus-axum/src/main.rs @@ -27,7 +27,6 @@ async fn main() { use shield_dioxus_axum::{AxumDioxusIntegration, ShieldLayer}; use shield_memory::{MemoryStorage, User}; use shield_oidc::{Keycloak, OidcMethod}; - use shield_workos::{WorkosMethod, WorkosOauthProvider, WorkosOptions}; use tokio::net::TcpListener; use tower_sessions::{Expiry, MemoryStore, SessionManagerLayer, cookie::time::Duration}; use tracing::{Level, info}; @@ -46,39 +45,21 @@ async fn main() { let storage = MemoryStorage::new(); let shield = Shield::new( storage.clone(), - [ - Some(Arc::new( - OidcMethod::new(storage).with_providers([Keycloak::builder( - "keycloak", - "http://localhost:18080/realms/Shield", - "client1", - ) - .client_secret("xcpQsaGbRILTljPtX4npjmYMBjKrariJ") - .redirect_url(format!( - "http://localhost:{}/api/auth/oidc/sign-in-callback/keycloak", - dioxus::cli_config::devserver_raw_addr() - .map(|addr| addr.port()) - .unwrap_or_else(|| addr.port()) - )) - .build()]), - ) as Arc), - env::var("WORKOS_API_KEY").ok().map(|api_key| { - Arc::new( - WorkosMethod::from_api_key(&api_key).with_options( - WorkosOptions::builder() - .oauth_providers(vec![ - WorkosOauthProvider::AppleOAuth, - WorkosOauthProvider::GoogleOAuth, - WorkosOauthProvider::MicrosoftOAuth, - ]) - .build(), - ), - ) as Arc - }), - ] - .into_iter() - .flatten() - .collect(), + vec![ + OidcMethod::new(storage).with_providers([Keycloak::builder( + "keycloak", + "http://localhost:18080/realms/Shield", + "client1", + ) + .client_secret("xcpQsaGbRILTljPtX4npjmYMBjKrariJ") + .redirect_url(format!( + "http://localhost:{}/api/auth/oidc/sign-in-callback/keycloak", + dioxus::cli_config::devserver_raw_addr() + .map(|addr| addr.port()) + .unwrap_or_else(|| addr.port()) + )) + .build()]), + ], ShieldOptions::default(), ); let shield_layer = ShieldLayer::new(shield.clone()); diff --git a/examples/leptos-actix/README.md b/examples/leptos-actix/README.md new file mode 100644 index 0000000..22f6a1a --- /dev/null +++ b/examples/leptos-actix/README.md @@ -0,0 +1,15 @@ +# Shield Leptos Actix Example + +1. Install binaries: + +```shell +cargo install --locked cargo-leptos +``` + +2. Start the development server: + +```shell +cargo letpos watch +``` + +3. Go to http://localhost:8080. diff --git a/examples/leptos-axum/README.md b/examples/leptos-axum/README.md new file mode 100644 index 0000000..11a1441 --- /dev/null +++ b/examples/leptos-axum/README.md @@ -0,0 +1,15 @@ +# Shield Leptos Axum Example + +1. Install binaries: + +```shell +cargo install --locked cargo-leptos +``` + +2. Start the development server: + +```shell +cargo letpos watch +``` + +3. Go to http://localhost:8080. diff --git a/examples/workos/.env.example b/examples/workos/.env.example new file mode 100644 index 0000000..3309d5e --- /dev/null +++ b/examples/workos/.env.example @@ -0,0 +1,2 @@ +WORKOS_API_KEY = change +WORKOS_CLIENT_ID = change diff --git a/examples/workos/Cargo.toml b/examples/workos/Cargo.toml new file mode 100644 index 0000000..706cd6b --- /dev/null +++ b/examples/workos/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "shield-examples-workos" +description = "Example with WorkOS." +publish = false + +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[features] +# TODO: Enabling these triggers multiple RustSec advisories. +# desktop = ["dioxus/desktop"] +# mobile = ["dioxus/mobile"] +server = [ + "dep:axum", + "dep:shield-dioxus-axum", + "dep:shield-memory", + "dep:shield-workos", + "dep:tokio", + "dep:tower-sessions", + "dioxus/server", + "shield-dioxus/server", +] +web = ["dioxus/web"] + +[dependencies] +axum = { workspace = true, optional = true } +dioxus = { workspace = true, features = ["fullstack", "router"] } +shield.workspace = true +shield-bootstrap = { workspace = true, features = ["dioxus"] } +shield-dioxus.workspace = true +shield-dioxus-axum = { workspace = true, optional = true } +shield-memory = { workspace = true, optional = true } +shield-workos = { workspace = true, optional = true } +tokio = { workspace = true, features = ["rt-multi-thread"], optional = true } +tower-sessions = { workspace = true, optional = true } +tracing.workspace = true diff --git a/examples/workos/Dioxus.toml b/examples/workos/Dioxus.toml new file mode 100644 index 0000000..c09bca0 --- /dev/null +++ b/examples/workos/Dioxus.toml @@ -0,0 +1,11 @@ +[application] + +[web.app] +title = "Shield WorkOS Example" + +[web.resource] +style = [] +script = [] + +[web.resource.dev] +script = [] diff --git a/examples/workos/README.md b/examples/workos/README.md new file mode 100644 index 0000000..d3f77c1 --- /dev/null +++ b/examples/workos/README.md @@ -0,0 +1,24 @@ +# Shield WorkOS Example + +1. Install binaries: + +```shell +cargo install --locked dioxus-cli +cargo install --locked dotenvy +``` + +2. Copy the default environment variables: + +```shell +cp .env.example .env +``` + +3. Configure the environment variables in `.env`. + +4. Start the development server: + +```shell +dotenvy dx serve --platform web +``` + +5. Go to http://localhost:8080. diff --git a/examples/workos/src/app.rs b/examples/workos/src/app.rs new file mode 100644 index 0000000..8c8ad0c --- /dev/null +++ b/examples/workos/src/app.rs @@ -0,0 +1,30 @@ +use dioxus::{document::Stylesheet, prelude::*}; +use shield_dioxus::ShieldRouter; + +use crate::home::Home; + +#[derive(Clone, Debug, PartialEq, Routable)] +#[rustfmt::skip] +enum Route { + #[route("/")] + Home {}, + #[child("/auth")] + Auth { + child: ShieldRouter + }, +} + +#[component] +pub fn App() -> Element { + rsx! { + Stylesheet { + href: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css", + integrity: "sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr", + crossorigin: "anonymous" + } + + main { + Router:: {} + } + } +} diff --git a/examples/workos/src/home.rs b/examples/workos/src/home.rs new file mode 100644 index 0000000..96d28fe --- /dev/null +++ b/examples/workos/src/home.rs @@ -0,0 +1,8 @@ +use dioxus::prelude::*; + +#[component] +pub fn Home() -> Element { + rsx! { + h1 { "Shield WorkOS Example" } + } +} diff --git a/examples/workos/src/main.rs b/examples/workos/src/main.rs new file mode 100644 index 0000000..4f248ac --- /dev/null +++ b/examples/workos/src/main.rs @@ -0,0 +1,86 @@ +mod app; +mod home; + +use crate::app::App; + +#[cfg(not(feature = "server"))] +fn main() { + use shield_bootstrap::BootstrapDioxusStyle; + + dioxus::LaunchBuilder::new() + .with_context(BootstrapDioxusStyle::default().context()) + .launch(App) +} + +#[cfg(feature = "server")] +#[tokio::main] +async fn main() { + use std::{env, sync::Arc}; + + use axum::Router; + use dioxus::{ + cli_config::fullstack_address_or_localhost, + prelude::{DioxusRouterExt, *}, + }; + use shield::{ErasedMethod, Method, Shield, ShieldOptions}; + use shield_bootstrap::BootstrapDioxusStyle; + use shield_dioxus_axum::{AxumDioxusIntegration, ShieldLayer}; + use shield_memory::{MemoryStorage, User}; + use shield_workos::{WorkosMethod, WorkosOauthProvider, WorkosOptions}; + use tokio::net::TcpListener; + use tower_sessions::{Expiry, MemoryStore, SessionManagerLayer, cookie::time::Duration}; + use tracing::{Level, info}; + + // Initialize Dioxus + dioxus::logger::init(Level::DEBUG).unwrap(); + let addr = fullstack_address_or_localhost(); + + // Initialize sessions + let session_store = MemoryStore::default(); + let session_layer = SessionManagerLayer::new(session_store) + .with_secure(false) + .with_expiry(Expiry::OnInactivity(Duration::minutes(10))); + + // Initialize Shield + let storage = MemoryStorage::new(); + let shield = Shield::new( + storage.clone(), + vec![Arc::new(WorkosMethod::from_api_key( + &env::var("WORKOS_API_KEY").expect("Missing `WORKOS_API_KEY`."), + &env::var("WORKOS_CLIENT_ID").expect("Missing `WORKOS_CLIENT_ID`."), + WorkosOptions::builder() + .oauth_providers(vec![ + WorkosOauthProvider::AppleOAuth, + WorkosOauthProvider::GoogleOAuth, + WorkosOauthProvider::MicrosoftOAuth, + ]) + .redirect_url(format!( + "http://localhost:{}/api/auth/workos/sign-in-callback", + addr.port() + )) + .build(), + ))], + ShieldOptions::default(), + ); + let shield_layer = ShieldLayer::new(shield.clone()); + + // Initialize router + let router = Router::new() + .serve_dioxus_application( + ServeConfig::builder() + .context(AxumDioxusIntegration::::default().context()) + .context(BootstrapDioxusStyle::default().context()) + .build() + .unwrap(), + App, + ) + .layer(shield_layer) + .layer(session_layer); + + // Start app + info!("listening on http://{}", &addr); + let listener = TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, router.into_make_service()) + .await + .unwrap(); +} diff --git a/packages/core/shield/src/response.rs b/packages/core/shield/src/response.rs index 5ff75f2..5420290 100644 --- a/packages/core/shield/src/response.rs +++ b/packages/core/shield/src/response.rs @@ -5,4 +5,5 @@ pub enum Response { // TODO: Remove temporary default variant. Default, Redirect(String), + RedirectToAction { action_id: String }, } diff --git a/packages/integrations/shield-dioxus/src/router.rs b/packages/integrations/shield-dioxus/src/router.rs index 3021a7d..e4db693 100644 --- a/packages/integrations/shield-dioxus/src/router.rs +++ b/packages/integrations/shield-dioxus/src/router.rs @@ -4,6 +4,8 @@ use crate::routes::Action; #[derive(Clone, Debug, PartialEq, Routable)] pub enum ShieldRouter { + #[route("", Action)] + ActionIndex, #[route("/:action_id")] Action { action_id: String }, } diff --git a/packages/integrations/shield-dioxus/src/routes/action.rs b/packages/integrations/shield-dioxus/src/routes/action.rs index 59f4e84..f74440b 100644 --- a/packages/integrations/shield-dioxus/src/routes/action.rs +++ b/packages/integrations/shield-dioxus/src/routes/action.rs @@ -6,6 +6,7 @@ use crate::ErasedDioxusStyle; #[derive(Clone, PartialEq, Props)] pub struct ActionProps { + #[props(default = "index".to_owned())] action_id: String, } diff --git a/packages/integrations/shield-leptos/src/router.rs b/packages/integrations/shield-leptos/src/router.rs index 43b1272..397dd3c 100644 --- a/packages/integrations/shield-leptos/src/router.rs +++ b/packages/integrations/shield-leptos/src/router.rs @@ -6,7 +6,8 @@ use crate::routes::Action; #[component(transparent)] pub fn ShieldRouter() -> impl MatchNestedRoutes + Clone { view! { - + + } .into_inner() } diff --git a/packages/integrations/shield-leptos/src/routes/action.rs b/packages/integrations/shield-leptos/src/routes/action.rs index 884dab9..b7e78e1 100644 --- a/packages/integrations/shield-leptos/src/routes/action.rs +++ b/packages/integrations/shield-leptos/src/routes/action.rs @@ -19,7 +19,7 @@ pub fn Action() -> impl IntoView { .as_ref() .ok() .and_then(|params| params.action_id.clone()) - .expect("TODO: Properly handle missing param.") + .unwrap_or("index".to_owned()) }; let resource = Resource::new(action_id, forms); @@ -85,6 +85,10 @@ pub async fn call( Response::Redirect(to) => { integration.redirect(&to); } + Response::RedirectToAction { action_id } => { + // TODO: Use actual router prefix instead of hardcoded `/auth`. + integration.redirect(&format!("/auth/{action_id}")); + } } Ok(()) diff --git a/packages/methods/shield-workos/Cargo.toml b/packages/methods/shield-workos/Cargo.toml index 03b33c4..2271e4a 100644 --- a/packages/methods/shield-workos/Cargo.toml +++ b/packages/methods/shield-workos/Cargo.toml @@ -14,5 +14,4 @@ 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/src/actions/index.rs b/packages/methods/shield-workos/src/actions/index.rs index 54d4966..7116ad0 100644 --- a/packages/methods/shield-workos/src/actions/index.rs +++ b/packages/methods/shield-workos/src/actions/index.rs @@ -4,46 +4,37 @@ use async_trait::async_trait; use serde::Deserialize; use shield::{ Action, Form, Input, InputType, InputTypeEmail, InputTypeHidden, InputTypeSubmit, Request, - Response, Session, ShieldError, erased_action, + Response, Session, ShieldError, SignInAction, SignUpAction, erased_action, }; -use tracing::info; use workos_sdk::{ - PaginationParams, WorkOs, + PaginationParams, sso::{ConnectionId, ListConnections, ListConnectionsParams}, - user_management::{ListUsers, ListUsersParams, OauthProvider}, + user_management::{ + ConnectionSelector, GetAuthorizationUrl, GetAuthorizationUrlParams, ListUsers, + ListUsersParams, OauthProvider, Provider, + }, }; -use crate::{WorkosOptions, provider::WorkosProvider}; - -// TODO: Make a special case for an index action reachable at the `/auth` root URL. +use crate::{client::WorkosClient, options::WorkosOptions, provider::WorkosProvider}; const ACTION_ID: &str = "index"; -const ACTION_NAME: &str = "Index"; +const ACTION_NAME: &str = "Welcome"; #[derive(Debug, Deserialize)] #[serde(untagged, rename_all = "camelCase", rename_all_fields = "camelCase")] pub enum IndexData { - Email { - // TODO: Dioxus records multiple values per field, but most of the time only a single value is expected. - email: Vec, - }, - Oauth { - // TODO: See above. - oauth_provider: Vec, - }, - Sso { - // TODO: See above. - connection_id: Vec, - }, + Email { email: String }, + Oauth { oauth_provider: OauthProvider }, + Sso { connection_id: ConnectionId }, } pub struct WorkosIndexAction { options: WorkosOptions, - client: Arc, + client: Arc, } impl WorkosIndexAction { - pub fn new(options: WorkosOptions, client: Arc) -> Self { + pub fn new(options: WorkosOptions, client: Arc) -> Self { Self { options, client } } } @@ -72,8 +63,6 @@ impl Action for WorkosIndexAction { .await .expect("TODO: handle error"); - info!("{connections:#?}"); - Ok([Form { inputs: vec![ Input { @@ -167,8 +156,6 @@ impl Action for WorkosIndexAction { match data { IndexData::Email { email } => { - info!("email: {email:#?}"); - let users = self .client .user_management() @@ -177,43 +164,62 @@ impl Action for WorkosIndexAction { limit: Some(1), ..Default::default() }, - // TODO: Remove [0] once email is a single value. - email: Some(&email[0]), + email: Some(&email), ..Default::default() }) .await .expect("TODO: handle error"); - info!("{users:#?}"); - + // TODO: Include email address as state. if users.data.is_empty() { - // TODO: Redirect to sign up action. + Ok(Response::RedirectToAction { + action_id: SignUpAction::id(), + }) } else { - // TODO: Redirect to sign in action. + Ok(Response::RedirectToAction { + action_id: SignInAction::id(), + }) } } IndexData::Oauth { oauth_provider } => { - info!("oauth {oauth_provider:#?}"); - - // TODO: Add client ID to method. - // self.client - // .user_management() - // .get_authorization_url(&GetAuthorizationUrlParams { - // client_id: todo!(), - // redirect_uri: todo!(), - // connection_selector: todo!(), - // state: todo!(), - // code_challenge: todo!(), - // login_hint: todo!(), - // domain_hint: todo!(), - // }) + let authorization_url = self + .client + .user_management() + .get_authorization_url(&GetAuthorizationUrlParams { + client_id: &self.client.client_id(), + redirect_uri: &self.options.redirect_url, + connection_selector: ConnectionSelector::Provider(&Provider::Oauth( + oauth_provider, + )), + // TODO: State and code challenge. + state: None, + code_challenge: None, + login_hint: None, + domain_hint: None, + }) + .expect("TODO: handle error"); + + Ok(Response::Redirect(authorization_url.to_string())) } IndexData::Sso { connection_id } => { - info!("sso {connection_id:#?}"); + let authorization_url = self + .client + .user_management() + .get_authorization_url(&GetAuthorizationUrlParams { + client_id: &self.client.client_id(), + redirect_uri: &self.options.redirect_url, + connection_selector: ConnectionSelector::Connection(&connection_id), + // TODO: State and code challenge. + state: None, + code_challenge: None, + login_hint: None, + domain_hint: None, + }) + .expect("TODO: handle error"); + + Ok(Response::Redirect(authorization_url.to_string())) } } - - Ok(Response::Default) } } diff --git a/packages/methods/shield-workos/src/actions/sign_in.rs b/packages/methods/shield-workos/src/actions/sign_in.rs index df6655b..33c04dd 100644 --- a/packages/methods/shield-workos/src/actions/sign_in.rs +++ b/packages/methods/shield-workos/src/actions/sign_in.rs @@ -5,18 +5,17 @@ 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; +use crate::{client::WorkosClient, provider::WorkosProvider}; pub struct WorkosSignInAction { // TODO: Remove expect. #[expect(unused)] - client: Arc, + client: Arc, } impl WorkosSignInAction { - pub fn new(client: Arc) -> Self { + pub fn new(client: Arc) -> Self { Self { client } } } diff --git a/packages/methods/shield-workos/src/actions/sign_out.rs b/packages/methods/shield-workos/src/actions/sign_out.rs index 89ee70d..045277b 100644 --- a/packages/methods/shield-workos/src/actions/sign_out.rs +++ b/packages/methods/shield-workos/src/actions/sign_out.rs @@ -2,18 +2,17 @@ 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; +use crate::{client::WorkosClient, provider::WorkosProvider}; pub struct WorkosSignOutAction { // TODO: Remove expect. #[expect(unused)] - client: Arc, + client: Arc, } impl WorkosSignOutAction { - pub fn new(client: Arc) -> Self { + pub fn new(client: Arc) -> Self { Self { client } } } diff --git a/packages/methods/shield-workos/src/actions/sign_up.rs b/packages/methods/shield-workos/src/actions/sign_up.rs index afbb617..183bc5a 100644 --- a/packages/methods/shield-workos/src/actions/sign_up.rs +++ b/packages/methods/shield-workos/src/actions/sign_up.rs @@ -5,18 +5,17 @@ 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; +use crate::{client::WorkosClient, provider::WorkosProvider}; pub struct WorkosSignUpAction { // TODO: Remove expect. #[expect(unused)] - client: Arc, + client: Arc, } impl WorkosSignUpAction { - pub fn new(client: Arc) -> Self { + pub fn new(client: Arc) -> Self { Self { client } } } diff --git a/packages/methods/shield-workos/src/client.rs b/packages/methods/shield-workos/src/client.rs new file mode 100644 index 0000000..4fbfaaa --- /dev/null +++ b/packages/methods/shield-workos/src/client.rs @@ -0,0 +1,29 @@ +use std::ops::Deref; + +use workos_sdk::{WorkOs, sso::ClientId}; + +pub struct WorkosClient { + client: WorkOs, + client_id: String, +} + +impl WorkosClient { + pub fn new(client: WorkOs, client_id: &str) -> Self { + Self { + client, + client_id: client_id.to_owned(), + } + } + + pub fn client_id(&self) -> ClientId { + ClientId::from(self.client_id.as_str()) + } +} + +impl Deref for WorkosClient { + type Target = WorkOs; + + fn deref(&self) -> &Self::Target { + &self.client + } +} diff --git a/packages/methods/shield-workos/src/lib.rs b/packages/methods/shield-workos/src/lib.rs index aa7a206..b7c84ff 100644 --- a/packages/methods/shield-workos/src/lib.rs +++ b/packages/methods/shield-workos/src/lib.rs @@ -1,4 +1,5 @@ mod actions; +mod client; mod method; mod options; mod provider; diff --git a/packages/methods/shield-workos/src/method.rs b/packages/methods/shield-workos/src/method.rs index bc7b4d3..1f23e41 100644 --- a/packages/methods/shield-workos/src/method.rs +++ b/packages/methods/shield-workos/src/method.rs @@ -6,6 +6,7 @@ use workos_sdk::{ApiKey, WorkOs}; use crate::{ actions::{WorkosIndexAction, WorkosSignInAction, WorkosSignOutAction, WorkosSignUpAction}, + client::WorkosClient, options::WorkosOptions, provider::WorkosProvider, }; @@ -14,19 +15,19 @@ pub const WORKOS_METHOD_ID: &str = "workos"; pub struct WorkosMethod { options: WorkosOptions, - client: Arc, + client: Arc, } impl WorkosMethod { - pub fn new(client: WorkOs) -> Self { + pub fn new(client: WorkOs, client_id: &str, options: WorkosOptions) -> Self { Self { - options: WorkosOptions::default(), - client: Arc::new(client), + options, + client: Arc::new(WorkosClient::new(client, client_id)), } } - pub fn from_api_key(api_key: &str) -> Self { - Self::new(WorkOs::new(&ApiKey::from(api_key))) + pub fn from_api_key(api_key: &str, client_id: &str, options: WorkosOptions) -> Self { + Self::new(WorkOs::new(&ApiKey::from(api_key)), client_id, options) } pub fn with_options(mut self, options: WorkosOptions) -> Self { diff --git a/packages/methods/shield-workos/src/options.rs b/packages/methods/shield-workos/src/options.rs index cb4c647..71ae530 100644 --- a/packages/methods/shield-workos/src/options.rs +++ b/packages/methods/shield-workos/src/options.rs @@ -6,10 +6,6 @@ use workos_sdk::user_management::OauthProvider; pub struct WorkosOptions { #[builder(default)] pub(crate) oauth_providers: Vec, -} - -impl Default for WorkosOptions { - fn default() -> Self { - Self::builder().build() - } + // TODO: Generate automatically? + pub(crate) redirect_url: String, } diff --git a/packages/styles/shield-bootstrap/src/dioxus/form.rs b/packages/styles/shield-bootstrap/src/dioxus/form.rs index c133599..7424618 100644 --- a/packages/styles/shield-bootstrap/src/dioxus/form.rs +++ b/packages/styles/shield-bootstrap/src/dioxus/form.rs @@ -1,6 +1,8 @@ +use std::collections::HashMap; + use dioxus::{logger::tracing::info, prelude::*}; use shield::Response; -use shield_dioxus::call; +use shield_dioxus::{ShieldRouter, call}; use crate::dioxus::input::FormInput; @@ -28,8 +30,15 @@ pub fn Form(props: FormProps) -> Element { async move { info!("{:?}", event); - // TODO: Replace `expect` with proper error handling. - let data = serde_json::to_value(event.data().values()).expect("Valid JSON."); + let data = serde_json::to_value( + // TODO: Support inputs with `multiple` attribute. + event + .data() + .values() + .into_iter() + .filter_map(|(key, values)| values.first().map(|value| (key, value.clone()))) + .collect::>() + ).expect("TODO: handle error"); let result = call(action_id, method_id, provider_id, data).await; info!("{:?}", result); @@ -41,6 +50,9 @@ pub fn Form(props: FormProps) -> Element { Response::Redirect(to) => { navigator.push(to); }, + Response::RedirectToAction { action_id } => { + navigator.push(ShieldRouter::Action { action_id }); + } } } } diff --git a/packages/styles/shield-bootstrap/src/dioxus/input.rs b/packages/styles/shield-bootstrap/src/dioxus/input.rs index 916eb98..2579f4e 100644 --- a/packages/styles/shield-bootstrap/src/dioxus/input.rs +++ b/packages/styles/shield-bootstrap/src/dioxus/input.rs @@ -27,6 +27,7 @@ pub fn FormInput(props: FormInputProps) -> Element { name: props.input.name, type: props.input.r#type.as_str(), value: props.input.value.clone(), + placeholder: props.input.label, } } } diff --git a/packages/styles/shield-bootstrap/src/leptos/input.rs b/packages/styles/shield-bootstrap/src/leptos/input.rs index 642f74d..380b320 100644 --- a/packages/styles/shield-bootstrap/src/leptos/input.rs +++ b/packages/styles/shield-bootstrap/src/leptos/input.rs @@ -29,6 +29,7 @@ fn Control(input: Input) -> impl IntoView { name=format!("data[{}]", input.name) r#type=input.r#type.as_str() value=input.value.clone() + placeholder=input.label /> } }