diff --git a/Cargo.lock b/Cargo.lock index c766bfa..2a1d4fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1491,6 +1491,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +dependencies = [ + "serde", + "typeid", +] + [[package]] name = "errno" version = "0.3.10" @@ -4362,6 +4372,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.9", + "typetag", ] [[package]] @@ -4496,12 +4507,14 @@ name = "shield-memory" version = "0.0.1" dependencies = [ "async-trait", + "serde", "shield", "shield-credentials", "shield-email", "shield-oauth", "shield-oidc", "shield-webauthn", + "typetag", "uuid", ] @@ -4540,6 +4553,7 @@ dependencies = [ "shield-oauth", "shield-oidc", "shield-webauthn", + "typetag", ] [[package]] @@ -5455,12 +5469,42 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "typeid" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "typetag" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "044fc3365ddd307c297fe0fe7b2e70588cdab4d0f62dc52055ca0d11b174cf0e" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9d30226ac9cbd2d1ff775f74e8febdab985dab14fb14aa2582c29a92d5555dc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "unicase" version = "2.8.0" @@ -5742,7 +5786,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b76939a..5e3db69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,5 +35,6 @@ tokio = "1.42.0" tower-layer = "0.3.3" tower-service = "0.3.3" tower-sessions = "0.13.0" +typetag = "0.2.19" uuid = "1.11.0" wasm-bindgen = "0.2.97" diff --git a/examples/leptos-actix/src/app.rs b/examples/leptos-actix/src/app.rs index de5fecd..2919c7c 100644 --- a/examples/leptos-actix/src/app.rs +++ b/examples/leptos-actix/src/app.rs @@ -1,10 +1,12 @@ use leptos::prelude::*; use leptos_meta::{provide_meta_context, MetaTags, Title}; use leptos_router::{ - components::{Route, Router, Routes, A}, + components::{Route, Router, Routes}, path, }; -use shield_leptos::routes::SignIn; +use shield_leptos::routes::{SignIn, SignOut}; + +use crate::home::HomePage; pub fn shell(options: LeptosOptions) -> impl IntoView { view! { @@ -34,21 +36,12 @@ pub fn App() -> impl IntoView {
- + +
} } - -#[component] -fn HomePage() -> impl IntoView { - view! { -

"Shield Leptos Actix Example"

- - - - } -} diff --git a/examples/leptos-actix/src/home.rs b/examples/leptos-actix/src/home.rs new file mode 100644 index 0000000..76a7216 --- /dev/null +++ b/examples/leptos-actix/src/home.rs @@ -0,0 +1,40 @@ +use std::sync::Arc; + +use leptos::{either::Either, prelude::*}; +use leptos_router::components::A; + +#[server] +pub async fn user() -> Result>, ServerFnError> { + use shield_leptos::context::extract_user; + + Ok(extract_user().await) +} + +#[component] +pub fn HomePage() -> impl IntoView { + let user = OnceResource::new(user()); + + view! { +

"Shield Leptos Actix Example"

+ + + {move || Suspend::new(async move { match user.await { + Ok(user) => Either::Left(match user { + Some(_user) => Either::Left(view! { + + + + }), + None => Either::Right(view! { + + + + }), + }), + Err(err) => Either::Right(view! { + {err.to_string()} + }) + }})} + + } +} diff --git a/examples/leptos-actix/src/lib.rs b/examples/leptos-actix/src/lib.rs index 151489d..bcc3754 100644 --- a/examples/leptos-actix/src/lib.rs +++ b/examples/leptos-actix/src/lib.rs @@ -1,4 +1,5 @@ pub mod app; +mod home; #[cfg(feature = "hydrate")] #[wasm_bindgen::prelude::wasm_bindgen] diff --git a/examples/leptos-axum/src/app.rs b/examples/leptos-axum/src/app.rs index 23d707c..85d044e 100644 --- a/examples/leptos-axum/src/app.rs +++ b/examples/leptos-axum/src/app.rs @@ -1,10 +1,12 @@ use leptos::prelude::*; use leptos_meta::{provide_meta_context, MetaTags, Title}; use leptos_router::{ - components::{Route, Router, Routes, A}, + components::{Route, Router, Routes}, path, }; -use shield_leptos::routes::SignIn; +use shield_leptos::routes::{SignIn, SignOut}; + +use crate::home::HomePage; pub fn shell(options: LeptosOptions) -> impl IntoView { view! { @@ -34,21 +36,12 @@ pub fn App() -> impl IntoView {
- + +
} } - -#[component] -fn HomePage() -> impl IntoView { - view! { -

"Shield Leptos Axum Example"

- - - - } -} diff --git a/examples/leptos-axum/src/home.rs b/examples/leptos-axum/src/home.rs new file mode 100644 index 0000000..abef5e2 --- /dev/null +++ b/examples/leptos-axum/src/home.rs @@ -0,0 +1,42 @@ +use std::sync::Arc; + +use leptos::{either::Either, prelude::*}; +use leptos_router::components::A; + +#[server] +pub async fn user() -> Result>, ServerFnError> { + use shield_leptos::context::extract_user; + + Ok(extract_user().await) +} + +#[component] +pub fn HomePage() -> impl IntoView { + let user = OnceResource::new(user()); + + view! { +

"Shield Leptos Axum Example"

+ + + {move || Suspend::new(async move { match user.await { + Ok(user) => Either::Left(match user { + Some(user) => Either::Left(view! { + {user.id()} + + + + + }), + None => Either::Right(view! { + + + + }), + }), + Err(err) => Either::Right(view! { + {err.to_string()} + }) + }})} + + } +} diff --git a/examples/leptos-axum/src/lib.rs b/examples/leptos-axum/src/lib.rs index 151489d..bcc3754 100644 --- a/examples/leptos-axum/src/lib.rs +++ b/examples/leptos-axum/src/lib.rs @@ -1,4 +1,5 @@ pub mod app; +mod home; #[cfg(feature = "hydrate")] #[wasm_bindgen::prelude::wasm_bindgen] diff --git a/packages/core/shield/Cargo.toml b/packages/core/shield/Cargo.toml index c45e423..9926a0c 100644 --- a/packages/core/shield/Cargo.toml +++ b/packages/core/shield/Cargo.toml @@ -10,8 +10,9 @@ version.workspace = true [dependencies] async-trait.workspace = true -chrono.workspace = true +chrono = { workspace = true, features = ["serde"] } futures.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true +typetag.workspace = true diff --git a/packages/core/shield/src/user.rs b/packages/core/shield/src/user.rs index fa4385b..6de5ed0 100644 --- a/packages/core/shield/src/user.rs +++ b/packages/core/shield/src/user.rs @@ -1,5 +1,7 @@ use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +#[typetag::serde(tag = "type")] pub trait User: Send + Sync { fn id(&self) -> String; } @@ -15,7 +17,7 @@ pub struct UpdateUser { pub name: Option>, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct EmailAddress { pub id: String, pub email: String, @@ -49,13 +51,16 @@ pub struct UpdateEmailAddress { #[cfg(test)] pub(crate) mod tests { + use serde::{Deserialize, Serialize}; + use super::User; - #[derive(Clone, Debug)] + #[derive(Clone, Debug, Deserialize, Serialize)] pub struct TestUser { id: String, } + #[typetag::serde] impl User for TestUser { fn id(&self) -> String { self.id.clone() diff --git a/packages/integrations/shield-actix/src/extract.rs b/packages/integrations/shield-actix/src/extract.rs index 2d998be..b3fe7cb 100644 --- a/packages/integrations/shield-actix/src/extract.rs +++ b/packages/integrations/shield-actix/src/extract.rs @@ -41,3 +41,22 @@ impl FromRequest for ExtractSession { ) } } + +pub struct ExtractUser(pub Option); + +impl FromRequest for ExtractUser { + type Error = Error; + type Future = Ready>; + + fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { + ready( + req.extensions() + .get::>() + .cloned() + .map(ExtractUser) + .ok_or(ErrorInternalServerError( + "Can't extract Shield user. Is `ShieldTransform` enabled?", + )), + ) + } +} diff --git a/packages/integrations/shield-axum/src/extract.rs b/packages/integrations/shield-axum/src/extract.rs index b98a81a..f3bd391 100644 --- a/packages/integrations/shield-axum/src/extract.rs +++ b/packages/integrations/shield-axum/src/extract.rs @@ -42,3 +42,22 @@ impl FromRequestParts for ExtractSession { )) } } + +pub struct ExtractUser(pub Option); + +#[async_trait] +impl FromRequestParts for ExtractUser { + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + parts + .extensions + .get::>() + .cloned() + .map(ExtractUser) + .ok_or(( + StatusCode::INTERNAL_SERVER_ERROR, + "Can't extract Shield user. Is `ShieldLayer` enabled?", + )) + } +} diff --git a/packages/integrations/shield-leptos-actix/src/integration.rs b/packages/integrations/shield-leptos-actix/src/integration.rs index c099c13..fcff72c 100644 --- a/packages/integrations/shield-leptos-actix/src/integration.rs +++ b/packages/integrations/shield-leptos-actix/src/integration.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use leptos::prelude::provide_context; use leptos_actix::{extract, redirect}; use shield::{Session, ShieldDyn, User}; -use shield_actix::{ExtractSession, ExtractShield}; +use shield_actix::{ExtractSession, ExtractShield, ExtractUser}; use shield_leptos::LeptosIntegration; pub struct LeptosActixIntegration(PhantomData); @@ -25,9 +25,16 @@ impl LeptosIntegration for LeptosActixIntegration async fn extract_session(&self) -> Session { let ExtractSession(session) = extract().await.expect("TODO"); + session } + async fn extract_user(&self) -> Option> { + let ExtractUser(user) = extract::>().await.expect("TODO"); + + user.map(|user| Arc::new(user) as Arc) + } + fn redirect(&self, path: &str) { redirect(path); } diff --git a/packages/integrations/shield-leptos-axum/src/integration.rs b/packages/integrations/shield-leptos-axum/src/integration.rs index 59899c4..6db8f50 100644 --- a/packages/integrations/shield-leptos-axum/src/integration.rs +++ b/packages/integrations/shield-leptos-axum/src/integration.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use leptos::prelude::provide_context; use leptos_axum::{extract, redirect}; use shield::{Session, ShieldDyn, User}; -use shield_axum::{ExtractSession, ExtractShield}; +use shield_axum::{ExtractSession, ExtractShield, ExtractUser}; use shield_leptos::LeptosIntegration; pub struct LeptosAxumIntegration(PhantomData); @@ -25,9 +25,16 @@ impl LeptosIntegration for LeptosAxumIntegration { async fn extract_session(&self) -> Session { let ExtractSession(session) = extract().await.expect("TODO"); + session } + async fn extract_user(&self) -> Option> { + let ExtractUser(user) = extract::>().await.expect("TODO"); + + user.map(|user| Arc::new(user) as Arc) + } + fn redirect(&self, path: &str) { redirect(path); } diff --git a/packages/integrations/shield-leptos/src/context.rs b/packages/integrations/shield-leptos/src/context.rs index bcb64c5..1141580 100644 --- a/packages/integrations/shield-leptos/src/context.rs +++ b/packages/integrations/shield-leptos/src/context.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use leptos::prelude::expect_context; -use shield::ShieldDyn; +use shield::{ShieldDyn, User}; use crate::integration::LeptosIntegration; @@ -13,3 +13,8 @@ pub async fn expect_shield() -> ShieldDyn { let server_integration = expect_server_integration(); server_integration.extract_shield().await } + +pub async fn extract_user() -> Option> { + let server_integration = expect_server_integration(); + server_integration.extract_user().await +} diff --git a/packages/integrations/shield-leptos/src/integration.rs b/packages/integrations/shield-leptos/src/integration.rs index 2d856e9..ea15313 100644 --- a/packages/integrations/shield-leptos/src/integration.rs +++ b/packages/integrations/shield-leptos/src/integration.rs @@ -1,6 +1,7 @@ -use async_trait::async_trait; +use std::sync::Arc; -use shield::{Session, ShieldDyn}; +use async_trait::async_trait; +use shield::{Session, ShieldDyn, User}; #[async_trait] pub trait LeptosIntegration: Send + Sync { @@ -8,5 +9,7 @@ pub trait LeptosIntegration: Send + Sync { async fn extract_session(&self) -> Session; + async fn extract_user(&self) -> Option>; + fn redirect(&self, path: &str); } diff --git a/packages/integrations/shield-leptos/src/routes.rs b/packages/integrations/shield-leptos/src/routes.rs index 82b597f..757615c 100644 --- a/packages/integrations/shield-leptos/src/routes.rs +++ b/packages/integrations/shield-leptos/src/routes.rs @@ -1,3 +1,5 @@ mod sign_in; +mod sign_out; pub use sign_in::*; +pub use sign_out::*; diff --git a/packages/integrations/shield-leptos/src/routes/sign_out.rs b/packages/integrations/shield-leptos/src/routes/sign_out.rs new file mode 100644 index 0000000..a6eea0a --- /dev/null +++ b/packages/integrations/shield-leptos/src/routes/sign_out.rs @@ -0,0 +1,50 @@ +use leptos::prelude::*; + +#[server] +pub async fn sign_out( + provider_id: String, + subprovider_id: Option, +) -> Result<(), ServerFnError> { + use shield::{Response, ShieldError, SignOutRequest}; + + use crate::context::expect_server_integration; + + let server_integration = expect_server_integration(); + let shield = server_integration.extract_shield().await; + let session = server_integration.extract_session().await; + + let response = shield + .sign_out( + SignOutRequest { + provider_id, + subprovider_id, + }, + session, + ) + .await + .map_err(ServerFnError::::from)?; + + match response { + Response::Redirect(url) => { + server_integration.redirect(&url); + + Ok(()) + } + } +} + +#[component] +pub fn SignOut() -> impl IntoView { + let sign_out = ServerAction::::new(); + + view! { +

"Sign out"

+ + + // + // + + + + } +} diff --git a/packages/integrations/shield-tower/src/service.rs b/packages/integrations/shield-tower/src/service.rs index d470b9b..93c8ce4 100644 --- a/packages/integrations/shield-tower/src/service.rs +++ b/packages/integrations/shield-tower/src/service.rs @@ -74,8 +74,31 @@ where }; let shield_session = Session::new(session_storage); + let user_id = match shield_session.data().lock() { + Ok(session) => session.user_id.clone(), + Err(_err) => return Ok(Self::internal_server_error()), + }; + + let user = if let Some(user_id) = user_id { + match shield.storage().user_by_id(&user_id).await { + Ok(user) => { + if user.is_none() { + if let Err(_err) = shield_session.purge().await { + return Ok(Self::internal_server_error()); + } + } + + user + } + Err(_err) => return Ok(Self::internal_server_error()), + } + } else { + None + }; + req.extensions_mut().insert(shield); req.extensions_mut().insert(shield_session); + req.extensions_mut().insert(user); inner.call(req).await }) diff --git a/packages/storage/shield-memory/Cargo.toml b/packages/storage/shield-memory/Cargo.toml index 11f6b83..41f5a61 100644 --- a/packages/storage/shield-memory/Cargo.toml +++ b/packages/storage/shield-memory/Cargo.toml @@ -10,12 +10,14 @@ version.workspace = true [dependencies] async-trait.workspace = true +serde.workspace = true shield = { path = "../../core/shield" } shield-credentials = { path = "../../providers/shield-credentials", optional = true } shield-email = { path = "../../providers/shield-email", optional = true } shield-oauth = { path = "../../providers/shield-oauth", optional = true } shield-oidc = { path = "../../providers/shield-oidc", optional = true } shield-webauthn = { path = "../../providers/shield-webauthn", optional = true } +typetag.workspace = true uuid = { workspace = true, features = ["v4"] } [features] diff --git a/packages/storage/shield-memory/src/user.rs b/packages/storage/shield-memory/src/user.rs index e769700..485307d 100644 --- a/packages/storage/shield-memory/src/user.rs +++ b/packages/storage/shield-memory/src/user.rs @@ -1,12 +1,14 @@ +use serde::{Deserialize, Serialize}; use shield::EmailAddress; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct User { pub(crate) id: String, pub(crate) name: Option, pub(crate) email_addresses: Vec, } +#[typetag::serde] impl shield::User for User { fn id(&self) -> String { self.id.clone() diff --git a/packages/storage/shield-sea-orm/Cargo.toml b/packages/storage/shield-sea-orm/Cargo.toml index 78ce54e..0a569b8 100644 --- a/packages/storage/shield-sea-orm/Cargo.toml +++ b/packages/storage/shield-sea-orm/Cargo.toml @@ -20,6 +20,7 @@ shield-email = { path = "../../providers/shield-email", optional = true } shield-oauth = { path = "../../providers/shield-oauth", optional = true } shield-oidc = { path = "../../providers/shield-oidc", optional = true } shield-webauthn = { path = "../../providers/shield-webauthn", optional = true } +typetag.workspace = true [features] default = [] diff --git a/packages/storage/shield-sea-orm/src/storage.rs b/packages/storage/shield-sea-orm/src/storage.rs index c843a1a..719c691 100644 --- a/packages/storage/shield-sea-orm/src/storage.rs +++ b/packages/storage/shield-sea-orm/src/storage.rs @@ -11,6 +11,7 @@ use crate::entities::{email_address, prelude::User, user}; pub const SEA_ORM_STORAGE_ID: &str = "sea-orm"; +#[typetag::serde] impl shield::User for user::Model { fn id(&self) -> String { self.id.to_string()