feat: got sessions working
This commit is contained in:
parent
bf879bb081
commit
0d6b2b4c16
160
Cargo.lock
generated
160
Cargo.lock
generated
@ -197,6 +197,7 @@ checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"axum-macros",
|
||||
"base64",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
@ -249,23 +250,38 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-login"
|
||||
version = "0.16.0"
|
||||
name = "axum-extra"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5260ed0ecc8ace8e7e61a7406672faba598c8a86b8f4742fcdde0ddc979a318f"
|
||||
checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"form_urlencoded",
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"cookie",
|
||||
"fastrand",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"multer",
|
||||
"pin-project-lite",
|
||||
"serde",
|
||||
"subtle",
|
||||
"thiserror 1.0.69",
|
||||
"tower-cookies",
|
||||
"tower 0.5.2",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tower-sessions",
|
||||
"tracing",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-macros"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -724,7 +740,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1841,7 +1856,6 @@ checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2533,28 +2547,6 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp"
|
||||
version = "0.8.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"num-traits",
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp-serde"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"rmp",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpassword"
|
||||
version = "7.3.1"
|
||||
@ -2962,9 +2954,8 @@ name = "sparse-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-login",
|
||||
"axum-extra",
|
||||
"axum-server",
|
||||
"cfg-if",
|
||||
"chrono",
|
||||
@ -2986,13 +2977,10 @@ dependencies = [
|
||||
"sqlx",
|
||||
"structopt",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tower 0.4.13",
|
||||
"tower-http 0.5.2",
|
||||
"tower-sessions",
|
||||
"tower-sessions-sqlx-store",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"wasm-bindgen",
|
||||
@ -3067,7 +3055,6 @@ dependencies = [
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"thiserror 2.0.11",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
@ -3152,7 +3139,6 @@ dependencies = [
|
||||
"sqlx-core",
|
||||
"stringprep",
|
||||
"thiserror 2.0.11",
|
||||
"time",
|
||||
"tracing",
|
||||
"whoami",
|
||||
]
|
||||
@ -3191,7 +3177,6 @@ dependencies = [
|
||||
"sqlx-core",
|
||||
"stringprep",
|
||||
"thiserror 2.0.11",
|
||||
"time",
|
||||
"tracing",
|
||||
"whoami",
|
||||
]
|
||||
@ -3216,7 +3201,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_urlencoded",
|
||||
"sqlx-core",
|
||||
"time",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
@ -3640,23 +3624,6 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-cookies"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"cookie",
|
||||
"futures-util",
|
||||
"http",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.5.2"
|
||||
@ -3721,71 +3688,6 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tower-sessions"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65856c81ee244e0f8a55ab0f7b769b72fbde387c235f0a73cd97c579818d05eb"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"http",
|
||||
"time",
|
||||
"tokio",
|
||||
"tower-cookies",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tower-sessions-core",
|
||||
"tower-sessions-memory-store",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-sessions-core"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb6abbfcaf6436ec5a772cd9f965401da12db793e404ae6134eac066fa5a04f3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"base64",
|
||||
"futures",
|
||||
"http",
|
||||
"parking_lot",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-sessions-memory-store"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fad75660c8afbe74f4e7cbbe8e9090171a056b57370ea4d7d5e9eb3e4af3092"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"time",
|
||||
"tokio",
|
||||
"tower-sessions-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-sessions-sqlx-store"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdd38eba51214e99accab78f6b7c8e273e90a9cb57575e86b592c60074e182d7"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"rmp-serde",
|
||||
"sqlx",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
"tower-sessions-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
@ -3973,12 +3875,6 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
|
||||
@ -87,6 +87,11 @@
|
||||
rust-analyzer
|
||||
zls
|
||||
|
||||
# Debuggers
|
||||
gdb
|
||||
pwndbg
|
||||
gdbgui
|
||||
|
||||
# Cargo lint tools
|
||||
taplo
|
||||
cargo-deny
|
||||
|
||||
@ -9,13 +9,14 @@ crate-type = ["cdylib", "rlib"]
|
||||
[dependencies]
|
||||
leptos = { version = "^0.7", features = ["nightly"] }
|
||||
leptos_router = { version = "^0.7", features = ["nightly"] }
|
||||
axum = { version = "^0.7", features = ["ws"], optional = true }
|
||||
axum = { version = "^0.7", features = ["ws", "macros"], optional = true }
|
||||
axum-extra = { version = "^0.9", features = ["cookie"], optional = true }
|
||||
console_error_panic_hook = "0.1"
|
||||
leptos_axum = { version = "^0.7", optional = true }
|
||||
leptos_meta = { version = "^0.7" }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "signal"], optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs", "compression-br", "compression-deflate", "compression-gzip", "compression-zstd"], optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs", "compression-br", "compression-deflate", "compression-gzip", "compression-zstd", "trace"], optional = true }
|
||||
wasm-bindgen = "0.2"
|
||||
thiserror = "1"
|
||||
http = "1"
|
||||
@ -33,28 +34,20 @@ codee = "0.2"
|
||||
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "sqlx-sqlite"], optional = true }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
rpassword = { version = "7.3", optional = true }
|
||||
pbkdf2 = { version = "0.12", features = ["simple", "sha2"], optional = true }
|
||||
pbkdf2 = { version = "0.12", features = ["simple", "sha2", "std"], optional = true }
|
||||
sha2 = { version = "0.10", optional = true }
|
||||
hex = { version = "0.4", optional = true }
|
||||
serde = "1.0"
|
||||
axum-login = { version = "0.16.0", optional = true }
|
||||
async-trait = "0.1.85"
|
||||
cfg-if = "1.0.0"
|
||||
tower-sessions = { version = "0.13.0", optional = true }
|
||||
tower-sessions-sqlx-store = { version = "0.14.0", features = ["sqlite"], optional = true }
|
||||
time = { version = "0.3.37", optional = true }
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "chrono/wasmbind"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:axum-login",
|
||||
"dep:axum-extra",
|
||||
"dep:tokio",
|
||||
"dep:time",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tower-sessions",
|
||||
"dep:tower-sessions-sqlx-store",
|
||||
"dep:leptos_axum",
|
||||
"dep:axum-server",
|
||||
"dep:tracing-subscriber",
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
CREATE TABLE sessions (
|
||||
session_id char(64) NOT NULL,
|
||||
user_id int NOT NULL,
|
||||
expires int NOT NULL,
|
||||
|
||||
PRIMARY KEY (session_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users
|
||||
);
|
||||
@ -2,6 +2,7 @@ use leptos::prelude::*;
|
||||
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
|
||||
use leptos_router::{
|
||||
components::{A, Route, Router, Routes},
|
||||
hooks::use_query_map,
|
||||
path
|
||||
};
|
||||
use serde::{Serialize, Deserialize};
|
||||
@ -21,56 +22,38 @@ pub async fn test_retrieve() -> Result<u64, ServerFnError> {
|
||||
Ok(since_the_epoch)
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
user_id: i64,
|
||||
user_name: String,
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn me() -> Result<Option<User>, ServerFnError> {
|
||||
let session: crate::db::user::AuthSession = leptos_axum::extract().await?;
|
||||
pub async fn me() -> Result<Option<User>, ServerFnError> {
|
||||
tracing::info!("I'm being checked!");
|
||||
|
||||
Ok(session.user.map(|user| User {
|
||||
let user = crate::db::user::get_auth_session().await?;
|
||||
|
||||
tracing::debug!("User returned: {:?}", user);
|
||||
|
||||
Ok(user.map(|user| User {
|
||||
user_id: user.user_id,
|
||||
user_name: user.user_name
|
||||
}))
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn login(username: String, password: String, next: Option<String>) -> Result<(), ServerFnError> {
|
||||
use leptos::server_fn::error::NoCustomError;
|
||||
async fn login(username: String, password: String, next: String) -> Result<(), ServerFnError> {
|
||||
crate::db::user::create_auth_session(username, password).await?;
|
||||
|
||||
let mut session: crate::db::user::AuthSession = leptos_axum::extract().await?;
|
||||
|
||||
let user = match session.authenticate((username, password).clone()).await {
|
||||
Ok(Some(user)) => user,
|
||||
Ok(None) => return Err(ServerFnError::<NoCustomError>::ServerError("Invalid credentials".to_string())),
|
||||
Err(e) => return Err(server_fn::server_fn_error!(e).into())
|
||||
};
|
||||
|
||||
if let Err(e) = session.login(&user).await {
|
||||
return Err(server_fn::server_fn_error!(e).into());
|
||||
}
|
||||
|
||||
if let Some(target) = next {
|
||||
leptos_axum::redirect(&target);
|
||||
}
|
||||
leptos_axum::redirect(&next);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn logout() -> Result<(), ServerFnError> {
|
||||
let mut session: crate::db::user::AuthSession = leptos_axum::extract().await?;
|
||||
|
||||
match session.logout().await {
|
||||
Ok(_) => {
|
||||
leptos_axum::redirect("/login");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(server_fn::server_fn_error!(e).into())
|
||||
}
|
||||
crate::db::user::destroy_auth_session().await
|
||||
}
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
@ -95,12 +78,38 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
|
||||
let user = Resource::new(|| (), |_| async { me().await });
|
||||
let login = ServerAction::<Login>::new();
|
||||
|
||||
let (user_res, set_user_res) = signal(None);
|
||||
|
||||
let user = Resource::new(
|
||||
move || {
|
||||
login.version().get()
|
||||
},
|
||||
|_| async {
|
||||
#[cfg(feature = "ssr")]
|
||||
tracing::info!("Checking user account");
|
||||
#[cfg(feature = "hydrate")]
|
||||
leptos::logging::log!("Checking user account");
|
||||
|
||||
me().await
|
||||
}
|
||||
);
|
||||
|
||||
Effect::new(move || {
|
||||
let u = user.get();
|
||||
set_user_res(u.map(|u2| u2.ok()).flatten().flatten());
|
||||
});
|
||||
|
||||
provide_context(user_res);
|
||||
|
||||
Effect::new(move || {
|
||||
leptos::logging::log!("User resource: {:?}", user.get());
|
||||
});
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/sparse-server.css"/>
|
||||
|
||||
// sets the document title
|
||||
<Title text="Sparse Control"/>
|
||||
|
||||
<Router>
|
||||
@ -108,14 +117,14 @@ pub fn App() -> impl IntoView {
|
||||
<h1>"Sparse control"</h1>
|
||||
<A href="/">"Home"</A>
|
||||
<Suspense fallback=|| ()>
|
||||
<A href="/beacons">"Beacon management"</A>
|
||||
<A href="/users">"Users"</A>
|
||||
{move || user
|
||||
.get()
|
||||
.map(|err| err.ok())
|
||||
.flatten()
|
||||
.flatten()
|
||||
.map(|_| view! {
|
||||
<A href="/beacons">"Beacon management"</A>
|
||||
<A href="/users">"Users"</A>
|
||||
<a
|
||||
href="#"
|
||||
on:click=move |_| {
|
||||
@ -145,7 +154,7 @@ pub fn App() -> impl IntoView {
|
||||
|
||||
<Routes fallback=|| "Page not found.".into_view()>
|
||||
<Route path=path!("users") view=crate::users::UserView />
|
||||
<Route path=path!("login") view=move || view! { <LoginPage /> } />
|
||||
<Route path=path!("login") view=move || view! { <LoginPage login/> }/>
|
||||
<Route path=path!("") view=HomePage/>
|
||||
</Routes>
|
||||
</Router>
|
||||
@ -153,98 +162,40 @@ pub fn App() -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn LoginPage() -> impl IntoView {
|
||||
fn LoginPage(login: ServerAction<Login>) -> impl IntoView {
|
||||
let next = move || use_query_map().read().get("next").unwrap_or("/".to_string());
|
||||
|
||||
view! {
|
||||
<main class="login">
|
||||
{move || match login.value().get() {
|
||||
Some(Ok(_)) => None,
|
||||
None => None,
|
||||
Some(Err(e)) => Some(view! {
|
||||
<div>
|
||||
"Error signing in: "
|
||||
{format!("{e:?}")}
|
||||
</div>
|
||||
})
|
||||
}}
|
||||
<ActionForm action=login>
|
||||
<label>"Username"</label>
|
||||
<input type="text" name="username" />
|
||||
<label>"Password"</label>
|
||||
<input type="password" name="password" />
|
||||
<div></div>
|
||||
<input type="submit" value="Submit" />
|
||||
<input type="hidden" value=next name="next" />
|
||||
</ActionForm>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the home page of your application.
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
|
||||
// Creates a reactive value to update the button
|
||||
let count = RwSignal::new(0);
|
||||
let on_click = move |_| *count.write() += 1;
|
||||
|
||||
let loaded_time = Resource::new(|| (), |_| async move { test_retrieve().await });
|
||||
|
||||
let (requested_time, set_requested_time) = signal(None::<String>);
|
||||
let request_time = Action::new(move |_: &()| async move {
|
||||
let t = match test_retrieve().await {
|
||||
Ok(t) => format!("{}", t),
|
||||
Err(_) => "Error!".to_string()
|
||||
};
|
||||
set_requested_time(Some(t));
|
||||
});
|
||||
let request_time_callback = move |_| {
|
||||
request_time.dispatch(());
|
||||
};
|
||||
let pending = request_time.pending();
|
||||
|
||||
let text_input = RwSignal::new("".to_owned());
|
||||
#[cfg_attr(feature = "ssr", allow(unused_variables))]
|
||||
let (messages, set_messages) = signal(Vec::<String>::new());
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use leptos_use::{UseWebSocketReturn, use_websocket};
|
||||
|
||||
let UseWebSocketReturn { send, message, .. } = use_websocket::<String, String, codee::string::FromToStringCodec>("/ws");
|
||||
|
||||
Effect::new(move |_| {
|
||||
message.with(move |message| {
|
||||
if let Some(m) = message {
|
||||
leptos::logging::log!("got update: {}", m);
|
||||
set_messages.update(|messages: &mut Vec<_>| messages.push(format!("msg: {}", m)));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let send_message = move |_| {
|
||||
send(&text_input.get());
|
||||
text_input.set("".to_string());
|
||||
};
|
||||
} else {
|
||||
let send_message = move |_| {};
|
||||
}
|
||||
}
|
||||
|
||||
view! {
|
||||
<main class="main">
|
||||
<h1>"Welcome to Leptos!"</h1>
|
||||
<button on:click=on_click>"Click Me: " {count}</button>
|
||||
<Suspense
|
||||
fallback=move || view! { <p>"Loading..."</p> }
|
||||
>
|
||||
<h2>"Loaded time:"</h2>
|
||||
{move || {
|
||||
loaded_time.get()
|
||||
.map(|time| match time {
|
||||
Ok(t) => view! { <p>{format!("{}", t)}</p>},
|
||||
Err(_) => view! { <p>{"Error!".to_string()}</p> }
|
||||
})
|
||||
}}
|
||||
<h2>"Requested time:"</h2>
|
||||
<button on:click=request_time_callback>"Request new time"</button>
|
||||
{move || pending.get().then_some("Loading...")}
|
||||
{move || match requested_time.get() {
|
||||
Some(t) => {
|
||||
leptos::logging::log!("updating time display");
|
||||
view! { <p>{t}</p> }
|
||||
},
|
||||
None => view! { <p>{"N/A".to_string()}</p> }
|
||||
}}
|
||||
</Suspense>
|
||||
<h2>"Messages"</h2>
|
||||
<input bind:value=text_input />
|
||||
<input
|
||||
on:click=send_message
|
||||
type="button"
|
||||
value="Send message"
|
||||
/>
|
||||
{move || messages
|
||||
.get()
|
||||
.iter()
|
||||
.map(|message| view! { <p>{message.clone()}</p> })
|
||||
.collect::<Vec<_>>()}
|
||||
<h1>"Welcome to sparse!"</h1>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,11 @@
|
||||
use leptos::{prelude::expect_context, server_fn::error::NoCustomError};
|
||||
use leptos_axum::{extract, ResponseOptions};
|
||||
use leptos::prelude::ServerFnError;
|
||||
use pbkdf2::{Pbkdf2, password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, rand_core::{OsRng, RngCore}, SaltString}};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct User {
|
||||
pub user_id: i64,
|
||||
@ -6,13 +14,6 @@ pub struct User {
|
||||
pub last_active: Option<i64>
|
||||
}
|
||||
|
||||
use async_trait::async_trait;
|
||||
use pbkdf2::{Pbkdf2, password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, rand_core::OsRng, SaltString}};
|
||||
use axum_login::{AuthUser, AuthnBackend, UserId};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
impl std::fmt::Debug for User {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("User")
|
||||
@ -23,27 +24,16 @@ impl std::fmt::Debug for User {
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthUser for User {
|
||||
type Id = i64;
|
||||
|
||||
fn id(&self) -> Self::Id {
|
||||
self.user_id
|
||||
}
|
||||
|
||||
fn session_auth_hash(&self) -> &[u8] {
|
||||
self.password_hash.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
async fn hash_password(pass: &[u8]) -> Result<String, Error> {
|
||||
Ok(tokio::task::spawn_blocking({
|
||||
let pass = pass.to_owned();
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
|
||||
move || Pbkdf2.hash_password(
|
||||
move ||
|
||||
Pbkdf2.hash_password(
|
||||
&*pass,
|
||||
&salt,
|
||||
).map(|hash| hash.to_string())
|
||||
).map(|hash| hash.serialize().as_str().to_string())
|
||||
}).await??)
|
||||
}
|
||||
|
||||
@ -114,67 +104,149 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Backend(SqlitePool);
|
||||
const SESSION_ID_KEY: &'static str = "session_id";
|
||||
const SESSION_AGE: i64 = 30 * 60;
|
||||
|
||||
impl Backend {
|
||||
pub fn new(db: SqlitePool) -> Self {
|
||||
Self(db)
|
||||
}
|
||||
}
|
||||
pub async fn create_auth_session(username: String, password: String) -> Result<(), ServerFnError> {
|
||||
use axum_extra::extract::cookie::{Cookie, SameSite};
|
||||
use axum::http::{header, HeaderValue};
|
||||
|
||||
#[async_trait]
|
||||
impl AuthnBackend for Backend {
|
||||
type User = User;
|
||||
type Credentials = (String, String);
|
||||
type Error = Error;
|
||||
let db = expect_context::<SqlitePool>();
|
||||
let resp = expect_context::<ResponseOptions>();
|
||||
|
||||
async fn authenticate(
|
||||
&self,
|
||||
creds: Self::Credentials
|
||||
) -> Result<Option<Self::User>, Self::Error> {
|
||||
let user: Option<Self::User> = sqlx::query_as!(
|
||||
let user: Option<User> = sqlx::query_as!(
|
||||
User,
|
||||
"SELECT * FROM users WHERE user_name = ?",
|
||||
creds.0
|
||||
username
|
||||
)
|
||||
.fetch_optional(&self.0)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
|
||||
let Some(user) = user else { return Ok(None); };
|
||||
let Some(user) = user else {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError("Invalid credentials".to_string()));
|
||||
};
|
||||
|
||||
let good_hash = verify_password(
|
||||
&user.password_hash,
|
||||
&creds.1
|
||||
&password,
|
||||
&user.password_hash
|
||||
).await?;
|
||||
|
||||
if good_hash {
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
let expires = now + SESSION_AGE;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE users SET last_active = ?",
|
||||
now
|
||||
)
|
||||
.execute(&self.0)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
Ok(Some(user))
|
||||
let session_id: String = tokio::task::spawn_blocking(|| {
|
||||
let mut key = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut key);
|
||||
hex::encode(&key[..])
|
||||
}).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO sessions (session_id, user_id, expires) VALUES (?, ?, ?)",
|
||||
session_id,
|
||||
user.user_id,
|
||||
expires
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let cookie = Cookie::build((SESSION_ID_KEY, &session_id))
|
||||
.http_only(true)
|
||||
.path("/")
|
||||
.same_site(SameSite::Lax);
|
||||
|
||||
if let Ok(cookie) = HeaderValue::from_str(&cookie.to_string()) {
|
||||
resp.insert_header(header::SET_COOKIE, cookie);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(None)
|
||||
Err(ServerFnError::<NoCustomError>::ServerError("Invalid credentials".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
|
||||
let user: Option<Self::User> = sqlx::query_as!(
|
||||
User,
|
||||
"SELECT * FROM users WHERE user_id = ?",
|
||||
user_id
|
||||
pub async fn destroy_auth_session() -> Result<(), ServerFnError> {
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
|
||||
let db = expect_context::<SqlitePool>();
|
||||
let jar = extract::<CookieJar>().await?;
|
||||
|
||||
let Some(cookie) = jar.get(SESSION_ID_KEY) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let session_id = cookie.value();
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM sessions WHERE session_id = ?",
|
||||
session_id
|
||||
)
|
||||
.fetch_optional(&self.0)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_auth_session() -> Result<Option<User>, ServerFnError> {
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
|
||||
let db = expect_context::<SqlitePool>();
|
||||
let jar = extract::<CookieJar>().await?;
|
||||
|
||||
let Some(cookie) = jar.get(SESSION_ID_KEY) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
let session_id = cookie.value();
|
||||
|
||||
let user = sqlx::query_as!(
|
||||
User,
|
||||
"SELECT users.user_id, user_name, password_hash, last_active \
|
||||
FROM users \
|
||||
INNER JOIN sessions \
|
||||
WHERE session_id = ? \
|
||||
AND expires > ?",
|
||||
session_id,
|
||||
now
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
|
||||
if let Some(u) = &user {
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
let expires = now + SESSION_AGE;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE users SET last_active = ? WHERE user_id = ?",
|
||||
now,
|
||||
u.user_id
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE sessions SET expires = ? WHERE session_id = ?",
|
||||
expires,
|
||||
session_id
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM sessions WHERE expires < ?",
|
||||
now
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
pub type AuthSession = axum_login::AuthSession<Backend>;
|
||||
|
||||
@ -38,7 +38,7 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| format!("{}=debug", env!("CARGO_CRATE_NAME")).into()),
|
||||
.unwrap_or_else(|_| format!("{}=debug,tower_http=trace", env!("CARGO_CRATE_NAME")).into()),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
@ -3,7 +3,9 @@ use leptos::prelude::*;
|
||||
use serde::{Serialize, Deserialize};
|
||||
#[cfg(feature = "ssr")]
|
||||
use {
|
||||
sqlx::SqlitePool
|
||||
sqlx::SqlitePool,
|
||||
leptos::server_fn::error::NoCustomError,
|
||||
crate::db::user
|
||||
};
|
||||
|
||||
fn format_delta(time: chrono::TimeDelta) -> String {
|
||||
@ -42,6 +44,12 @@ pub struct PubUser {
|
||||
|
||||
#[server]
|
||||
async fn delete_user(user_id: i64) -> Result<(), ServerFnError> {
|
||||
let user = user::get_auth_session().await?;
|
||||
|
||||
if user.is_none() {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
|
||||
}
|
||||
|
||||
let pool = expect_context::<SqlitePool>();
|
||||
|
||||
sqlx::query!(
|
||||
@ -56,6 +64,12 @@ async fn delete_user(user_id: i64) -> Result<(), ServerFnError> {
|
||||
|
||||
#[server]
|
||||
async fn reset_password(user_id: i64, password: String) -> Result<(), ServerFnError> {
|
||||
let user = user::get_auth_session().await?;
|
||||
|
||||
if user.is_none() {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
|
||||
}
|
||||
|
||||
let pool = expect_context::<SqlitePool>();
|
||||
|
||||
crate::db::user::reset_password(&pool, user_id as i16, password).await?;
|
||||
@ -67,7 +81,9 @@ async fn reset_password(user_id: i64, password: String) -> Result<(), ServerFnEr
|
||||
pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl IntoView {
|
||||
use leptos_use::{use_interval, UseIntervalReturn};
|
||||
|
||||
#[cfg_attr(feature = "ssr", allow(unused_variables))]
|
||||
let UseIntervalReturn { counter, .. } = use_interval(1000);
|
||||
#[cfg_attr(feature = "ssr", allow(unused_variables))]
|
||||
let (time_ago, set_time_ago) = signal(user.last_active.map(|active| format_delta(Utc::now() - active)));
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
@ -189,6 +205,12 @@ pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl Into
|
||||
|
||||
#[server]
|
||||
async fn list_users() -> Result<Vec<PubUser>, ServerFnError> {
|
||||
let user = user::get_auth_session().await?;
|
||||
|
||||
if user.is_none() {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
|
||||
}
|
||||
|
||||
use futures::stream::StreamExt;
|
||||
|
||||
let pool = expect_context::<SqlitePool>();
|
||||
@ -213,6 +235,12 @@ async fn list_users() -> Result<Vec<PubUser>, ServerFnError> {
|
||||
|
||||
#[server]
|
||||
async fn add_user(name: String, password: String) -> Result<(), ServerFnError> {
|
||||
let user = user::get_auth_session().await?;
|
||||
|
||||
if user.is_none() {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
|
||||
}
|
||||
|
||||
let pool = expect_context::<SqlitePool>();
|
||||
|
||||
crate::db::user::create_user(&pool, name, password).await?;
|
||||
@ -222,6 +250,15 @@ async fn add_user(name: String, password: String) -> Result<(), ServerFnError> {
|
||||
|
||||
#[component]
|
||||
pub fn UserView() -> impl IntoView {
|
||||
#[cfg(feature = "hydrate")]
|
||||
Effect::new(move || {
|
||||
let user = expect_context::<ReadSignal<Option<crate::app::User>>>();
|
||||
if user.get().is_none() {
|
||||
let navigate = leptos_router::hooks::use_navigate();
|
||||
navigate("/login?next=/users", Default::default());
|
||||
}
|
||||
});
|
||||
|
||||
let user_list = Resource::new(|| (), |_| async move { list_users().await });
|
||||
|
||||
let modal_ref = NodeRef::<leptos::html::Dialog>::new();
|
||||
|
||||
@ -4,68 +4,15 @@ use sqlx::sqlite::SqlitePool;
|
||||
use axum::Router;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use tokio::{signal, task::AbortHandle};
|
||||
use tower_sessions::{Expiry, SessionManagerLayer, session_store::ExpiredDeletion};
|
||||
use tower_sessions_sqlx_store::SqliteStore;
|
||||
use tokio::signal;
|
||||
|
||||
use sparse_server::app::*;
|
||||
|
||||
pub async fn websocket(ws: axum::extract::ws::WebSocketUpgrade) -> axum::response::Response {
|
||||
tracing::info!("Handling websocket request to /ws");
|
||||
ws.on_upgrade(handle_websocket)
|
||||
}
|
||||
|
||||
async fn handle_websocket(mut socket: axum::extract::ws::WebSocket) {
|
||||
use tracing::info;
|
||||
|
||||
let mut count = 0;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = socket.recv() => {
|
||||
match msg {
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
Some(msg) => {
|
||||
let Ok(axum::extract::ws::Message::Text(msg)) = msg else { continue; };
|
||||
|
||||
info!("Received message! {}", msg);
|
||||
|
||||
let Ok(_) = socket.send(axum::extract::ws::Message::Text(msg)).await else { break; };
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = tokio::time::sleep(tokio::time::Duration::from_secs(1)) => {
|
||||
let Ok(_) = socket.send(axum::extract::ws::Message::Text(format!("{}", count))).await else { break; };
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serve_web(management_address: SocketAddrV4, _bind_address: SocketAddrV4, db: SqlitePool) -> anyhow::Result<ExitCode> {
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
let session_store = SqliteStore::new(db.clone());
|
||||
session_store.migrate().await?;
|
||||
|
||||
let deletion_task = tokio::task::spawn(
|
||||
session_store
|
||||
.clone()
|
||||
.continuously_delete_expired(tokio::time::Duration::from_secs(60))
|
||||
);
|
||||
|
||||
let session_layer = SessionManagerLayer::new(session_store)
|
||||
.with_secure(false)
|
||||
.with_expiry(Expiry::OnInactivity(time::Duration::minutes(20)));
|
||||
|
||||
|
||||
let backend = crate::db::user::Backend::new(db.clone());
|
||||
let auth_layer = axum_login::AuthManagerLayerBuilder::new(backend, session_layer).build();
|
||||
|
||||
let compression_layer = tower_http::compression::CompressionLayer::new()
|
||||
.gzip(true)
|
||||
.deflate(true)
|
||||
@ -73,7 +20,6 @@ pub async fn serve_web(management_address: SocketAddrV4, _bind_address: SocketAd
|
||||
.zstd(true);
|
||||
|
||||
let app = Router::new()
|
||||
.route("/ws", axum::routing::any(websocket))
|
||||
.leptos_routes_with_context(
|
||||
&leptos_options,
|
||||
routes,
|
||||
@ -81,11 +27,15 @@ pub async fn serve_web(management_address: SocketAddrV4, _bind_address: SocketAd
|
||||
{
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
})
|
||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||
}
|
||||
)
|
||||
.fallback(leptos_axum::file_and_error_handler::<leptos::config::LeptosOptions, _>(shell))
|
||||
.with_state(leptos_options)
|
||||
.layer(auth_layer)
|
||||
.layer(compression_layer);
|
||||
.layer(
|
||||
tower::ServiceBuilder::new()
|
||||
.layer(tower_http::trace::TraceLayer::new_for_http())
|
||||
.layer(compression_layer)
|
||||
);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
@ -93,15 +43,13 @@ pub async fn serve_web(management_address: SocketAddrV4, _bind_address: SocketAd
|
||||
tracing::info!("management interface listening on http://{}", &management_address);
|
||||
|
||||
axum::serve(management_listener, app.into_make_service())
|
||||
.with_graceful_shutdown(shutdown_signal(deletion_task.abort_handle()))
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await?;
|
||||
|
||||
deletion_task.await??;
|
||||
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
async fn shutdown_signal(deletion_task_abort_handle: AbortHandle) {
|
||||
async fn shutdown_signal() {
|
||||
let ctrl_c = async {
|
||||
signal::ctrl_c()
|
||||
.await
|
||||
@ -122,11 +70,9 @@ async fn shutdown_signal(deletion_task_abort_handle: AbortHandle) {
|
||||
tokio::select! {
|
||||
_ = ctrl_c => {
|
||||
tracing::info!("Received Ctrl-C");
|
||||
deletion_task_abort_handle.abort()
|
||||
},
|
||||
_ = terminate => {
|
||||
tracing::info!("Received terminate command");
|
||||
deletion_task_abort_handle.abort()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user