mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 09:54:41 -05:00
projects: port session_auth_axum (#2970)
This commit is contained in:
@@ -30,23 +30,21 @@ sqlx = { version = "0.8.0", features = [
|
||||
], optional = true }
|
||||
thiserror = "1.0"
|
||||
wasm-bindgen = "0.2.0"
|
||||
axum_session_auth = { version = "0.14.0", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
axum_session = { version = "0.14.0", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
axum_session_auth = { version = "0.14.0", features = [], optional = true }
|
||||
axum_session = { version = "0.14.0", features = [], optional = true }
|
||||
axum_session_sqlx = { version = "0.3.0", features = [ "sqlite", "tls-rustls"], optional = true }
|
||||
bcrypt = { version = "0.15.0", optional = true }
|
||||
async-trait = { version = "0.1.0", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:axum_session_sqlx",
|
||||
"dep:axum_session_auth",
|
||||
"dep:axum_session",
|
||||
"dep:async-trait",
|
||||
|
||||
@@ -28,9 +28,8 @@ impl Default for User {
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr {
|
||||
pub use super::{User, UserPasshash};
|
||||
pub use axum_session_auth::{
|
||||
Authentication, HasPermission, SessionSqlitePool,
|
||||
};
|
||||
pub use axum_session_auth::{Authentication, HasPermission};
|
||||
use axum_session_sqlx::SessionSqlitePool;
|
||||
pub use sqlx::SqlitePool;
|
||||
pub use std::collections::HashSet;
|
||||
pub type AuthSession = axum_session_auth::AuthSession<
|
||||
|
||||
@@ -8,10 +8,10 @@ use leptos_axum::ResponseOptions;
|
||||
#[component]
|
||||
pub fn ErrorTemplate(
|
||||
#[prop(optional)] outside_errors: Option<Errors>,
|
||||
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||
#[prop(optional)] errors: Option<ArcRwSignal<Errors>>,
|
||||
) -> impl IntoView {
|
||||
let errors = match outside_errors {
|
||||
Some(e) => RwSignal::new(e),
|
||||
Some(e) => ArcRwSignal::new(e),
|
||||
None => match errors {
|
||||
Some(e) => e,
|
||||
None => panic!("No Errors found and we expected errors!"),
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
use crate::{error_template::ErrorTemplate, errors::TodoAppError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::{view, Errors, LeptosOptions};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
pub async fn file_and_error_handler(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let mut errors = Errors::default();
|
||||
errors.insert_with_default_key(TodoAppError::NotFound);
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
move || view! {<ErrorTemplate outside_errors=errors.clone()/>},
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@ pub mod auth;
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod state;
|
||||
pub mod todo;
|
||||
|
||||
@@ -14,5 +12,5 @@ pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(TodoApp);
|
||||
leptos::mount::hydrate_body(TodoApp);
|
||||
}
|
||||
|
||||
@@ -7,14 +7,16 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::{AuthConfig, AuthSessionLayer, SessionSqlitePool};
|
||||
use leptos::{get_configuration, logging::log, provide_context};
|
||||
use axum_session_auth::{AuthConfig, AuthSessionLayer};
|
||||
use axum_session_sqlx::SessionSqlitePool;
|
||||
use leptos::{
|
||||
config::get_configuration, logging::log, prelude::provide_context,
|
||||
};
|
||||
use leptos_axum::{
|
||||
generate_route_list, handle_server_fns_with_context, LeptosRoutes,
|
||||
};
|
||||
use session_auth_axum::{
|
||||
auth::{ssr::AuthSession, User},
|
||||
fallback::file_and_error_handler,
|
||||
state::AppState,
|
||||
todo::*,
|
||||
};
|
||||
@@ -40,19 +42,19 @@ async fn server_fn_handler(
|
||||
|
||||
async fn leptos_routes_handler(
|
||||
auth_session: AuthSession,
|
||||
State(app_state): State<AppState>,
|
||||
state: State<AppState>,
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let State(app_state) = state.clone();
|
||||
let handler = leptos_axum::render_route_with_context(
|
||||
app_state.leptos_options.clone(),
|
||||
app_state.routes.clone(),
|
||||
move || {
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
},
|
||||
TodoApp,
|
||||
move || shell(app_state.leptos_options.clone()),
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
handler(state, req).await.into_response()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -111,7 +113,7 @@ async fn main() {
|
||||
get(server_fn_handler).post(server_fn_handler),
|
||||
)
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
|
||||
.fallback(file_and_error_handler)
|
||||
.fallback(leptos_axum::file_and_error_handler::<AppState, _>(shell))
|
||||
.layer(
|
||||
AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(
|
||||
Some(pool.clone()),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use axum::extract::FromRef;
|
||||
use leptos::LeptosOptions;
|
||||
use leptos_router::RouteListing;
|
||||
use leptos::prelude::LeptosOptions;
|
||||
use leptos_axum::AxumRouteListing;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
/// This takes advantage of Axum's SubStates feature by deriving FromRef. This is the only way to have more than one
|
||||
@@ -9,5 +9,5 @@ use sqlx::SqlitePool;
|
||||
pub struct AppState {
|
||||
pub leptos_options: LeptosOptions,
|
||||
pub pool: SqlitePool,
|
||||
pub routes: Vec<RouteListing>,
|
||||
pub routes: Vec<AxumRouteListing>,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{auth::*, error_template::ErrorTemplate};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use leptos_router::{components::*, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -109,13 +109,33 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
.map(|_| ())?)
|
||||
}
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<AutoReload options=options.clone() />
|
||||
<HydrationScripts options/>
|
||||
<link rel="stylesheet" id="leptos" href="/pkg/session_auth_axum.css"/>
|
||||
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
|
||||
<MetaTags/>
|
||||
</head>
|
||||
<body>
|
||||
<TodoApp/>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp() -> impl IntoView {
|
||||
let login = create_server_action::<Login>();
|
||||
let logout = create_server_action::<Logout>();
|
||||
let signup = create_server_action::<Signup>();
|
||||
let login = ServerAction::<Login>::new();
|
||||
let logout = ServerAction::<Logout>::new();
|
||||
let signup = ServerAction::<Signup>::new();
|
||||
|
||||
let user = create_resource(
|
||||
let user = Resource::new(
|
||||
move || {
|
||||
(
|
||||
login.version().get(),
|
||||
@@ -128,8 +148,6 @@ pub fn TodoApp() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Stylesheet id="leptos" href="/pkg/session_auth_axum.css"/>
|
||||
<Router>
|
||||
<header>
|
||||
<A href="/">
|
||||
@@ -149,7 +167,7 @@ pub fn TodoApp() -> impl IntoView {
|
||||
", "
|
||||
<span>{format!("Login error: {}", e)}</span>
|
||||
}
|
||||
.into_view()
|
||||
.into_any()
|
||||
}
|
||||
Ok(None) => {
|
||||
view! {
|
||||
@@ -159,7 +177,7 @@ pub fn TodoApp() -> impl IntoView {
|
||||
", "
|
||||
<span>"Logged out."</span>
|
||||
}
|
||||
.into_view()
|
||||
.into_any()
|
||||
}
|
||||
Ok(Some(user)) => {
|
||||
view! {
|
||||
@@ -169,7 +187,7 @@ pub fn TodoApp() -> impl IntoView {
|
||||
{format!("Logged in as: {} ({})", user.username, user.id)}
|
||||
</span>
|
||||
}
|
||||
.into_view()
|
||||
.into_any()
|
||||
}
|
||||
})
|
||||
}}
|
||||
@@ -178,13 +196,15 @@ pub fn TodoApp() -> impl IntoView {
|
||||
</header>
|
||||
<hr/>
|
||||
<main>
|
||||
<Routes>
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
// Route
|
||||
<Route path="" view=Todos/>
|
||||
<Route path="signup" view=move || view! { <Signup action=signup/> }/>
|
||||
<Route path="login" view=move || view! { <Login action=login/> }/>
|
||||
<Route
|
||||
path="settings"
|
||||
<Route path=path!("") view=Todos/>
|
||||
<Route path=path!("signup") view=move || view! { <Signup action=signup/> }/>
|
||||
<Route path=path!("login") view=move || view! { <Login action=login/> }/>
|
||||
<ProtectedRoute
|
||||
path=path!("settings")
|
||||
condition=move || user.get().map(|r| r.ok().flatten().is_some())
|
||||
redirect_path=|| "/"
|
||||
view=move || {
|
||||
view! {
|
||||
<h1>"Settings"</h1>
|
||||
@@ -193,7 +213,7 @@ pub fn TodoApp() -> impl IntoView {
|
||||
}
|
||||
/>
|
||||
|
||||
</Routes>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
@@ -201,12 +221,12 @@ pub fn TodoApp() -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
pub fn Todos() -> impl IntoView {
|
||||
let add_todo = create_server_multi_action::<AddTodo>();
|
||||
let delete_todo = create_server_action::<DeleteTodo>();
|
||||
let add_todo = ServerMultiAction::<AddTodo>::new();
|
||||
let delete_todo = ServerAction::<DeleteTodo>::new();
|
||||
let submissions = add_todo.submissions();
|
||||
|
||||
// list of todos is loaded from the server in reaction to changes
|
||||
let todos = create_resource(
|
||||
let todos = Resource::new(
|
||||
move || (add_todo.version().get(), delete_todo.version().get()),
|
||||
move |_| get_todos(),
|
||||
);
|
||||
@@ -231,11 +251,11 @@ pub fn Todos() -> impl IntoView {
|
||||
view! {
|
||||
<pre class="error">"Server Error: " {e.to_string()}</pre>
|
||||
}
|
||||
.into_view()
|
||||
.into_any()
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
view! { <p>"No tasks were found."</p> }.into_view()
|
||||
view! { <p>"No tasks were found."</p> }.into_any()
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
@@ -252,10 +272,11 @@ pub fn Todos() -> impl IntoView {
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.unwrap_or(().into_any())
|
||||
}
|
||||
};
|
||||
let pending_todos = move || {
|
||||
@@ -266,7 +287,7 @@ pub fn Todos() -> impl IntoView {
|
||||
.map(|submission| {
|
||||
view! {
|
||||
<li class="pending">
|
||||
{move || submission.input.get().map(|data| data.title)}
|
||||
{move || submission.input().get().map(|data| data.title)}
|
||||
</li>
|
||||
}
|
||||
})
|
||||
@@ -282,9 +303,7 @@ pub fn Todos() -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Login(
|
||||
action: Action<Login, Result<(), ServerFnError>>,
|
||||
) -> impl IntoView {
|
||||
pub fn Login(action: ServerAction<Login>) -> impl IntoView {
|
||||
view! {
|
||||
<ActionForm action=action>
|
||||
<h1>"Log In"</h1>
|
||||
@@ -317,9 +336,7 @@ pub fn Login(
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Signup(
|
||||
action: Action<Signup, Result<(), ServerFnError>>,
|
||||
) -> impl IntoView {
|
||||
pub fn Signup(action: ServerAction<Signup>) -> impl IntoView {
|
||||
view! {
|
||||
<ActionForm action=action>
|
||||
<h1>"Sign Up"</h1>
|
||||
@@ -362,9 +379,7 @@ pub fn Signup(
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Logout(
|
||||
action: Action<Logout, Result<(), ServerFnError>>,
|
||||
) -> impl IntoView {
|
||||
pub fn Logout(action: ServerAction<Logout>) -> impl IntoView {
|
||||
view! {
|
||||
<div id="loginbox">
|
||||
<ActionForm action=action>
|
||||
|
||||
Reference in New Issue
Block a user