Compare commits

..

24 Commits

Author SHA1 Message Date
f2a1296454 Move server setup into router_setup
All checks were successful
Push Workflows / rustfmt (push) Successful in 5s
Push Workflows / tailwind-build (push) Successful in 8s
Push Workflows / docs (push) Successful in 33s
Push Workflows / clippy (push) Successful in 31s
Push Workflows / test (push) Successful in 57s
Push Workflows / build (push) Successful in 1m25s
Push Workflows / nix-build (push) Successful in 5m17s
Remove tokio (dioxus::serve provides a runtime)
2026-06-27 18:51:44 -04:00
d52c4cbe9e Add open_signup config flag
All checks were successful
Push Workflows / rustfmt (push) Successful in 5s
Push Workflows / tailwind-build (push) Successful in 7s
Push Workflows / clippy (push) Successful in 18s
Push Workflows / docs (push) Successful in 22s
Push Workflows / test (push) Successful in 26s
Push Workflows / build (push) Successful in 49s
Push Workflows / nix-build (push) Successful in 5m11s
2026-06-27 17:33:23 -04:00
1282c2b8b5 Implement axum-login
All checks were successful
Push Workflows / rustfmt (push) Successful in 5s
Push Workflows / tailwind-build (push) Successful in 7s
Push Workflows / docs (push) Successful in 40s
Push Workflows / clippy (push) Successful in 46s
Push Workflows / test (push) Successful in 1m2s
Push Workflows / build (push) Successful in 1m54s
Push Workflows / nix-build (push) Successful in 5m20s
2026-06-27 17:25:46 -04:00
f7f4fd2813 Add password check function 2026-06-27 17:23:02 -04:00
46ce08e02f Add HashedPassword::auth_hash 2026-06-27 17:22:35 -04:00
8a049edeeb Add axum-login 2026-06-27 17:21:52 -04:00
0b7e25c792 Add database::DbConn type 2026-06-27 17:07:42 -04:00
554ae23175 Remove unnecessary error conversion 2026-06-27 17:07:27 -04:00
87aa18b7cc Allow error::Result type to use other error
Convenience since this Result will shadow std::result::Result when
imported
2026-06-27 17:06:24 -04:00
86291f1eb5 Convert deadpool::PoolError into Database error 2026-06-27 17:06:02 -04:00
f8e2dad58a Set up key-value store in server main
All checks were successful
Push Workflows / rustfmt (push) Successful in 13s
Push Workflows / tailwind-build (push) Successful in 12s
Push Workflows / docs (push) Successful in 38s
Push Workflows / clippy (push) Successful in 41s
Push Workflows / test (push) Successful in 48s
Push Workflows / build (push) Successful in 1m45s
Push Workflows / nix-build (push) Successful in 5m21s
2026-06-27 15:28:52 -04:00
2b800c2df4 Add key-value store setup function 2026-06-27 15:28:39 -04:00
9e3d534190 Add config for key-value store connection 2026-06-27 15:26:07 -04:00
d74479851f Add format_uri function 2026-06-27 15:25:52 -04:00
4ecbf6da15 Add error type for key-value store 2026-06-27 15:17:22 -04:00
674b58e290 Use wildcard match for Error variants without special status codes 2026-06-27 15:17:02 -04:00
ef9f88e72c Add fred 2026-06-27 15:16:20 -04:00
97cf3f62ad Add User models 2026-06-27 13:24:12 -04:00
3677b6adfa Add cfg_if 2026-06-27 13:23:57 -04:00
ca8c96306f Add pbkdf2
All checks were successful
Push Workflows / rustfmt (push) Successful in 5s
Push Workflows / tailwind-build (push) Successful in 8s
Push Workflows / test (push) Successful in 40s
Push Workflows / clippy (push) Successful in 40s
Push Workflows / docs (push) Successful in 48s
Push Workflows / build (push) Successful in 1m26s
Push Workflows / nix-build (push) Successful in 5m14s
2026-06-27 12:35:05 -04:00
f4f1e4b96f Move schema module to server only
All checks were successful
Push Workflows / rustfmt (push) Successful in 4s
Push Workflows / tailwind-build (push) Successful in 5s
Push Workflows / docs (push) Successful in 39s
Push Workflows / clippy (push) Successful in 42s
Push Workflows / test (push) Successful in 52s
Push Workflows / build (push) Successful in 1m26s
Push Workflows / nix-build (push) Successful in 5m3s
2026-06-26 18:10:13 -04:00
1bf5c0f2da Ignore diesel lock file 2026-06-26 18:08:52 -04:00
773d8dffd1 Create users table 2026-06-26 18:08:03 -04:00
fb3afaf31c Add chrono
Enable chrono feature for diesel
2026-06-26 17:46:50 -04:00
16 changed files with 720 additions and 63 deletions

2
.gitignore vendored
View File

@@ -16,3 +16,5 @@ config.ron
config.toml config.toml
config.yaml config.yaml
config.yml config.yml
/migrations/.diesel_lock

286
Cargo.lock generated
View File

@@ -32,6 +32,15 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arc-swap"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
dependencies = [
"rustversion",
]
[[package]] [[package]]
name = "arraydeque" name = "arraydeque"
version = "0.5.1" version = "0.5.1"
@@ -185,6 +194,25 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "axum-login"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "964ea6eb764a227baa8c3368e45c94d23b6863cc7b880c6c9e341c143c5a5ff7"
dependencies = [
"axum",
"form_urlencoded",
"serde",
"subtle",
"thiserror 2.0.18",
"tower-cookies",
"tower-layer",
"tower-service",
"tower-sessions",
"tracing",
"urlencoding",
]
[[package]] [[package]]
name = "axum-macros" name = "axum-macros"
version = "0.5.1" version = "0.5.1"
@@ -208,6 +236,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.13.0" version = "2.13.0"
@@ -256,6 +290,16 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bytes-utils"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35"
dependencies = [
"bytes",
"either",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.63" version = "1.2.63"
@@ -325,6 +369,7 @@ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits",
"serde",
"wasm-bindgen", "wasm-bindgen",
"windows-link", "windows-link",
] ]
@@ -534,6 +579,12 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "cookie-factory"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b"
[[package]] [[package]]
name = "cookie_store" name = "cookie_store"
version = "0.22.1" version = "0.22.1"
@@ -586,6 +637,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crc16"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff"
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.21" version = "0.8.21"
@@ -705,6 +762,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [ dependencies = [
"powerfmt", "powerfmt",
"serde_core",
] ]
[[package]] [[package]]
@@ -738,6 +796,7 @@ checksum = "29fe29a87fb84c631ffb3ba21798c4b1f3a964701ba78f0dce4bf8668562ec88"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"byteorder", "byteorder",
"chrono",
"diesel_derives", "diesel_derives",
"downcast-rs", "downcast-rs",
"itoa", "itoa",
@@ -1554,6 +1613,15 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "float-cmp"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@@ -1575,6 +1643,43 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fred"
version = "10.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a7b2fd0f08b23315c13b6156f971aeedb6f75fb16a29ac1872d2eabccc1490e"
dependencies = [
"arc-swap",
"async-trait",
"bytes",
"bytes-utils",
"float-cmp",
"fred-macros",
"futures",
"log",
"parking_lot",
"rand 0.8.6",
"redis-protocol",
"semver",
"socket2 0.5.10",
"tokio",
"tokio-stream",
"tokio-util",
"url",
"urlencoding",
]
[[package]]
name = "fred-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1458c6e22d36d61507034d5afecc64f105c1d39712b7ac6ec3b352c423f715cc"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.32" version = "0.3.32"
@@ -1992,7 +2097,7 @@ dependencies = [
"libc", "libc",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2 0.6.4",
"system-configuration", "system-configuration",
"tokio", "tokio",
"tower-layer", "tower-layer",
@@ -2306,18 +2411,22 @@ dependencies = [
name = "libretunes" name = "libretunes"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"axum-login",
"cfg-if",
"chrono",
"config", "config",
"diesel", "diesel",
"diesel-async", "diesel-async",
"diesel_migrations", "diesel_migrations",
"dioxus", "dioxus",
"dotenvy", "dotenvy",
"fred",
"getrandom 0.4.3", "getrandom 0.4.3",
"lucide-dioxus", "lucide-dioxus",
"pbkdf2",
"rand 0.10.1", "rand 0.10.1",
"serde", "serde",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio",
"tracing", "tracing",
] ]
@@ -2346,6 +2455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [ dependencies = [
"scopeguard", "scopeguard",
"serde",
] ]
[[package]] [[package]]
@@ -2525,6 +2635,12 @@ dependencies = [
"unicase", "unicase",
] ]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.2.1" version = "1.2.1"
@@ -2583,6 +2699,16 @@ dependencies = [
"jni-sys 0.3.1", "jni-sys 0.3.1",
] ]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.2.2" version = "0.2.2"
@@ -2702,12 +2828,34 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "password-hash"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b"
dependencies = [
"getrandom 0.4.3",
"phc",
]
[[package]] [[package]]
name = "pathdiff" name = "pathdiff"
version = "0.2.3" version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "pbkdf2"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629"
dependencies = [
"digest 0.11.3",
"hmac",
"password-hash",
"sha2 0.11.0",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@@ -2757,6 +2905,17 @@ dependencies = [
"sha2 0.10.9", "sha2 0.10.9",
] ]
[[package]]
name = "phc"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892"
dependencies = [
"base64ct",
"ctutils",
"getrandom 0.4.3",
]
[[package]] [[package]]
name = "phf" name = "phf"
version = "0.13.1" version = "0.13.1"
@@ -2914,7 +3073,7 @@ dependencies = [
"quinn-udp", "quinn-udp",
"rustc-hash 2.1.2", "rustc-hash 2.1.2",
"rustls", "rustls",
"socket2", "socket2 0.6.4",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tracing", "tracing",
@@ -2951,7 +3110,7 @@ dependencies = [
"cfg_aliases", "cfg_aliases",
"libc", "libc",
"once_cell", "once_cell",
"socket2", "socket2 0.6.4",
"tracing", "tracing",
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
@@ -2977,13 +3136,24 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.9.4" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [ dependencies = [
"rand_chacha", "rand_chacha 0.9.0",
"rand_core 0.9.5", "rand_core 0.9.5",
] ]
@@ -2998,6 +3168,16 @@ dependencies = [
"rand_core 0.10.1", "rand_core 0.10.1",
] ]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]] [[package]]
name = "rand_chacha" name = "rand_chacha"
version = "0.9.0" version = "0.9.0"
@@ -3008,6 +3188,15 @@ dependencies = [
"rand_core 0.9.5", "rand_core 0.9.5",
] ]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.17",
]
[[package]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.9.5" version = "0.9.5"
@@ -3029,6 +3218,20 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "redis-protocol"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdba59219406899220fc4cdfd17a95191ba9c9afb719b5fa5a083d63109a9f1"
dependencies = [
"bytes",
"bytes-utils",
"cookie-factory",
"crc16",
"log",
"nom",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.18" version = "0.5.18"
@@ -3473,6 +3676,16 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.4" version = "0.6.4"
@@ -3722,7 +3935,7 @@ dependencies = [
"libc", "libc",
"mio", "mio",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2 0.6.4",
"tokio-macros", "tokio-macros",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@@ -3758,7 +3971,7 @@ dependencies = [
"postgres-protocol", "postgres-protocol",
"postgres-types", "postgres-types",
"rand 0.10.1", "rand 0.10.1",
"socket2", "socket2 0.6.4",
"tokio", "tokio",
"tokio-util", "tokio-util",
"whoami", "whoami",
@@ -3906,6 +4119,22 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "tower-cookies"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36"
dependencies = [
"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.6.11" version = "0.6.11"
@@ -3945,6 +4174,43 @@ 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.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3"
dependencies = [
"async-trait",
"http",
"time",
"tokio",
"tower-cookies",
"tower-layer",
"tower-service",
"tower-sessions-core",
"tracing",
]
[[package]]
name = "tower-sessions-core"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b"
dependencies = [
"async-trait",
"base64",
"futures",
"http",
"parking_lot",
"rand 0.8.6",
"serde",
"serde_json",
"thiserror 2.0.18",
"time",
"tokio",
"tracing",
]
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.44" version = "0.1.44"
@@ -4150,6 +4416,12 @@ dependencies = [
"serde", "serde",
] ]
[[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"

View File

@@ -9,17 +9,21 @@ edition = "2024"
build = "src/build.rs" build = "src/build.rs"
[dependencies] [dependencies]
axum-login = { version = "0.18.0", optional = true }
cfg-if = "1.0.4"
chrono = { version = "0.4.45", features = ["serde"] }
config = { version = "0.15.24", optional = true } config = { version = "0.15.24", optional = true }
diesel = { version = "2.3.10", optional = true } diesel = { version = "2.3.10", optional = true, features = ["chrono"] }
diesel-async = { version = "0.9.1", optional = true, features = ["postgres", "deadpool", "migrations"] } diesel-async = { version = "0.9.1", optional = true, features = ["postgres", "deadpool", "migrations"] }
diesel_migrations = { version = "2.3.2", optional = true } diesel_migrations = { version = "2.3.2", optional = true }
dioxus = { version = "0.7.9", features = ["router", "fullstack"] } dioxus = { version = "0.7.9", features = ["router", "fullstack"] }
dotenvy = { version = "0.15.7", optional = true } dotenvy = { version = "0.15.7", optional = true }
fred = { version = "10.1.0", optional = true }
lucide-dioxus = { version = "3.11.0", features = ["notifications"] } lucide-dioxus = { version = "3.11.0", features = ["notifications"] }
pbkdf2 = { version = "0.13.0", optional = true, features = ["getrandom", "phc"] }
rand = "0.10.1" rand = "0.10.1"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
thiserror = "2.0.18" thiserror = "2.0.18"
tokio = { version = "1.52.3", optional = true, features = ["rt-multi-thread"] }
tracing = "0.1.44" tracing = "0.1.44"
[features] [features]
@@ -27,12 +31,14 @@ default = ["web"]
web = ["dioxus/web"] web = ["dioxus/web"]
server = [ server = [
"dioxus/server", "dioxus/server",
"dep:axum-login",
"dep:config", "dep:config",
"dep:diesel", "dep:diesel",
"dep:diesel-async", "dep:diesel-async",
"dep:diesel_migrations", "dep:diesel_migrations",
"dep:dotenvy", "dep:dotenvy",
"dep:tokio", "dep:fred",
"dep:pbkdf2",
] ]
# Disabled until supported # Disabled until supported

View File

@@ -0,0 +1,2 @@
DROP INDEX users_username_idx;
DROP TABLE users;

View File

@@ -0,0 +1,8 @@
CREATE TABLE users (
id INTEGER PRIMARY KEY UNIQUE NOT NULL GENERATED ALWAYS AS IDENTITY,
username VARCHAR UNIQUE NOT NULL,
hashed_password VARCHAR NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX users_username_idx ON users(username);

View File

@@ -3,9 +3,11 @@ pub mod app;
pub mod components; pub mod components;
pub mod models; pub mod models;
pub mod pages; pub mod pages;
pub mod schema;
pub mod util; pub mod util;
#[cfg(feature = "server")]
pub mod schema;
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub mod server; pub mod server;
@@ -26,9 +28,8 @@ fn main() {
fn main() -> std::process::ExitCode { fn main() -> std::process::ExitCode {
tracing_setup(); tracing_setup();
if let Err(e) = server::main() { let Err(e) = server::main();
tracing::error!("Server main failed:\n{e}"); tracing::error!("Server main failed:\n{e}");
}
std::process::ExitCode::FAILURE std::process::ExitCode::FAILURE
} }

View File

@@ -1 +1 @@
pub mod user;

144
src/models/user.rs Normal file
View File

@@ -0,0 +1,144 @@
//! Various user types. Some types marked server-only to help prevent
//! leaking passwords to the frontend
/// Standard informational user type, contains no password information
#[derive(Clone, Debug)]
#[cfg_attr(feature = "server", derive(Queryable, Selectable, Identifiable))]
#[cfg_attr(feature = "server", diesel(table_name = crate::schema::users,
check_for_backend(diesel::pg::Pg)))]
pub struct User {
pub id: i32,
pub username: String,
pub created_at: chrono::DateTime<chrono::Local>,
}
/// Plaintext user credentials, used for login/signup form
pub struct UserCredentials {
pub username: String,
pub password: String,
}
cfg_if::cfg_if! {
if #[cfg(feature = "server")] {
use diesel::{
deserialize::{FromSql, FromSqlRow},
expression::AsExpression,
prelude::*,
serialize::ToSql,
sql_types,
};
use pbkdf2::{
PasswordHasher, PasswordVerifier, Pbkdf2, password_hash::Error::PasswordInvalid,
phc::PasswordHash,
};
use crate::util::error::{Error, Result};
/// Newtype for a `String`-represented hashed password
#[derive(Clone, Debug, AsExpression, FromSqlRow)]
#[diesel(sql_type = sql_types::Text)]
pub struct HashedPassword(String);
impl HashedPassword {
/// Check a password attempt against this hashed password
///
/// # Returns
///
/// `Ok(true)` for a correct password
/// `Ok(false)` for an incorrect password
/// `Err` for a hashing error
pub fn check(&self, password_attempt: String) -> Result<bool> {
let pw_hash = PasswordHash::new(&self.0)
.map_err(|e| Error::message_here(format!("Error parsing `HashedPassword`: {e}")))?;
match Pbkdf2::default().verify_password(password_attempt.as_bytes(), &pw_hash) {
Ok(()) => Ok(true),
Err(PasswordInvalid) => Ok(false),
Err(e) => Err(Error::message_here(format!(
"Error comparing password attempt against hash: {e}"
))),
}
}
/// Returns the "session auth hash" for `axum-login`, just the hashed password as bytes
pub fn auth_hash(&self) -> &[u8] {
self.0.as_bytes()
}
}
impl<DB> FromSql<diesel::sql_types::Text, DB> for HashedPassword
where
DB: diesel::backend::Backend,
String: FromSql<sql_types::Text, DB>,
{
fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result<Self> {
Ok(Self(String::from_sql(bytes)?))
}
}
impl<DB> ToSql<diesel::sql_types::Text, DB> for HashedPassword
where
DB: diesel::backend::Backend,
String: ToSql<sql_types::Text, DB>,
{
fn to_sql<'b>(
&'b self,
out: &mut diesel::serialize::Output<'b, '_, DB>,
) -> diesel::serialize::Result {
self.0.to_sql(out)
}
}
/// User as it appears in the database, with hashed password
#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
#[diesel(table_name = crate::schema::users, check_for_backend(diesel::pg::Pg))]
pub struct DbUser {
pub id: i32,
pub username: String,
pub hashed_password: HashedPassword,
pub created_at: chrono::DateTime<chrono::Local>,
}
impl From<DbUser> for User {
fn from(db_user: DbUser) -> Self {
User {
id: db_user.id,
username: db_user.username,
created_at: db_user.created_at,
}
}
}
/// User credentials with hashed password
#[derive(Clone, Debug, Insertable, Queryable, Selectable)]
#[diesel(table_name = crate::schema::users, check_for_backend(diesel::pg::Pg))]
pub struct HashedUserCredentials {
username: String,
hashed_password: HashedPassword,
}
impl From<DbUser> for HashedUserCredentials {
fn from(db_user: DbUser) -> Self {
HashedUserCredentials {
username: db_user.username,
hashed_password: db_user.hashed_password,
}
}
}
impl UserCredentials {
/// Attempt to convert into `HashedUserCredentials` by hashing the password. Yields a PBKDF2
/// error on failure.
pub fn try_hash(self) -> Result<HashedUserCredentials, pbkdf2::password_hash::Error> {
let hashed_password = Pbkdf2::default().hash_password(self.password.as_bytes())?;
Ok(HashedUserCredentials {
username: self.username,
hashed_password: HashedPassword(hashed_password.to_string()),
})
}
}
}
}

View File

@@ -1 +1,10 @@
// @generated automatically by Diesel CLI. // @generated automatically by Diesel CLI.
diesel::table! {
users (id) {
id -> Int4,
username -> Varchar,
hashed_password -> Varchar,
created_at -> Timestamptz,
}
}

91
src/server/auth.rs Normal file
View File

@@ -0,0 +1,91 @@
use axum_login::{AuthUser, AuthnBackend, UserId};
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use crate::models::user::{DbUser, UserCredentials};
use crate::server::database::{DbConn, DbPool};
use crate::util::error::{Contextualize, Error, Result};
impl AuthUser for DbUser {
type Id = i32;
fn id(&self) -> Self::Id {
self.id
}
fn session_auth_hash(&self) -> &[u8] {
self.hashed_password.auth_hash()
}
}
#[derive(Clone)]
pub struct AuthBackend {
pub db_pool: DbPool,
}
impl AuthnBackend for AuthBackend {
type User = DbUser;
type Credentials = UserCredentials;
type Error = Error;
async fn authenticate(
&self,
attempt_creds: Self::Credentials,
) -> Result<Option<Self::User>, Self::Error> {
let mut db_conn = self
.db_pool
.get()
.await
.err_context("Failed to get database pool connection")?;
let user = get_user_by_username(&mut db_conn, attempt_creds.username)
.await
.err_context("Error fetching user for authentication check")?;
let Some(user) = user else { return Ok(None) };
let password_result = user
.hashed_password
.check(attempt_creds.password)
.err_context("Error checking user password attempt")?;
if password_result {
Ok(Some(user))
} else {
Ok(None)
}
}
async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
let mut db_conn = self
.db_pool
.get()
.await
.err_context("Failed to get database pool connection")?;
get_user_by_id(&mut db_conn, *user_id)
.await
.err_context("Failed fetching user for session")
}
}
pub async fn get_user_by_id(db_conn: &mut DbConn, id: i32) -> Result<Option<DbUser>> {
crate::schema::users::table
.find(id)
.first(db_conn)
.await
.optional()
.err_context("Error fetching user from database by id")
}
pub async fn get_user_by_username(
db_conn: &mut DbConn,
username: String,
) -> Result<Option<DbUser>> {
crate::schema::users::table
.filter(crate::schema::users::username.eq(username))
.first(db_conn)
.await
.optional()
.err_context("Error fetching user from database by username")
}

View File

@@ -1,5 +1,44 @@
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct AuthConfig {
pub open_signup: bool,
}
/// Build a connection URI from parts
fn format_uri(
scheme: &str,
username: &Option<String>,
password: &Option<String>,
host: &str,
port: &Option<u16>,
path: &Option<String>,
) -> String {
let mut url = format!("{scheme}://");
if let Some(username) = username {
url.push_str(username);
if let Some(password) = password {
url.push_str(&format!(":{password}"));
}
url.push('@');
}
url.push_str(host);
if let Some(port) = port {
url.push_str(&format!(":{port}"));
}
if let Some(path) = path {
url.push_str(&format!("/{path}"));
}
url
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct DatabaseConfig { pub struct DatabaseConfig {
#[serde(flatten)] #[serde(flatten)]
@@ -39,39 +78,70 @@ impl DatabaseConnectionConfig {
database, database,
username, username,
password, password,
} => { } => format_uri("postgres", username, password, host, port, database),
let mut url = "postgres://".to_string();
if let Some(username) = username {
url.push_str(username);
if let Some(password) = password {
url.push_str(&format!(":{password}"));
}
url.push('@');
}
url.push_str(host);
if let Some(port) = port {
url.push_str(&format!(":{port}"));
}
if let Some(database) = database {
url.push_str(&format!("/{database}"));
}
url
}
} }
} }
} }
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum KeyValStoreConnectionConfig {
FromUrl {
url: String,
},
FromParts {
scheme: Option<String>,
host: String,
port: Option<u16>,
database: Option<String>,
username: Option<String>,
password: Option<String>,
},
}
impl KeyValStoreConnectionConfig {
/// Convert this configuration into the Redis connection URI
pub fn as_uri(&self) -> String {
match self {
Self::FromUrl { url } => url.clone(),
Self::FromParts {
scheme,
host,
port,
database,
username,
password,
} => format_uri(
scheme.as_deref().unwrap_or("redis"),
username,
password,
host,
port,
database,
),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct KeyValStoreConfig {
#[serde(flatten)]
connection: KeyValStoreConnectionConfig,
}
impl KeyValStoreConfig {
/// Get the configured database connection URI
pub fn connection_uri(&self) -> String {
self.connection.as_uri()
}
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
/// Top-level application configuration /// Top-level application configuration
pub struct Config { pub struct Config {
pub auth: AuthConfig,
pub database: DatabaseConfig, pub database: DatabaseConfig,
pub key_val_store: KeyValStoreConfig,
} }
/// Parse configuration from the expected files and environment variables /// Parse configuration from the expected files and environment variables
@@ -82,6 +152,7 @@ pub fn load_config() -> Result<Config, config::ConfigError> {
config::Config::builder() config::Config::builder()
.set_default("server.port", 8080)? .set_default("server.port", 8080)?
.set_default("auth.open_signup", false)?
.add_source(File::with_name(&format!("/etc/{pkg_name}/config")).required(false)) .add_source(File::with_name(&format!("/etc/{pkg_name}/config")).required(false))
.add_source(File::with_name(&format!("/etc/{pkg_name}")).required(false)) .add_source(File::with_name(&format!("/etc/{pkg_name}")).required(false))
.add_source(File::with_name("config").required(false)) .add_source(File::with_name("config").required(false))

View File

@@ -9,6 +9,7 @@ use crate::util::error::{Contextualize, Error, ErrorType};
pub const DB_MIGRATIONS: EmbeddedMigrations = embed_migrations!(); pub const DB_MIGRATIONS: EmbeddedMigrations = embed_migrations!();
pub type DbPool = Pool<AsyncPgConnection>; pub type DbPool = Pool<AsyncPgConnection>;
pub type DbConn = AsyncPgConnection;
/// Connect to the database using the given URI, and perform migrations /// Connect to the database using the given URI, and perform migrations
pub async fn setup<S: Into<String>>(database_uri: S) -> Result<DbPool, Error> { pub async fn setup<S: Into<String>>(database_uri: S) -> Result<DbPool, Error> {
@@ -26,7 +27,6 @@ pub async fn setup<S: Into<String>>(database_uri: S) -> Result<DbPool, Error> {
let migration_conn = pool let migration_conn = pool
.get() .get()
.await .await
.map_err(|e| ErrorType::Database(e.to_string()))
.err_context("Failed to get connection to database")?; .err_context("Failed to get connection to database")?;
tracing::debug!("Running migrations..."); tracing::debug!("Running migrations...");

View File

@@ -0,0 +1,29 @@
use fred::prelude::*;
use crate::util::error::{Contextualize, Error, ErrorType};
const KEY_VAL_POOL_SIZE: usize = 4;
pub type KeyValPool = Pool;
pub async fn setup(connection_uri: &str) -> Result<KeyValPool, Error> {
let config = Config::from_url(connection_uri)
.map_err(|e| ErrorType::KeyValStore(e.to_string()))
.err_context("Error creating key-value store config")?;
let pool = Builder::from_config(config)
.build_pool(KEY_VAL_POOL_SIZE)
// At time of writing the only error that could occur here is if config is not provided.
// Since we're building a pool `from_config`, this shouldn't be possible
.map_err(|e| ErrorType::KeyValStore(e.to_string()))
.err_context("Error creating pool for key-value store")?;
tracing::debug!("Establishing connection to key-value store...");
pool.init()
.await
.map_err(|e| ErrorType::KeyValStore(e.to_string()))
.err_context("Error connecting to key-value store")?;
Ok(pool)
}

View File

@@ -1,35 +1,33 @@
use tokio::runtime::Runtime; use dioxus::fullstack::axum::Router;
use crate::App; use crate::App;
use crate::server::{config, database}; use crate::server::{config, database, key_val_store};
use crate::util::error::{Contextualize, Error, Result}; use crate::util::error::{Contextualize, Error, Result};
pub fn main() -> Result<()> { pub fn main() -> Result<std::convert::Infallible> {
if let Err(e) = dotenvy::dotenv() { if let Err(e) = dotenvy::dotenv() {
tracing::warn!("Error reading .env: {e}"); tracing::warn!("Error reading .env: {e}");
} }
// `Ok(...?)` is because `dioxus::serve` expects an `anyhow::Result`
dioxus::serve(async move || Ok(router_setup().await?));
}
/// Set up the axum Router
async fn router_setup() -> Result<Router> {
tracing::debug!("Loading configuration..."); tracing::debug!("Loading configuration...");
let config = config::load_config() let config = config::load_config()
.map_err(|e| Error::message_here(e.to_string())) .map_err(|e| Error::message_here(e.to_string()))
.err_context("Failed to load config")?; .err_context("Failed to load config")?;
// `dioxus::launch` creates its own runtime, and starting a runtime inside of a runtime isn't let _db_pool = database::setup(config.database.connection_uri())
// allowed. Therefore, this function can't be made async, and we must manually create a runtime .await
// for any async setup tasks .err_context("Failed database setup")?;
tracing::debug!("Starting setup runtime...");
let setup_rt = Runtime::new()
.map_err(|e| Error::message_here(e.to_string()))
.err_context("Failed to create tokio runtime for server setup")?;
let _db_pool = setup_rt.block_on(async { let _key_val_pool = key_val_store::setup(&config.key_val_store.connection_uri())
database::setup(config.database.connection_uri()) .await
.await .err_context("Failed key-value store setup")?;
.err_context("Failed database setup")
})?;
tracing::info!("Setup complete, launching web server..."); tracing::info!("Setup complete, returning Router...");
dioxus::launch(App); Ok(dioxus::server::router(App))
Err(Error::message_here("Web server exited"))
} }

View File

@@ -1,5 +1,7 @@
pub mod auth;
pub mod config; pub mod config;
pub mod database; pub mod database;
pub mod key_val_store;
pub mod main; pub mod main;
pub use main::main; pub use main::main;

View File

@@ -43,7 +43,7 @@ impl fmt::Display for ErrorLocation {
} }
} }
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, Clone, Deserialize, Serialize, thiserror::Error)] #[derive(Debug, Clone, Deserialize, Serialize, thiserror::Error)]
pub struct Error { pub struct Error {
@@ -242,9 +242,8 @@ impl dioxus_fullstack::AsStatusCode for Error {
ErrorType::Database(msg) if *msg == (diesel::result::Error::NotFound).to_string() => { ErrorType::Database(msg) if *msg == (diesel::result::Error::NotFound).to_string() => {
StatusCode::NOT_FOUND StatusCode::NOT_FOUND
} }
ErrorType::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
ErrorType::Error(_) => StatusCode::INTERNAL_SERVER_ERROR,
ErrorType::ServerFnError(e) => e.as_status_code(), ErrorType::ServerFnError(e) => e.as_status_code(),
_ => StatusCode::INTERNAL_SERVER_ERROR,
} }
} }
} }
@@ -293,6 +292,11 @@ pub enum ErrorType {
#[error("Server function error: {0}")] #[error("Server function error: {0}")]
ServerFnError(ServerFnError), ServerFnError(ServerFnError),
// Using string to represent Fred errors, because Fred's Error type is not `Serialize`,
// and Fred is only available on the server
#[error("Key-value store error: {0}")]
KeyValStore(String),
} }
impl From<ErrorType> for Error { impl From<ErrorType> for Error {
@@ -309,3 +313,21 @@ impl From<diesel::result::Error> for Error {
Error::new_here(ErrorType::Database(format!("{err}"))) Error::new_here(ErrorType::Database(format!("{err}")))
} }
} }
// This would capture any `deapool::PoolError` and treat it as a database error
// but we're only using `deadpool` for our database, so it's fine
#[cfg(feature = "server")]
impl From<diesel_async::pooled_connection::deadpool::PoolError> for Error {
#[track_caller]
fn from(err: diesel_async::pooled_connection::deadpool::PoolError) -> Self {
Error::new_here(ErrorType::Database(format!("{err}")))
}
}
#[cfg(feature = "server")]
impl From<fred::error::Error> for Error {
#[track_caller]
fn from(err: fred::error::Error) -> Self {
Error::new_here(ErrorType::KeyValStore(format!("{err}")))
}
}