feat: set up basic sessions

This commit is contained in:
Andrew Rioux 2025-01-28 03:10:43 -05:00
parent bee66a8d6c
commit bf879bb081
Signed by: andrew.rioux
GPG Key ID: 9B8BAC47C17ABB94
23 changed files with 862 additions and 106 deletions

327
Cargo.lock generated
View File

@ -26,6 +26,21 @@ dependencies = [
"memchr",
]
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
[[package]]
name = "alloc-stdlib"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
@ -74,6 +89,22 @@ version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
[[package]]
name = "async-compression"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522"
dependencies = [
"brotli",
"flate2",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
"zstd",
"zstd-safe",
]
[[package]]
name = "async-lock"
version = "3.4.0"
@ -217,6 +248,26 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-login"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5260ed0ecc8ace8e7e61a7406672faba598c8a86b8f4742fcdde0ddc979a318f"
dependencies = [
"async-trait",
"axum",
"form_urlencoded",
"serde",
"subtle",
"thiserror 1.0.69",
"tower-cookies",
"tower-layer",
"tower-service",
"tower-sessions",
"tracing",
"urlencoding",
]
[[package]]
name = "axum-server"
version = "0.7.1"
@ -311,6 +362,27 @@ dependencies = [
"generic-array",
]
[[package]]
name = "brotli"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
name = "brotli-decompressor"
version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
@ -341,6 +413,8 @@ version = "1.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
dependencies = [
"jobserver",
"libc",
"shlex",
]
@ -490,6 +564,17 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@ -520,6 +605,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
@ -623,6 +717,16 @@ dependencies = [
"zeroize",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
name = "derive-where"
version = "1.2.7"
@ -750,6 +854,16 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "flate2"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "flume"
version = "0.11.1"
@ -1396,6 +1510,15 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "jobserver"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.77"
@ -1718,6 +1841,7 @@ checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
"serde",
]
[[package]]
@ -1890,6 +2014,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
@ -2121,6 +2251,12 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
@ -2397,6 +2533,28 @@ 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"
@ -2721,6 +2879,15 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "2.2.0"
@ -2795,8 +2962,11 @@ name = "sparse-server"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"axum",
"axum-login",
"axum-server",
"cfg-if",
"chrono",
"codee",
"console_error_panic_hook",
@ -2816,10 +2986,13 @@ 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",
@ -2894,6 +3067,7 @@ dependencies = [
"sha2",
"smallvec",
"thiserror 2.0.11",
"time",
"tokio",
"tokio-stream",
"tracing",
@ -2978,6 +3152,7 @@ dependencies = [
"sqlx-core",
"stringprep",
"thiserror 2.0.11",
"time",
"tracing",
"whoami",
]
@ -3016,6 +3191,7 @@ dependencies = [
"sqlx-core",
"stringprep",
"thiserror 2.0.11",
"time",
"tracing",
"whoami",
]
@ -3040,6 +3216,7 @@ dependencies = [
"serde",
"serde_urlencoded",
"sqlx-core",
"time",
"tracing",
"url",
]
@ -3268,6 +3445,37 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "time"
version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.7.6"
@ -3304,6 +3512,7 @@ dependencies = [
"libc",
"mio",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
@ -3431,14 +3640,33 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
dependencies = [
"async-compression",
"bitflags 2.8.0",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
@ -3493,6 +3721,71 @@ 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"
@ -3680,6 +3973,12 @@ 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"
@ -4189,3 +4488,31 @@ dependencies = [
"quote",
"syn 2.0.96",
]
[[package]]
name = "zstd"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.13+zstd.1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
dependencies = [
"cc",
"pkg-config",
]

View File

@ -13,6 +13,7 @@ panic = "abort"
[profile.wasm-release]
inherits = "release"
strip = true
opt-level = 'z'
lto = true
codegen-units = 1

View File

@ -30,14 +30,6 @@ let
src = craneLib.cleanCargoSource ./.;
commonArgs = buildEnvironment // {
inherit src;
strictDeps = true;
nativeBuildInputs = buildTools.linux;
buildInputs = buildTools.all;
};
fileSetForBeaconCrate = pkgs.lib.fileset.toSource {
root = ./.;
fileset = pkgs.lib.fileset.unions [
@ -97,6 +89,14 @@ let
];
};
commonArgs = buildEnvironment // {
inherit src;
strictDeps = true;
nativeBuildInputs = buildTools.linux;
buildInputs = buildTools.all;
};
freebsdArgs = commonArgs // {
# Sigh...
# For some reason, crane and cargo don't run the build script for FreeBSD
@ -249,8 +249,11 @@ let
installPhase = ''
mkdir -p $out/bin
cp target/x86_64-unknown-linux-musl/release/sparse-server $out/bin
cp target/x86_64-unknown-linux-gnu/release/sparse-server $out/bin
'';
doCheck = false;
RUSTFLAGS = "-Ctarget-feature=+crt-static";
SPARSE_INSTALLER_LINUX = "${sparse-installer-linux}/bin/sparse-installer";
SPARSE_INSTALLER_FREEBSD =

View File

@ -3,3 +3,10 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[unstable]
build-std = ["std", "panic_abort", "core", "alloc"]
build-std-features = ["panic_immediate_abort"]
[build]
rustflags = ["--cfg=has_std"]

View File

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE users SET last_active = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "09801043d7da4a27d3388f289ef8bf040f1279bb1aee533f7ab45d375f6e0b70"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO users (user_name, password_hash) VALUES (?, \"\")",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "36691252e9640a76c9381b00ab14931aaa45f8d1cd1de4697bcd726865719d70"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "SELECT user_id, user_name, (SELECT MAX(expires) FROM sessions s WHERE s.user_id = u.user_id) as last_active FROM users u",
"query": "SELECT user_id, user_name, last_active FROM users",
"describe": {
"columns": [
{
@ -28,5 +28,5 @@
true
]
},
"hash": "ed123391fb7afe255dc30bb1006410e8537b03cc9ff00c248ccc7c34a4a8366c"
"hash": "4eeb48b1e4f85bae416b9d91b663d25b9abb6ecb4a31700b95141937c2f8f1f9"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE users SET password_hash = ? WHERE user_id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "6bccf4d930b1603d7df48cdbc605dc9095185b0fdcc5bf3613966699a9e67577"
}

View File

@ -0,0 +1,38 @@
{
"db_name": "SQLite",
"query": "SELECT * FROM users WHERE user_id = ?",
"describe": {
"columns": [
{
"name": "user_id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "user_name",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "password_hash",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "last_active",
"ordinal": 3,
"type_info": "Integer"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true
]
},
"hash": "7ca12d1edd84924ca65f597196eb618e4a313caf315a90aceaaaa253ff25947b"
}

View File

@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "INSERT INTO users (user_name, password_salt, password_hash) VALUES (?, \"\", \"\")",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "a1833e1eebd2373430b370b6fb3f2bfba2c7451759b741f4e7f5a71a49d76417"
}

View File

@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE users SET password_hash = ?, password_salt = ? WHERE user_id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "bcaa134040954a687027e3b9d3cc2d9e9c2ade4c04eee8abea4db9d15db70fce"
}

View File

@ -0,0 +1,38 @@
{
"db_name": "SQLite",
"query": "SELECT * FROM users WHERE user_name = ?",
"describe": {
"columns": [
{
"name": "user_id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "user_name",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "password_hash",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "last_active",
"ordinal": 3,
"type_info": "Integer"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true
]
},
"hash": "e0951ca9b4ff37ca9d9c8c4ea1ab618ad0dc8cdff118b6d801b568592762a29f"
}

View File

@ -9,13 +9,13 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
leptos = { version = "^0.7", features = ["nightly"] }
leptos_router = { version = "^0.7", features = ["nightly"] }
axum = { version = "^0.7", optional = true }
axum = { version = "^0.7", features = ["ws"], 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"], optional = true }
tokio = { version = "1", features = ["rt-multi-thread", "signal"], optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
tower-http = { version = "0.5", features = ["fs", "compression-br", "compression-deflate", "compression-gzip", "compression-zstd"], optional = true }
wasm-bindgen = "0.2"
thiserror = "1"
http = "1"
@ -37,14 +37,24 @@ pbkdf2 = { version = "0.12", features = ["simple", "sha2"], 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: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",
@ -60,8 +70,7 @@ ssr = [
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"leptos-use/ssr",
"axum/ws"
"leptos-use/ssr"
]
[package.metadata.leptos]

View File

@ -0,0 +1,4 @@
ALTER TABLE users DROP COLUMN password_salt;-- Add migration script here
ALTER TABLE users ADD COLUMN last_active int;
DROP TABLE sessions;

View File

@ -2,8 +2,9 @@ use leptos::prelude::*;
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
use leptos_router::{
components::{A, Route, Router, Routes},
StaticSegment,
path
};
use serde::{Serialize, Deserialize};
#[server]
pub async fn test_retrieve() -> Result<u64, ServerFnError> {
@ -20,6 +21,58 @@ pub async fn test_retrieve() -> Result<u64, ServerFnError> {
Ok(since_the_epoch)
}
#[derive(Clone, 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?;
Ok(session.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;
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);
}
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())
}
}
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
@ -42,34 +95,71 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
pub fn App() -> impl IntoView {
provide_meta_context();
let user = Resource::new(|| (), |_| async { me().await });
view! {
<Stylesheet id="leptos" href="/pkg/sparse-server.css"/>
// sets the document title
<Title text="Welcome to Leptos"/>
<Title text="Sparse Control"/>
// content for this welcome page
<Router>
<nav>
<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="#"
on:click=move |_| {
leptos::task::spawn_local(async move {
let _ = logout().await;
user.refetch();
});
}
>
"Log out"
</a>
})}
{move || user
.get()
.map(|err| err.ok())
.flatten()
.flatten()
.is_none()
.then(|| view! {
<A href="/login">"Log in"</A>
})}
</Suspense>
</nav>
<aside class="beacons">
</aside>
<Routes fallback=|| "Page not found.".into_view()>
<Route path=StaticSegment("") view=HomePage/>
<Route path=StaticSegment("/users") view=crate::users::UserView/>
<Route path=path!("users") view=crate::users::UserView />
<Route path=path!("login") view=move || view! { <LoginPage /> } />
<Route path=path!("") view=HomePage/>
</Routes>
</Router>
}
}
#[component]
fn LoginPage() -> impl IntoView {
}
/// Renders the home page of your application.
#[component]
fn HomePage() -> impl IntoView {
use leptos_use::{UseWebSocketReturn, use_websocket};
// Creates a reactive value to update the button
let count = RwSignal::new(0);
@ -91,7 +181,11 @@ fn HomePage() -> impl IntoView {
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");
@ -108,6 +202,10 @@ fn HomePage() -> impl IntoView {
send(&text_input.get());
text_input.set("".to_string());
};
} else {
let send_message = move |_| {};
}
}
view! {
<main class="main">

View File

@ -1,4 +1,5 @@
use std::path::PathBuf;
use std::{net::SocketAddrV4, path::PathBuf};
use structopt::StructOpt;
pub mod user;
@ -25,7 +26,15 @@ pub struct Options {
#[structopt()]
pub enum Command {
/// Run the web and API server
Serve {},
Serve {
/// Address to bind to for the management interface
#[structopt(default_value = "127.0.0.1:3000")]
management_address: SocketAddrV4,
/// Public address to bind to for the beacons to call back to
#[structopt(default_value = "127.0.0.1:5000")]
bind_address: SocketAddrV4,
},
/// Extract the public key and print it to standard out
ExtractPubKey {},

View File

@ -1,7 +1,7 @@
use std::process::ExitCode;
use futures_util::StreamExt;
use sqlx::{Database, query, sqlite::SqlitePool};
use sqlx::{query, sqlite::SqlitePool};
use crate::cli::UserCommand as UC;

View File

@ -1,15 +1,2 @@
#[cfg(feature = "ssr")]
pub mod user;
pub struct User {
pub user_id: i16,
pub user_name: String,
pub password_salt: String,
pub password_hash: String,
}
pub struct Sessions {
pub session_id: String,
pub user_id: i16,
pub expires: chrono::DateTime<chrono::offset::Local>,
}

View File

@ -1,29 +1,77 @@
use pbkdf2::{pbkdf2_hmac_array, password_hash::{rand_core::OsRng, SaltString}};
use sha2::Sha256;
#[derive(Clone)]
pub struct User {
pub user_id: i64,
pub user_name: String,
password_hash: String,
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;
const PASSWORD_ITERATIONS: u32 = 100_000;
impl std::fmt::Debug for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("User")
.field("user_id", &self.user_id)
.field("user_name", &self.user_name)
.field("password_hash", &"[redacted]")
.finish()
}
}
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(
&*pass,
&salt,
).map(|hash| hash.to_string())
}).await??)
}
async fn verify_password(pass: &str, hash: &str) -> Result<bool, Error> {
Ok(tokio::task::spawn_blocking({
let pass = pass.to_owned();
let hash = hash.to_owned();
move ||
PasswordHash::new(&*hash)
.map(|parsed| Pbkdf2.verify_password(
&pass.as_bytes(),
&parsed
).is_ok())
}).await??)
}
pub async fn reset_password<'a, E>(pool: E, id: i16, password: String) -> Result<(), crate::error::Error>
where
E: sqlx::SqliteExecutor<'a>
{
let salt = SaltString::generate(&mut OsRng);
let key = pbkdf2_hmac_array::<Sha256, 20>(
password.as_bytes(),
salt.as_str().as_bytes(),
PASSWORD_ITERATIONS
);
let salt_string = hex::encode(salt.as_str().as_bytes());
let password_string = hex::encode(&key[..]);
let password_string = hash_password(
password.as_bytes()
).await?;
sqlx::query!(
"UPDATE users SET password_hash = ?, password_salt = ? WHERE user_id = ?",
"UPDATE users SET password_hash = ? WHERE user_id = ?",
password_string,
salt_string,
id
)
.execute(pool)
@ -52,7 +100,7 @@ where
tracing::info!("Creating new user {}", name);
let new_id = sqlx::query!(
r#"INSERT INTO users (user_name, password_salt, password_hash) VALUES (?, "", "")"#,
r#"INSERT INTO users (user_name, password_hash) VALUES (?, "")"#,
name
)
.execute(&mut *tx)
@ -65,3 +113,68 @@ where
Ok(())
}
#[derive(Clone)]
pub struct Backend(SqlitePool);
impl Backend {
pub fn new(db: SqlitePool) -> Self {
Self(db)
}
}
#[async_trait]
impl AuthnBackend for Backend {
type User = User;
type Credentials = (String, String);
type Error = Error;
async fn authenticate(
&self,
creds: Self::Credentials
) -> Result<Option<Self::User>, Self::Error> {
let user: Option<Self::User> = sqlx::query_as!(
User,
"SELECT * FROM users WHERE user_name = ?",
creds.0
)
.fetch_optional(&self.0)
.await?;
let Some(user) = user else { return Ok(None); };
let good_hash = verify_password(
&user.password_hash,
&creds.1
).await?;
if good_hash {
let now = chrono::Utc::now().timestamp();
sqlx::query!(
"UPDATE users SET last_active = ?",
now
)
.execute(&self.0)
.await?;
Ok(Some(user))
} else {
Ok(None)
}
}
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
)
.fetch_optional(&self.0)
.await?;
Ok(user)
}
}
pub type AuthSession = axum_login::AuthSession<Backend>;

View File

@ -1,13 +1,21 @@
#[derive(Debug)]
pub enum Error {
Generic(String),
UserCreate(String),
#[cfg(feature = "ssr")]
Sqlx(sqlx::Error),
#[cfg(feature = "ssr")]
TokioJoin(tokio::task::JoinError),
#[cfg(feature = "ssr")]
Pbkdf2(pbkdf2::password_hash::errors::Error),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Generic(err) => {
write!(f, "generic error: {err}")
}
Error::UserCreate(err) => {
write!(f, "user create error: {err}")
}
@ -15,6 +23,14 @@ impl std::fmt::Display for Error {
Error::Sqlx(err) => {
write!(f, "sqlx error: {err:?}")
}
#[cfg(feature = "ssr")]
Error::TokioJoin(err) => {
write!(f, "tokio join error: {err:?}")
}
#[cfg(feature = "ssr")]
Error::Pbkdf2(err) => {
write!(f, "password hash error: {err:?}")
}
}
}
}
@ -24,14 +40,38 @@ impl std::error::Error for Error {
match self {
#[cfg(feature = "ssr")]
Error::Sqlx(err) => Some(err),
#[cfg(feature = "ssr")]
Error::TokioJoin(err) => Some(err),
_ => None,
}
}
}
impl std::str::FromStr for Error {
type Err = Self;
fn from_str(err: &str) -> Result<Self, Self::Err> {
Ok(Self::Generic(err.to_string()))
}
}
#[cfg(feature = "ssr")]
impl From<sqlx::Error> for Error {
fn from(err: sqlx::Error) -> Self {
Self::Sqlx(err)
}
}
#[cfg(feature = "ssr")]
impl From<tokio::task::JoinError> for Error {
fn from(err: tokio::task::JoinError) -> Self {
Self::TokioJoin(err)
}
}
#[cfg(feature = "ssr")]
impl From<pbkdf2::password_hash::errors::Error> for Error {
fn from(err: pbkdf2::password_hash::errors::Error) -> Self {
Self::Pbkdf2(err)
}
}

View File

@ -1,10 +1,15 @@
#[cfg(feature = "ssr")]
pub(crate) mod beacons {
#[allow(dead_code)]
pub const LINUX_BEACON: &'static [u8] = include_bytes!(std::env!("SPARSE_BEACON_LINUX"));
#[allow(dead_code)]
pub const FREEBSD_BEACON: &'static [u8] = include_bytes!(std::env!("SPARSE_BEACON_FREEBSD"));
#[allow(dead_code)]
pub const WINDOWS_BEACON: &'static [u8] = include_bytes!(std::env!("SPARSE_BEACON_WINDOWS"));
#[allow(dead_code)]
pub const LINUX_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_LINUX"));
#[allow(dead_code)]
pub const FREEBSD_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_FREEBSD"));
}
@ -78,9 +83,9 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
tracing::info!("Done running database migrations!");
match options.command.clone() {
Some(cli::Command::Serve { }) => {
Some(cli::Command::Serve { management_address, bind_address }) => {
tracing::info!("Performing requested action, acting as web server");
webserver::serve_web(options, pool).await
webserver::serve_web(management_address, bind_address, pool).await
}
Some(cli::Command::ExtractPubKey { }) => {
Ok(ExitCode::SUCCESS)
@ -89,8 +94,13 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
cli::user::handle_user_command(command, pool).await
}
None => {
use std::net::{Ipv4Addr, SocketAddrV4};
tracing::info!("Performing default action of acting as web server");
webserver::serve_web(options, pool).await
let default_management_ip = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 3000);
let default_beacon_ip = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 5000);
webserver::serve_web(default_management_ip, default_beacon_ip, pool).await
}
}
}

View File

@ -70,6 +70,7 @@ pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl Into
let UseIntervalReturn { counter, .. } = use_interval(1000);
let (time_ago, set_time_ago) = signal(user.last_active.map(|active| format_delta(Utc::now() - active)));
#[cfg(feature = "hydrate")]
Effect::watch(
move || counter.get(),
move |_, _, _| {
@ -194,7 +195,7 @@ async fn list_users() -> Result<Vec<PubUser>, ServerFnError> {
let users = sqlx::query_as!(
DbUser,
"SELECT user_id, user_name, (SELECT MAX(expires) FROM sessions s WHERE s.user_id = u.user_id) as last_active FROM users u"
"SELECT user_id, user_name, last_active FROM users"
)
.fetch(&pool)
.map(|user| user.map(|u| PubUser {

View File

@ -1,12 +1,14 @@
use std::process::ExitCode;
use std::{net::SocketAddrV4, process::ExitCode};
use sqlx::sqlite::SqlitePool;
use axum::Router;
use leptos::logging::log;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use sparse_server::app::*;
use tokio::{signal, task::AbortHandle};
use tower_sessions::{Expiry, SessionManagerLayer, session_store::ExpiredDeletion};
use tower_sessions_sqlx_store::SqliteStore;
use sparse_server::app::*;
pub async fn websocket(ws: axum::extract::ws::WebSocketUpgrade) -> axum::response::Response {
tracing::info!("Handling websocket request to /ws");
@ -14,7 +16,6 @@ pub async fn websocket(ws: axum::extract::ws::WebSocketUpgrade) -> axum::respons
}
async fn handle_websocket(mut socket: axum::extract::ws::WebSocket) {
use futures_util::StreamExt;
use tracing::info;
let mut count = 0;
@ -43,13 +44,34 @@ async fn handle_websocket(mut socket: axum::extract::ws::WebSocket) {
}
}
pub async fn serve_web(options: crate::cli::Options, 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 addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
// Generate the list of routes in your Leptos 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()
.gzip(true)
.deflate(true)
.br(true)
.zstd(true);
let app = Router::new()
.route("/ws", axum::routing::any(websocket))
.leptos_routes_with_context(
@ -61,13 +83,50 @@ pub async fn serve_web(options: crate::cli::Options, db: SqlitePool) -> anyhow::
move || shell(leptos_options.clone())
})
.fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options);
.with_state(leptos_options)
.layer(auth_layer)
.layer(compression_layer);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
tracing::info!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, app.into_make_service()).await?;
let management_listener = tokio::net::TcpListener::bind(&management_address).await?;
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()))
.await?;
deletion_task.await??;
Ok(ExitCode::SUCCESS)
}
async fn shutdown_signal(deletion_task_abort_handle: AbortHandle) {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
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()
},
}
}