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 = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum-core",
|
"axum-core",
|
||||||
|
"axum-macros",
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@ -249,23 +250,38 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-login"
|
name = "axum-extra"
|
||||||
version = "0.16.0"
|
version = "0.9.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5260ed0ecc8ace8e7e61a7406672faba598c8a86b8f4742fcdde0ddc979a318f"
|
checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
|
||||||
"axum",
|
"axum",
|
||||||
"form_urlencoded",
|
"axum-core",
|
||||||
|
"bytes",
|
||||||
|
"cookie",
|
||||||
|
"fastrand",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"multer",
|
||||||
|
"pin-project-lite",
|
||||||
"serde",
|
"serde",
|
||||||
"subtle",
|
"tower 0.5.2",
|
||||||
"thiserror 1.0.69",
|
|
||||||
"tower-cookies",
|
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"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]]
|
[[package]]
|
||||||
@ -724,7 +740,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1841,7 +1856,6 @@ checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2533,28 +2547,6 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"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]]
|
[[package]]
|
||||||
name = "rpassword"
|
name = "rpassword"
|
||||||
version = "7.3.1"
|
version = "7.3.1"
|
||||||
@ -2962,9 +2954,8 @@ name = "sparse-server"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
|
||||||
"axum",
|
"axum",
|
||||||
"axum-login",
|
"axum-extra",
|
||||||
"axum-server",
|
"axum-server",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -2986,13 +2977,10 @@ dependencies = [
|
|||||||
"sqlx",
|
"sqlx",
|
||||||
"structopt",
|
"structopt",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"time",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tower 0.4.13",
|
"tower 0.4.13",
|
||||||
"tower-http 0.5.2",
|
"tower-http 0.5.2",
|
||||||
"tower-sessions",
|
|
||||||
"tower-sessions-sqlx-store",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@ -3067,7 +3055,6 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.11",
|
"thiserror 2.0.11",
|
||||||
"time",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
@ -3152,7 +3139,6 @@ dependencies = [
|
|||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror 2.0.11",
|
"thiserror 2.0.11",
|
||||||
"time",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
@ -3191,7 +3177,6 @@ dependencies = [
|
|||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror 2.0.11",
|
"thiserror 2.0.11",
|
||||||
"time",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
@ -3216,7 +3201,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"time",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
@ -3640,23 +3624,6 @@ dependencies = [
|
|||||||
"tracing",
|
"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]]
|
[[package]]
|
||||||
name = "tower-http"
|
name = "tower-http"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@ -3721,71 +3688,6 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
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]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.41"
|
version = "0.1.41"
|
||||||
@ -3973,12 +3875,6 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "urlencoding"
|
|
||||||
version = "2.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf-8"
|
name = "utf-8"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
|
|||||||
@ -87,6 +87,11 @@
|
|||||||
rust-analyzer
|
rust-analyzer
|
||||||
zls
|
zls
|
||||||
|
|
||||||
|
# Debuggers
|
||||||
|
gdb
|
||||||
|
pwndbg
|
||||||
|
gdbgui
|
||||||
|
|
||||||
# Cargo lint tools
|
# Cargo lint tools
|
||||||
taplo
|
taplo
|
||||||
cargo-deny
|
cargo-deny
|
||||||
|
|||||||
@ -9,13 +9,14 @@ crate-type = ["cdylib", "rlib"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
leptos = { version = "^0.7", features = ["nightly"] }
|
leptos = { version = "^0.7", features = ["nightly"] }
|
||||||
leptos_router = { 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"
|
console_error_panic_hook = "0.1"
|
||||||
leptos_axum = { version = "^0.7", optional = true }
|
leptos_axum = { version = "^0.7", optional = true }
|
||||||
leptos_meta = { version = "^0.7" }
|
leptos_meta = { version = "^0.7" }
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "signal"], optional = true }
|
tokio = { version = "1", features = ["rt-multi-thread", "signal"], optional = true }
|
||||||
tower = { version = "0.4", 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"
|
wasm-bindgen = "0.2"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
http = "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 }
|
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "sqlx-sqlite"], optional = true }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
rpassword = { version = "7.3", optional = true }
|
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 }
|
sha2 = { version = "0.10", optional = true }
|
||||||
hex = { version = "0.4", optional = true }
|
hex = { version = "0.4", optional = true }
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
axum-login = { version = "0.16.0", optional = true }
|
|
||||||
async-trait = "0.1.85"
|
|
||||||
cfg-if = "1.0.0"
|
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]
|
[features]
|
||||||
hydrate = ["leptos/hydrate", "chrono/wasmbind"]
|
hydrate = ["leptos/hydrate", "chrono/wasmbind"]
|
||||||
ssr = [
|
ssr = [
|
||||||
"dep:axum",
|
"dep:axum",
|
||||||
"dep:axum-login",
|
"dep:axum-extra",
|
||||||
"dep:tokio",
|
"dep:tokio",
|
||||||
"dep:time",
|
|
||||||
"dep:tower",
|
"dep:tower",
|
||||||
"dep:tower-http",
|
"dep:tower-http",
|
||||||
"dep:tower-sessions",
|
|
||||||
"dep:tower-sessions-sqlx-store",
|
|
||||||
"dep:leptos_axum",
|
"dep:leptos_axum",
|
||||||
"dep:axum-server",
|
"dep:axum-server",
|
||||||
"dep:tracing-subscriber",
|
"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_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
|
||||||
use leptos_router::{
|
use leptos_router::{
|
||||||
components::{A, Route, Router, Routes},
|
components::{A, Route, Router, Routes},
|
||||||
|
hooks::use_query_map,
|
||||||
path
|
path
|
||||||
};
|
};
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
@ -21,56 +22,38 @@ pub async fn test_retrieve() -> Result<u64, ServerFnError> {
|
|||||||
Ok(since_the_epoch)
|
Ok(since_the_epoch)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
user_name: String,
|
user_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
async fn me() -> Result<Option<User>, ServerFnError> {
|
pub async fn me() -> Result<Option<User>, ServerFnError> {
|
||||||
let session: crate::db::user::AuthSession = leptos_axum::extract().await?;
|
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_id: user.user_id,
|
||||||
user_name: user.user_name
|
user_name: user.user_name
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
async fn login(username: String, password: String, next: Option<String>) -> Result<(), ServerFnError> {
|
async fn login(username: String, password: String, next: String) -> Result<(), ServerFnError> {
|
||||||
use leptos::server_fn::error::NoCustomError;
|
crate::db::user::create_auth_session(username, password).await?;
|
||||||
|
|
||||||
let mut session: crate::db::user::AuthSession = leptos_axum::extract().await?;
|
leptos_axum::redirect(&next);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
async fn logout() -> Result<(), ServerFnError> {
|
async fn logout() -> Result<(), ServerFnError> {
|
||||||
let mut session: crate::db::user::AuthSession = leptos_axum::extract().await?;
|
crate::db::user::destroy_auth_session().await
|
||||||
|
|
||||||
match session.logout().await {
|
|
||||||
Ok(_) => {
|
|
||||||
leptos_axum::redirect("/login");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => Err(server_fn::server_fn_error!(e).into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||||
@ -95,12 +78,38 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
|||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
provide_meta_context();
|
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! {
|
view! {
|
||||||
<Stylesheet id="leptos" href="/pkg/sparse-server.css"/>
|
<Stylesheet id="leptos" href="/pkg/sparse-server.css"/>
|
||||||
|
|
||||||
// sets the document title
|
|
||||||
<Title text="Sparse Control"/>
|
<Title text="Sparse Control"/>
|
||||||
|
|
||||||
<Router>
|
<Router>
|
||||||
@ -108,14 +117,14 @@ pub fn App() -> impl IntoView {
|
|||||||
<h1>"Sparse control"</h1>
|
<h1>"Sparse control"</h1>
|
||||||
<A href="/">"Home"</A>
|
<A href="/">"Home"</A>
|
||||||
<Suspense fallback=|| ()>
|
<Suspense fallback=|| ()>
|
||||||
<A href="/beacons">"Beacon management"</A>
|
|
||||||
<A href="/users">"Users"</A>
|
|
||||||
{move || user
|
{move || user
|
||||||
.get()
|
.get()
|
||||||
.map(|err| err.ok())
|
.map(|err| err.ok())
|
||||||
.flatten()
|
.flatten()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|_| view! {
|
.map(|_| view! {
|
||||||
|
<A href="/beacons">"Beacon management"</A>
|
||||||
|
<A href="/users">"Users"</A>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
on:click=move |_| {
|
on:click=move |_| {
|
||||||
@ -145,7 +154,7 @@ pub fn App() -> impl IntoView {
|
|||||||
|
|
||||||
<Routes fallback=|| "Page not found.".into_view()>
|
<Routes fallback=|| "Page not found.".into_view()>
|
||||||
<Route path=path!("users") view=crate::users::UserView />
|
<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/>
|
<Route path=path!("") view=HomePage/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
@ -153,98 +162,40 @@ pub fn App() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[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.
|
/// Renders the home page of your application.
|
||||||
#[component]
|
#[component]
|
||||||
fn HomePage() -> impl IntoView {
|
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! {
|
view! {
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<h1>"Welcome to Leptos!"</h1>
|
<h1>"Welcome to sparse!"</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<_>>()}
|
|
||||||
</main>
|
</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)]
|
#[derive(Clone)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub user_id: i64,
|
pub user_id: i64,
|
||||||
@ -6,13 +14,6 @@ pub struct User {
|
|||||||
pub last_active: Option<i64>
|
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 {
|
impl std::fmt::Debug for User {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("User")
|
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> {
|
async fn hash_password(pass: &[u8]) -> Result<String, Error> {
|
||||||
Ok(tokio::task::spawn_blocking({
|
Ok(tokio::task::spawn_blocking({
|
||||||
let pass = pass.to_owned();
|
let pass = pass.to_owned();
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
|
||||||
move || Pbkdf2.hash_password(
|
move ||
|
||||||
|
Pbkdf2.hash_password(
|
||||||
&*pass,
|
&*pass,
|
||||||
&salt,
|
&salt,
|
||||||
).map(|hash| hash.to_string())
|
).map(|hash| hash.serialize().as_str().to_string())
|
||||||
}).await??)
|
}).await??)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,67 +104,149 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
const SESSION_ID_KEY: &'static str = "session_id";
|
||||||
pub struct Backend(SqlitePool);
|
const SESSION_AGE: i64 = 30 * 60;
|
||||||
|
|
||||||
impl Backend {
|
pub async fn create_auth_session(username: String, password: String) -> Result<(), ServerFnError> {
|
||||||
pub fn new(db: SqlitePool) -> Self {
|
use axum_extra::extract::cookie::{Cookie, SameSite};
|
||||||
Self(db)
|
use axum::http::{header, HeaderValue};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
let db = expect_context::<SqlitePool>();
|
||||||
impl AuthnBackend for Backend {
|
let resp = expect_context::<ResponseOptions>();
|
||||||
type User = User;
|
|
||||||
type Credentials = (String, String);
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
async fn authenticate(
|
let user: Option<User> = sqlx::query_as!(
|
||||||
&self,
|
|
||||||
creds: Self::Credentials
|
|
||||||
) -> Result<Option<Self::User>, Self::Error> {
|
|
||||||
let user: Option<Self::User> = sqlx::query_as!(
|
|
||||||
User,
|
User,
|
||||||
"SELECT * FROM users WHERE user_name = ?",
|
"SELECT * FROM users WHERE user_name = ?",
|
||||||
creds.0
|
username
|
||||||
)
|
)
|
||||||
.fetch_optional(&self.0)
|
.fetch_optional(&db)
|
||||||
.await?;
|
.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(
|
let good_hash = verify_password(
|
||||||
&user.password_hash,
|
&password,
|
||||||
&creds.1
|
&user.password_hash
|
||||||
).await?;
|
).await?;
|
||||||
|
|
||||||
if good_hash {
|
if good_hash {
|
||||||
let now = chrono::Utc::now().timestamp();
|
let now = chrono::Utc::now().timestamp();
|
||||||
|
let expires = now + SESSION_AGE;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"UPDATE users SET last_active = ?",
|
"UPDATE users SET last_active = ?",
|
||||||
now
|
now
|
||||||
)
|
)
|
||||||
.execute(&self.0)
|
.execute(&db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Some(user))
|
let session_id: String = tokio::task::spawn_blocking(|| {
|
||||||
} else {
|
let mut key = [0u8; 32];
|
||||||
Ok(None)
|
OsRng.fill_bytes(&mut key);
|
||||||
}
|
hex::encode(&key[..])
|
||||||
}
|
}).await?;
|
||||||
|
|
||||||
async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
|
sqlx::query!(
|
||||||
let user: Option<Self::User> = sqlx::query_as!(
|
"INSERT INTO sessions (session_id, user_id, expires) VALUES (?, ?, ?)",
|
||||||
User,
|
session_id,
|
||||||
"SELECT * FROM users WHERE user_id = ?",
|
user.user_id,
|
||||||
user_id
|
expires
|
||||||
)
|
)
|
||||||
.fetch_optional(&self.0)
|
.execute(&db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(user)
|
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 {
|
||||||
|
Err(ServerFnError::<NoCustomError>::ServerError("Invalid credentials".to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type AuthSession = axum_login::AuthSession<Backend>;
|
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
|
||||||
|
)
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
|
|||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(
|
.with(
|
||||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
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())
|
.with(tracing_subscriber::fmt::layer())
|
||||||
.init();
|
.init();
|
||||||
|
|||||||
@ -3,7 +3,9 @@ use leptos::prelude::*;
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use {
|
use {
|
||||||
sqlx::SqlitePool
|
sqlx::SqlitePool,
|
||||||
|
leptos::server_fn::error::NoCustomError,
|
||||||
|
crate::db::user
|
||||||
};
|
};
|
||||||
|
|
||||||
fn format_delta(time: chrono::TimeDelta) -> String {
|
fn format_delta(time: chrono::TimeDelta) -> String {
|
||||||
@ -42,6 +44,12 @@ pub struct PubUser {
|
|||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
async fn delete_user(user_id: i64) -> Result<(), ServerFnError> {
|
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>();
|
let pool = expect_context::<SqlitePool>();
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
@ -56,6 +64,12 @@ async fn delete_user(user_id: i64) -> Result<(), ServerFnError> {
|
|||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
async fn reset_password(user_id: i64, password: String) -> Result<(), ServerFnError> {
|
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>();
|
let pool = expect_context::<SqlitePool>();
|
||||||
|
|
||||||
crate::db::user::reset_password(&pool, user_id as i16, password).await?;
|
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 {
|
pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl IntoView {
|
||||||
use leptos_use::{use_interval, UseIntervalReturn};
|
use leptos_use::{use_interval, UseIntervalReturn};
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ssr", allow(unused_variables))]
|
||||||
let UseIntervalReturn { counter, .. } = use_interval(1000);
|
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)));
|
let (time_ago, set_time_ago) = signal(user.last_active.map(|active| format_delta(Utc::now() - active)));
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
@ -126,7 +142,7 @@ pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl Into
|
|||||||
{user.user_name}
|
{user.user_name}
|
||||||
{move || time_ago.get().map(|active| view! {
|
{move || time_ago.get().map(|active| view! {
|
||||||
<span>
|
<span>
|
||||||
"(last activity: "
|
" (last activity: "
|
||||||
{active}
|
{active}
|
||||||
")"
|
")"
|
||||||
</span>
|
</span>
|
||||||
@ -189,6 +205,12 @@ pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl Into
|
|||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
async fn list_users() -> Result<Vec<PubUser>, ServerFnError> {
|
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;
|
use futures::stream::StreamExt;
|
||||||
|
|
||||||
let pool = expect_context::<SqlitePool>();
|
let pool = expect_context::<SqlitePool>();
|
||||||
@ -213,6 +235,12 @@ async fn list_users() -> Result<Vec<PubUser>, ServerFnError> {
|
|||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
async fn add_user(name: String, password: String) -> Result<(), ServerFnError> {
|
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>();
|
let pool = expect_context::<SqlitePool>();
|
||||||
|
|
||||||
crate::db::user::create_user(&pool, name, password).await?;
|
crate::db::user::create_user(&pool, name, password).await?;
|
||||||
@ -222,6 +250,15 @@ async fn add_user(name: String, password: String) -> Result<(), ServerFnError> {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn UserView() -> impl IntoView {
|
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 user_list = Resource::new(|| (), |_| async move { list_users().await });
|
||||||
|
|
||||||
let modal_ref = NodeRef::<leptos::html::Dialog>::new();
|
let modal_ref = NodeRef::<leptos::html::Dialog>::new();
|
||||||
|
|||||||
@ -4,68 +4,15 @@ use sqlx::sqlite::SqlitePool;
|
|||||||
use axum::Router;
|
use axum::Router;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||||
use tokio::{signal, task::AbortHandle};
|
use tokio::signal;
|
||||||
use tower_sessions::{Expiry, SessionManagerLayer, session_store::ExpiredDeletion};
|
|
||||||
use tower_sessions_sqlx_store::SqliteStore;
|
|
||||||
|
|
||||||
use sparse_server::app::*;
|
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> {
|
pub async fn serve_web(management_address: SocketAddrV4, _bind_address: SocketAddrV4, db: SqlitePool) -> anyhow::Result<ExitCode> {
|
||||||
let conf = get_configuration(None).unwrap();
|
let conf = get_configuration(None).unwrap();
|
||||||
let leptos_options = conf.leptos_options;
|
let leptos_options = conf.leptos_options;
|
||||||
let routes = generate_route_list(App);
|
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()
|
let compression_layer = tower_http::compression::CompressionLayer::new()
|
||||||
.gzip(true)
|
.gzip(true)
|
||||||
.deflate(true)
|
.deflate(true)
|
||||||
@ -73,7 +20,6 @@ pub async fn serve_web(management_address: SocketAddrV4, _bind_address: SocketAd
|
|||||||
.zstd(true);
|
.zstd(true);
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/ws", axum::routing::any(websocket))
|
|
||||||
.leptos_routes_with_context(
|
.leptos_routes_with_context(
|
||||||
&leptos_options,
|
&leptos_options,
|
||||||
routes,
|
routes,
|
||||||
@ -81,11 +27,15 @@ pub async fn serve_web(management_address: SocketAddrV4, _bind_address: SocketAd
|
|||||||
{
|
{
|
||||||
let leptos_options = leptos_options.clone();
|
let leptos_options = leptos_options.clone();
|
||||||
move || shell(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)
|
.with_state(leptos_options)
|
||||||
.layer(auth_layer)
|
.layer(
|
||||||
.layer(compression_layer);
|
tower::ServiceBuilder::new()
|
||||||
|
.layer(tower_http::trace::TraceLayer::new_for_http())
|
||||||
|
.layer(compression_layer)
|
||||||
|
);
|
||||||
|
|
||||||
// run our app with hyper
|
// run our app with hyper
|
||||||
// `axum::Server` is a re-export of `hyper::Server`
|
// `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);
|
tracing::info!("management interface listening on http://{}", &management_address);
|
||||||
|
|
||||||
axum::serve(management_listener, app.into_make_service())
|
axum::serve(management_listener, app.into_make_service())
|
||||||
.with_graceful_shutdown(shutdown_signal(deletion_task.abort_handle()))
|
.with_graceful_shutdown(shutdown_signal())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
deletion_task.await??;
|
|
||||||
|
|
||||||
Ok(ExitCode::SUCCESS)
|
Ok(ExitCode::SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn shutdown_signal(deletion_task_abort_handle: AbortHandle) {
|
async fn shutdown_signal() {
|
||||||
let ctrl_c = async {
|
let ctrl_c = async {
|
||||||
signal::ctrl_c()
|
signal::ctrl_c()
|
||||||
.await
|
.await
|
||||||
@ -122,11 +70,9 @@ async fn shutdown_signal(deletion_task_abort_handle: AbortHandle) {
|
|||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = ctrl_c => {
|
_ = ctrl_c => {
|
||||||
tracing::info!("Received Ctrl-C");
|
tracing::info!("Received Ctrl-C");
|
||||||
deletion_task_abort_handle.abort()
|
|
||||||
},
|
},
|
||||||
_ = terminate => {
|
_ = terminate => {
|
||||||
tracing::info!("Received terminate command");
|
tracing::info!("Received terminate command");
|
||||||
deletion_task_abort_handle.abort()
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user