Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,016 changes: 687 additions & 329 deletions Cargo.lock

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ chrono = "0.4.39"
console_error_panic_hook = "0.1.2"
futures = "0.3.31"
http = "1.2.0"
leptos = "0.7.0"
leptos_actix = "0.7.0"
leptos_axum = "0.7.0"
leptos_meta = "0.7.0"
leptos_router = "0.7.0"
leptos = "0.7.4"
leptos_actix = "0.7.4"
leptos_axum = "0.7.4"
leptos_meta = "0.7.4"
leptos_router = "0.7.4"
thiserror = "2.0.7"
sea-orm = "1.1.2"
sea-orm-migration = "1.1.2"
Expand All @@ -38,6 +38,7 @@ tower-service = "0.3.3"
tower-sessions = "0.13.0"
tracing = "0.1.41"
tracing-subscriber = "0.3.19"
utoipa = { version = "5.3.1", features = ["chrono", "uuid"] }
uuid = "1.11.0"
wasm-bindgen = "0.2.99"
wasm-bindgen = "0.2.100"
wasm-tracing = "1.0.1"
10 changes: 9 additions & 1 deletion examples/leptos-axum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ leptos_meta.workspace = true
leptos_router.workspace = true
shield = { path = "../../packages/core/shield" }
shield-leptos = { path = "../../packages/integrations/shield-leptos" }
shield-leptos-axum = { path = "../../packages/integrations/shield-leptos-axum", optional = true }
shield-leptos-axum = { path = "../../packages/integrations/shield-leptos-axum", features = [
"utoipa",
], optional = true }
shield-memory = { path = "../../packages/storage/shield-memory", optional = true }
shield-oidc = { path = "../../packages/providers/shield-oidc", optional = true }
time = "0.3.37"
Expand All @@ -31,6 +33,12 @@ tracing.workspace = true
tracing-subscriber.workspace = true
wasm-bindgen.workspace = true
wasm-tracing.workspace = true
utoipa.workspace = true
utoipa-swagger-ui = { version = "=8.1.0", features = [
"axum",
"reqwest",
"vendored",
] }

[features]
default = ["ssr"]
Expand Down
20 changes: 15 additions & 5 deletions examples/leptos-axum/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ async fn main() {
use leptos_axum::{generate_route_list, LeptosRoutes};
use shield::Shield;
use shield_examples_leptos_axum::app::*;
use shield_leptos_axum::{auth_router, provide_axum_integration, ShieldLayer};
use shield_leptos_axum::{provide_axum_integration, AuthRoutes, ShieldLayer};
use shield_memory::{MemoryStorage, User};
use shield_oidc::{Keycloak, OidcProvider};
use time::Duration;
use tokio::net::TcpListener;
use tower_sessions::{Expiry, MemoryStore, SessionManagerLayer};
use tracing::level_filters::LevelFilter;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;

// Initialize tracing
tracing_subscriber::fmt()
Expand Down Expand Up @@ -54,9 +56,17 @@ async fn main() {
);
let shield_layer = ShieldLayer::new(shield.clone());

// Initialize app
let app = Router::new()
.nest("/api/auth", auth_router::<User, LeptosOptions>())
// Initialize OpenAPI specification (optional)
#[derive(OpenApi)]
#[openapi(nest(
(path = "/api/auth", api = AuthRoutes, tags = ["auth"]),
))]
struct Docs;

// Initialize router
let router = Router::new()
.nest("/api/auth", AuthRoutes::router::<User, LeptosOptions>())
.merge(SwaggerUi::new("/api-docs").url("/api/openapi.json", Docs::openapi()))
.leptos_routes_with_context(
&leptos_options,
routes,
Expand All @@ -76,7 +86,7 @@ async fn main() {
// Start app
log!("listening on http://{}", &addr);
let listener = TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
axum::serve(listener, router.into_make_service())
.await
.unwrap();
}
Expand Down
5 changes: 5 additions & 0 deletions packages/core/shield/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@ serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
thiserror.workspace = true
tracing.workspace = true
utoipa = { workspace = true, optional = true }

[features]
default = []
utoipa = ["dep:utoipa"]
2 changes: 2 additions & 0 deletions packages/core/shield/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ pub trait Subprovider: Send + Sync {
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct SubproviderVisualisation {
pub key: String,
pub provider_id: String,
Expand Down
3 changes: 3 additions & 0 deletions packages/core/shield/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SignInRequest {
pub provider_id: String,
pub subprovider_id: Option<String>,
Expand All @@ -10,6 +11,7 @@ pub struct SignInRequest {
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SignInCallbackRequest {
pub provider_id: String,
pub subprovider_id: Option<String>,
Expand All @@ -18,6 +20,7 @@ pub struct SignInCallbackRequest {
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SignOutRequest {
pub provider_id: String,
pub subprovider_id: Option<String>,
Expand Down
1 change: 1 addition & 0 deletions packages/core/shield/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub struct UpdateUser {
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EmailAddress {
pub id: String,
pub email: String,
Expand Down
5 changes: 5 additions & 0 deletions packages/integrations/shield-axum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ serde.workspace = true
serde_json.workspace = true
shield = { path = "../../core/shield", version = "0.0.4" }
shield-tower = { path = "../shield-tower", version = "0.0.4" }
utoipa = { workspace = true, features = ["axum_extras"], optional = true }

[features]
default = []
utoipa = ["dep:utoipa"]
6 changes: 5 additions & 1 deletion packages/integrations/shield-axum/src/path.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use serde::Deserialize;

#[derive(Deserialize)]
pub struct AuthPath {
#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams))]
#[serde(rename_all = "camelCase")]
pub struct AuthPathParams {
/// ID of authentication provider.
pub provider_id: String,
/// ID of authentication subprovider (optional).
pub subprovider_id: Option<String>,
}
35 changes: 22 additions & 13 deletions packages/integrations/shield-axum/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,27 @@ use axum::{
};
use shield::User;

use crate::routes::{sign_in, sign_in_callback, sign_out, subproviders, user};
use crate::routes::*;

pub fn auth_router<U: User + Clone + 'static, S: Clone + Send + Sync + 'static>() -> Router<S> {
Router::new()
.route("/subproviders", get(subproviders::<U>))
.route("/sign-in/:provider_id", post(sign_in::<U>))
.route("/sign-in/:provider_id/:subprovider_id", post(sign_in::<U>))
.route("/sign-in/callback/:provider_id", get(sign_in_callback::<U>))
.route(
"/sign-in/callback/:provider_id/:subprovider_id",
get(sign_in_callback::<U>),
)
.route("/sign-out", post(sign_out::<U>))
.route("/user", get(user::<U>))
#[cfg_attr(feature = "utoipa", derive(utoipa::OpenApi))]
#[cfg_attr(
feature = "utoipa",
openapi(paths(subproviders, sign_in, sign_in_callback, sign_out, user))
)]
pub struct AuthRoutes;

impl AuthRoutes {
pub fn router<U: User + Clone + 'static, S: Clone + Send + Sync + 'static>() -> Router<S> {
Router::new()
.route("/subproviders", get(subproviders::<U>))
.route("/sign-in/:providerId", post(sign_in::<U>))
.route("/sign-in/:providerId/:subproviderId", post(sign_in::<U>))
.route("/sign-in/callback/:providerId", get(sign_in_callback::<U>))
.route(
"/sign-in/callback/:providerId/:subproviderId",
get(sign_in_callback::<U>),
)
.route("/sign-out", post(sign_out::<U>))
.route("/user", get(user::<U>))
}
}
22 changes: 19 additions & 3 deletions packages/integrations/shield-axum/src/routes/sign_in.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,31 @@ use shield::{SignInRequest, User};
use crate::{
error::RouteError,
extract::{ExtractSession, ExtractShield},
path::AuthPath,
path::AuthPathParams,
response::RouteResponse,
};

#[cfg_attr(
feature = "utoipa",
utoipa::path(
post,
path = "/sign-in/{providerId}/{subproviderId}",
description = "Sign in to an account with the specified authentication provider.",
params(
AuthPathParams,
),
responses(
(status = 200, description = "Successfully signed in."),
(status = 303, description = "Redirect to authentication provider for sign in."),
(status = 500, description = "Internal server error."),
)
)
)]
pub async fn sign_in<U: User>(
Path(AuthPath {
Path(AuthPathParams {
provider_id,
subprovider_id,
}): Path<AuthPath>,
}): Path<AuthPathParams>,
ExtractShield(shield): ExtractShield<U>,
ExtractSession(session): ExtractSession,
) -> Result<RouteResponse, RouteError> {
Expand Down
21 changes: 18 additions & 3 deletions packages/integrations/shield-axum/src/routes/sign_in_callback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,30 @@ use shield::{SignInCallbackRequest, User};
use crate::{
error::RouteError,
extract::{ExtractSession, ExtractShield},
path::AuthPath,
path::AuthPathParams,
response::RouteResponse,
};

#[cfg_attr(
feature = "utoipa",
utoipa::path(
post,
path = "/sign-in/callback/{providerId}/{subproviderId}",
description = "Callback after signing in with authentication provider.",
params(
AuthPathParams,
),
responses(
(status = 200, description = "Successfully signed in."),
(status = 500, description = "Internal server error."),
)
)
)]
pub async fn sign_in_callback<U: User>(
Path(AuthPath {
Path(AuthPathParams {
provider_id,
subprovider_id,
}): Path<AuthPath>,
}): Path<AuthPathParams>,
Query(query): Query<Value>,
ExtractShield(shield): ExtractShield<U>,
ExtractSession(session): ExtractSession,
Expand Down
27 changes: 25 additions & 2 deletions packages/integrations/shield-axum/src/routes/sign_out.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
use shield::User;

use crate::ExtractShield;
use crate::{
error::RouteError,
extract::{ExtractSession, ExtractShield},
response::RouteResponse,
};

pub async fn sign_out<U: User>(ExtractShield(_shield): ExtractShield<U>) {}
#[cfg_attr(
feature = "utoipa",
utoipa::path(
post,
path = "/sign-out",
description = "Sign out of the current account.",
responses(
(status = 201, description = "Successfully signed out."),
(status = 500, description = "Internal server error.")
)
)
)]
pub async fn sign_out<U: User>(
ExtractShield(shield): ExtractShield<U>,
ExtractSession(session): ExtractSession,
) -> Result<RouteResponse, RouteError> {
let response = shield.sign_out(session).await?;

Ok(response.into())
}
12 changes: 12 additions & 0 deletions packages/integrations/shield-axum/src/routes/subproviders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ use shield::{SubproviderVisualisation, User};

use crate::{error::RouteError, extract::ExtractShield};

#[cfg_attr(
feature = "utoipa",
utoipa::path(
get,
path = "/subproviders",
description = "Get a list of authentication subproviders.",
responses(
(status = 200, description = "List of authentication subproviders.", body = Vec<SubproviderVisualisation>),
(status = 500, description = "Internal server error.")
)
)
)]
pub async fn subproviders<U: User>(
ExtractShield(shield): ExtractShield<U>,
) -> Result<Json<Vec<SubproviderVisualisation>>, RouteError> {
Expand Down
47 changes: 36 additions & 11 deletions packages/integrations/shield-axum/src/routes/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,49 @@ use axum::{
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use serde::{Deserialize, Serialize};
use shield::User;

use crate::extract::ExtractUser;

#[derive(Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", schema(as = User))]
#[serde(rename_all = "camelCase")]
struct UserBody {
id: String,
name: Option<String>,
}

impl UserBody {
async fn new<U: User>(user: U) -> Self {
// TODO: Include email addresses.
// let email_addresses = user.email_addresses().await;

Self {
id: user.id(),
name: user.name(),
}
}
}

#[cfg_attr(
feature = "utoipa",
utoipa::path(
get,
path = "/user",
description = "Get the current user account.",
responses(
(status = 200, description = "Current user account.", body = UserBody),
(status = 401, description = "No account signed in."),
(status = 500, description = "Internal server error."),
)
)
)]
pub async fn user<U: User>(ExtractUser(user): ExtractUser<U>) -> Response {
// TODO: Send JSON error using some util.
match user {
Some(user) => {
// TODO: Include email addresses.
// let email_addresses = user.email_addresses().await;

Json(json!({
"id": user.id(),
"name": user.name(),
}))
.into_response()
}
Some(user) => Json(UserBody::new(user).await).into_response(),
None => (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(),
}
}
4 changes: 4 additions & 0 deletions packages/integrations/shield-leptos-axum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ leptos_axum.workspace = true
shield = { path = "../../core/shield", version = "0.0.4" }
shield-axum = { path = "../../integrations/shield-axum", version = "0.0.4" }
shield-leptos = { path = "../../integrations/shield-leptos", version = "0.0.4" }

[features]
default = []
utoipa = ["shield/utoipa", "shield-axum/utoipa"]
Loading