Compare commits
63 Commits
f4f1e4b96f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
d5eba9620c
|
|||
|
39afdd83a7
|
|||
|
34b887730a
|
|||
|
f2dc4c908e
|
|||
|
219f1f5e7d
|
|||
|
102387fdad
|
|||
|
87bd1b50a9
|
|||
|
c8cd4b95dc
|
|||
|
8348277a87
|
|||
|
e7c43e7a47
|
|||
|
4bacbfd1ab
|
|||
|
fa9ea95657
|
|||
|
3ec07c0469
|
|||
|
f41f4f8d80
|
|||
|
c9b7bbfece
|
|||
|
9de0b13ee8
|
|||
|
8b00db60eb
|
|||
|
69eb3a5e80
|
|||
|
7133e53536
|
|||
|
ebfe18aca2
|
|||
|
843572b487
|
|||
|
2cbc6aeaff
|
|||
|
f84e51d13a
|
|||
|
4e03be050a
|
|||
|
32699fcf9b
|
|||
|
b0d5c45caf
|
|||
|
280d63ed71
|
|||
|
8da7f1ab66
|
|||
|
3527892cfd
|
|||
|
4466396259
|
|||
|
142182af74
|
|||
|
0463714882
|
|||
|
62a7103423
|
|||
|
d028636e43
|
|||
|
7ca7056fac
|
|||
|
ab90cd81f9
|
|||
|
04ca8abce9
|
|||
|
08fc3995e5
|
|||
|
4b2fb25c6d
|
|||
|
17ea0ee46a
|
|||
|
1b5a5125a7
|
|||
|
978c9c4202
|
|||
|
7fc0513efc
|
|||
|
f2a1296454
|
|||
|
d52c4cbe9e
|
|||
|
1282c2b8b5
|
|||
|
f7f4fd2813
|
|||
|
46ce08e02f
|
|||
|
8a049edeeb
|
|||
|
0b7e25c792
|
|||
|
554ae23175
|
|||
|
87aa18b7cc
|
|||
|
86291f1eb5
|
|||
|
f8e2dad58a
|
|||
|
2b800c2df4
|
|||
|
9e3d534190
|
|||
|
d74479851f
|
|||
|
4ecbf6da15
|
|||
|
674b58e290
|
|||
|
ef9f88e72c
|
|||
|
97cf3f62ad
|
|||
|
3677b6adfa
|
|||
|
ca8c96306f
|
350
Cargo.lock
generated
350
Cargo.lock
generated
@@ -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"
|
||||||
@@ -535,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"
|
||||||
@@ -587,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"
|
||||||
@@ -706,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]]
|
||||||
@@ -1039,7 +1096,7 @@ dependencies = [
|
|||||||
"tokio-tungstenite 0.28.0",
|
"tokio-tungstenite 0.28.0",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http 0.6.11",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tungstenite 0.27.0",
|
"tungstenite 0.27.0",
|
||||||
@@ -1317,7 +1374,7 @@ dependencies = [
|
|||||||
"tokio-tungstenite 0.28.0",
|
"tokio-tungstenite 0.28.0",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http 0.6.11",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-futures",
|
"tracing-futures",
|
||||||
"url",
|
"url",
|
||||||
@@ -1556,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"
|
||||||
@@ -1577,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"
|
||||||
@@ -1994,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",
|
||||||
@@ -2308,19 +2411,25 @@ dependencies = [
|
|||||||
name = "libretunes"
|
name = "libretunes"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"axum-login",
|
||||||
|
"cfg-if",
|
||||||
"chrono",
|
"chrono",
|
||||||
"config",
|
"config",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel-async",
|
"diesel-async",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"dioxus",
|
"dioxus",
|
||||||
|
"dioxus-html",
|
||||||
"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",
|
"tower-http 0.7.0",
|
||||||
|
"tower-sessions-redis-store",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2349,6 +2458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2528,6 +2638,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"
|
||||||
@@ -2586,6 +2702,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"
|
||||||
@@ -2705,12 +2831,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"
|
||||||
@@ -2760,6 +2908,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"
|
||||||
@@ -2917,7 +3076,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",
|
||||||
@@ -2954,7 +3113,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",
|
||||||
]
|
]
|
||||||
@@ -2980,13 +3139,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",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3001,6 +3171,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"
|
||||||
@@ -3011,6 +3191,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"
|
||||||
@@ -3032,6 +3221,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"
|
||||||
@@ -3092,7 +3295,7 @@ dependencies = [
|
|||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http 0.6.11",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -3116,6 +3319,25 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmp"
|
||||||
|
version = "0.8.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmp-serde"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
|
||||||
|
dependencies = [
|
||||||
|
"rmp",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ron"
|
name = "ron"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -3476,6 +3698,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"
|
||||||
@@ -3725,7 +3957,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",
|
||||||
]
|
]
|
||||||
@@ -3761,7 +3993,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",
|
||||||
@@ -3909,6 +4141,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"
|
||||||
@@ -3936,6 +4184,31 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"http-range-header",
|
||||||
|
"httpdate",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-layer"
|
name = "tower-layer"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@@ -3948,6 +4221,57 @@ 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]]
|
||||||
|
name = "tower-sessions-redis-store"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e15b774f3d46625a27a8ac1238ecd73c8bd50013244e2de004026e161aad728"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"fred",
|
||||||
|
"rmp-serde",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"time",
|
||||||
|
"tower-sessions-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.44"
|
version = "0.1.44"
|
||||||
@@ -4153,6 +4477,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"
|
||||||
|
|||||||
16
Cargo.toml
16
Cargo.toml
@@ -9,18 +9,24 @@ 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"] }
|
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, features = ["chrono"] }
|
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"] }
|
||||||
|
dioxus-html = "0.7.9"
|
||||||
dotenvy = { version = "0.15.7", optional = true }
|
dotenvy = { version = "0.15.7", optional = true }
|
||||||
lucide-dioxus = { version = "3.11.0", features = ["notifications"] }
|
fred = { version = "10.1.0", optional = true }
|
||||||
|
lucide-dioxus = { version = "3.11.0", features = ["notifications", "account"] }
|
||||||
|
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"] }
|
tower-http = { version = "0.7.0", optional = true, features = ["fs"] }
|
||||||
|
tower-sessions-redis-store = { version = "0.16.0", optional = true }
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
@@ -28,12 +34,16 @@ 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",
|
||||||
|
"dep:tower-http",
|
||||||
|
"dep:tower-sessions-redis-store",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Disabled until supported
|
# Disabled until supported
|
||||||
|
|||||||
BIN
assets/favicon.ico
Normal file
BIN
assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
1
assets/logo.svg
Normal file
1
assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg viewBox="0 0 254 254" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><clipPath clipPathUnits="userSpaceOnUse" id="clippath"><path fill="#4032a8" d="m 9.028871e-5,480.0001 v 0 C 9.028871e-5,214.90342 214.90341,1.0022047e-4 480.00009,1.0022047e-4 v 0 c 127.30393,0 249.39377,50.57128477953 339.41122,140.58874977953 90.01752,90.01746 140.58875,212.10733 140.58875,339.41125 v 0 c 0,265.09665 -214.90332,479.99997 -479.99997,479.99997 v 0 C 214.90341,960.00007 9.028871e-5,745.09675 9.028871e-5,480.0001 Z" fill-rule="evenodd" style="display:inline;fill:#4032a8;fill-opacity:.495496"/></clipPath><g><g style="fill:none;stroke:none;stroke-linecap:square;stroke-miterlimit:10" transform="matrix(0.26458333,0,0,0.26458333,0,4.5672766)"><g clip-path="url(#p.0)" transform="translate(0,-17.262248)"><path fill="#4032a8" d="m 9.028871e-5,480.0001 v 0 C 9.028871e-5,214.90342 214.90341,1e-4 480.00009,1e-4 v 0 c 127.30393,0 249.39377,50.571285 339.41122,140.58875 90.01752,90.01746 140.58875,212.10733 140.58875,339.41125 v 0 c 0,265.09665 -214.90332,479.99997 -479.99997,479.99997 v 0 C 214.90341,960.00007 9.028871e-5,745.09675 9.028871e-5,480.0001 Z" fill-rule="evenodd"/><path stroke="#ffffff" stroke-width="4" stroke-linejoin="round" stroke-linecap="butt" d="m 60.00009,480.0001 v 0 c 0,-231.9596 188.0404,-420 420,-420 v 0 c 111.3909,0 218.21957,44.24988 296.98483,123.01516 78.76532,78.76527 123.01514,185.59392 123.01514,296.98486 v 0 c 0,231.95956 -188.0404,419.99997 -419.99997,419.99997 v 0 c -231.9596,0 -420,-188.0404 -420,-419.99997 z" fill-rule="evenodd" style="stroke-width:10.6666668;stroke-dasharray:none"/><path stroke="#ffffff" stroke-width="4" stroke-linejoin="round" stroke-linecap="butt" d="m 120.00009,480.0001 v 0 c 0,-198.82251 161.17749,-360 360,-360 v 0 c 95.47794,0 187.04532,37.92846 254.55844,105.44157 67.51312,67.51309 105.44153,159.0805 105.44153,254.55844 v 0 c 0,198.82248 -161.17749,359.99997 -359.99997,359.99997 v 0 c -198.82251,0 -360,-161.17749 -360,-359.99997 z" fill-rule="evenodd" style="stroke-width:10.6666668;stroke-dasharray:none"/><path fill="#ffffff" d="m 319.01666,480.00092 v 0 c 0,-88.90915 72.0751,-160.98425 160.98425,-160.98425 v 0 c 42.69568,0 83.64264,16.96078 113.83307,47.15121 30.19037,30.1904 47.15118,71.13736 47.15118,113.83304 v 0 c 0,88.90918 -72.07507,160.98425 -160.98425,160.98425 v 0 c -88.90915,0 -160.98425,-72.07507 -160.98425,-160.98425 z" fill-rule="evenodd" style="display:inline"/><path stroke="#ffffff" stroke-width="4" stroke-linejoin="round" stroke-linecap="butt" d="m 180.00009,480.0001 v 0 c 0,-165.68542 134.31458,-300 300,-300 v 0 c 79.56497,0 155.87112,31.60704 212.13205,87.86795 56.26092,56.26092 87.86792,132.56711 87.86792,212.13205 v 0 c 0,165.6854 -134.31458,299.99997 -299.99997,299.99997 v 0 c -165.68542,0 -300,-134.31458 -300,-299.99997 z" fill-rule="evenodd" style="stroke-width:10.6666668;stroke-dasharray:none"/><path fill="#ffffff" d="M 427.60658,-273.65095 504.05265,93.386828 478.34507,135.07715 570.21183,450.80446 544.78065,501.65549 662.47712,1028.614 477.52388,620.42282 508.81779,566.37301 393.66837,305.06321 429.7686,227.61648 308.24258,-46.542095 Z" fill-rule="evenodd" style="stroke-width:1.16873" clip-path="url(#clippath)"/></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
94
src/api/auth.rs
Normal file
94
src/api/auth.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
use crate::models::user::{User, UserCredentials};
|
||||||
|
use crate::util::error::Result;
|
||||||
|
|
||||||
|
cfg_if::cfg_if! {
|
||||||
|
if #[cfg(feature = "server")] {
|
||||||
|
|
||||||
|
use dioxus::server::axum::Extension;
|
||||||
|
|
||||||
|
use crate::server::{auth::{AuthSession, create_user}, config::Config, database::DbPool};
|
||||||
|
use crate::util::error::{AuthError, Contextualize, Error, ErrorType};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/api/v1/auth/signup", mut auth: Extension<AuthSession>, db_pool: Extension<DbPool>, config: Extension<Config>)]
|
||||||
|
pub async fn signup(credentials: UserCredentials) -> Result<User> {
|
||||||
|
if !config.auth.open_signup {
|
||||||
|
return Err(Error::message_here("Signup is disabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow signup when already logged in
|
||||||
|
if auth.user.is_some() {
|
||||||
|
return Err(Error::message_here("Log out before creating an account"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let hashed_creds = credentials
|
||||||
|
.try_hash()
|
||||||
|
.map_err(|e| Error::message_here(e.to_string()))
|
||||||
|
.err_context("Error hashing new user credentials")?;
|
||||||
|
|
||||||
|
let mut db_conn = db_pool
|
||||||
|
.get()
|
||||||
|
.await
|
||||||
|
.err_context("Failed to get database pool connection")?;
|
||||||
|
|
||||||
|
let new_user = create_user(&mut db_conn, &hashed_creds)
|
||||||
|
.await
|
||||||
|
.err_context("Error creating user")?;
|
||||||
|
|
||||||
|
// Don't return this to the client, logging in immediately isn't strictly necessary
|
||||||
|
if let Err(e) = auth.login(&new_user).await {
|
||||||
|
tracing::warn!("Failed to log in user after creating: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(new_user.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/api/v1/auth/login", mut auth: Extension<AuthSession>)]
|
||||||
|
pub async fn login(credentials: UserCredentials) -> Result<User> {
|
||||||
|
let db_user = match auth.authenticate(credentials).await {
|
||||||
|
Ok(Some(db_user)) => Ok(db_user),
|
||||||
|
Ok(None) => Err(Error::new_here(ErrorType::Auth(
|
||||||
|
AuthError::InvalidCredentials,
|
||||||
|
))),
|
||||||
|
Err(axum_login::Error::Session(e)) => Err(Error::new_here(ErrorType::Auth(
|
||||||
|
AuthError::Error(format!("Session error: {e}")),
|
||||||
|
))),
|
||||||
|
Err(axum_login::Error::Backend(e)) => Err(e),
|
||||||
|
}
|
||||||
|
.err_context("Error authenticating")?;
|
||||||
|
|
||||||
|
auth.login(&db_user)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::new_here(ErrorType::Auth(AuthError::Error(e.to_string()))))
|
||||||
|
.err_context("Error logging in")?;
|
||||||
|
|
||||||
|
Ok(db_user.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/api/v1/auth/logout", mut auth: Extension<AuthSession>)]
|
||||||
|
pub async fn logout() -> Result<()> {
|
||||||
|
match auth.logout().await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(axum_login::Error::Session(e)) => Err(Error::new_here(ErrorType::Auth(
|
||||||
|
AuthError::Error(format!("Session error: {e}")),
|
||||||
|
))),
|
||||||
|
Err(axum_login::Error::Backend(e)) => Err(e),
|
||||||
|
}
|
||||||
|
.err_context("Error logging out")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve the currently logged-in user, or `None` if unauthenticated
|
||||||
|
#[get("/api/v1/auth/user", auth: Extension<AuthSession>)]
|
||||||
|
pub async fn get_user() -> Result<Option<User>> {
|
||||||
|
Ok(auth.user.clone().map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if open signup is enabled
|
||||||
|
#[get("/api/v1/auth/open-signup", config: Extension<Config>)]
|
||||||
|
pub async fn open_signup() -> Result<bool> {
|
||||||
|
Ok(config.auth.open_signup)
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
|
pub mod auth;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
pub const LOGO_SVG: Asset = asset!("/assets/logo.svg");
|
||||||
|
pub const LOGO_ICO: Asset = asset!("/assets/favicon.ico");
|
||||||
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
|
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
|
||||||
|
|
||||||
#[derive(Debug, Clone, Routable, PartialEq)]
|
#[derive(Debug, Clone, Routable, PartialEq)]
|
||||||
@@ -12,6 +14,7 @@ enum Route {
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn App() -> Element {
|
pub fn App() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
|
document::Link { rel: "icon", href: LOGO_ICO }
|
||||||
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
|
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
|
||||||
Router::<Route> {}
|
Router::<Route> {}
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/components/footer.rs
Normal file
35
src/components/footer.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
use crate::app::LOGO_SVG;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Footer() -> Element {
|
||||||
|
rsx! {
|
||||||
|
footer {
|
||||||
|
class: "footer md:footer-horizontal fixed bottom-0 text-base-content/70 bg-base-300 items-center p-4",
|
||||||
|
|
||||||
|
aside {
|
||||||
|
class: "flex items-center",
|
||||||
|
|
||||||
|
img {
|
||||||
|
class: "w-12 h-12",
|
||||||
|
src: LOGO_SVG,
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
p {
|
||||||
|
class: "text-xl font-bold",
|
||||||
|
"LibreTunes",
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
class: "text-sm",
|
||||||
|
href: env!("CARGO_PKG_REPOSITORY"),
|
||||||
|
"v" {env!("CARGO_PKG_VERSION")},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/components/form.rs
Normal file
101
src/components/form.rs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
use crate::util::error::Error;
|
||||||
|
|
||||||
|
/// A simple styled form card. Parses form fields into the provided generic type, and calls the
|
||||||
|
/// callback when submitted via button or enter key.
|
||||||
|
#[component]
|
||||||
|
pub fn Form<T>(
|
||||||
|
/// Heading above the form
|
||||||
|
title: String,
|
||||||
|
/// Form content, placed inside a `fieldset`
|
||||||
|
children: Element,
|
||||||
|
/// Text displayed on the form button
|
||||||
|
action_message: String,
|
||||||
|
/// Form-related error. Set to a parsing error on parse failure, or can be used to display any
|
||||||
|
/// submission error. Clears on each submit. Displays as a card between the inputs and action
|
||||||
|
/// button, opens a modal on click.
|
||||||
|
#[props(default)]
|
||||||
|
error: Signal<Option<Error>>,
|
||||||
|
/// Whether the form should display in a "loading" state, disables the action button and
|
||||||
|
/// displays a loading animation instead. For best UX, especially on pages the user might open
|
||||||
|
/// directly as opposed to navigate to within the app, initialize to true. This component will
|
||||||
|
/// set it to false when run. This sequence will discourage users from submitting the form
|
||||||
|
/// before the page is ready to handle it. Can also be used to indicate background work is
|
||||||
|
/// taking place in the `callback`, but this is not done automatically.
|
||||||
|
#[props(default)]
|
||||||
|
loading: Signal<bool>,
|
||||||
|
/// Content to display below the action button
|
||||||
|
#[props(default)]
|
||||||
|
extra_content: Option<Element>,
|
||||||
|
/// Form submission callback. Called with the deserialized form inputs.
|
||||||
|
onsubmit: Callback<T>,
|
||||||
|
) -> Element
|
||||||
|
where
|
||||||
|
T: DeserializeOwned + 'static,
|
||||||
|
{
|
||||||
|
// Button is initialized to loading, then set back when WASM is finished loading (use_effect
|
||||||
|
// runs only on the client)
|
||||||
|
use_effect(move || loading.set(false));
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: "card card-xl w-full sm:w-100 sm:h-fit bg-base-200",
|
||||||
|
|
||||||
|
form {
|
||||||
|
onsubmit: move |evt| {
|
||||||
|
evt.prevent_default();
|
||||||
|
|
||||||
|
let data: T = match evt.data.parsed_values() {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(e) => {
|
||||||
|
let e = Error::message_here(e.to_string())
|
||||||
|
.with_context("Failed to parse form inputs");
|
||||||
|
|
||||||
|
error.set(Some(e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
error.set(None);
|
||||||
|
|
||||||
|
onsubmit.call(data);
|
||||||
|
},
|
||||||
|
|
||||||
|
class: "card-body gap-3",
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
class: "card-title",
|
||||||
|
{title}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
class: "fieldset pt-0 gap-1",
|
||||||
|
{children}
|
||||||
|
}
|
||||||
|
|
||||||
|
{error().map(|e| e.as_alert())}
|
||||||
|
|
||||||
|
div {
|
||||||
|
class: "card-actions gap-4",
|
||||||
|
|
||||||
|
button {
|
||||||
|
class: "btn btn-primary btn-block",
|
||||||
|
disabled: loading(),
|
||||||
|
|
||||||
|
if loading() {
|
||||||
|
span {
|
||||||
|
class: "loading loading-dots"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{action_message}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{extra_content}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/components/form_input.rs
Normal file
68
src/components/form_input.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
/// A simple styled input box
|
||||||
|
#[component]
|
||||||
|
pub fn FormInput(
|
||||||
|
/// The placeholder text and label. When text is entered in the input, the label is shown above
|
||||||
|
/// the input box.
|
||||||
|
label: String,
|
||||||
|
#[props(default)] name: Option<String>,
|
||||||
|
#[props(default)] required: bool,
|
||||||
|
#[props(default)] r#type: String,
|
||||||
|
children: Element,
|
||||||
|
) -> Element {
|
||||||
|
let name = name.unwrap_or(label.to_lowercase());
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: "group/field",
|
||||||
|
|
||||||
|
p {
|
||||||
|
class: "opacity-0 not-group-has-placeholder-shown/field:opacity-70 transition-opacity pl-2",
|
||||||
|
{label.clone()}
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
class: "input w-full",
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
input {
|
||||||
|
name,
|
||||||
|
required,
|
||||||
|
r#type,
|
||||||
|
placeholder: label,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn UsernameInput() -> Element {
|
||||||
|
rsx! {
|
||||||
|
FormInput {
|
||||||
|
label: "Username",
|
||||||
|
required: true,
|
||||||
|
|
||||||
|
lucide_dioxus::UserRound {
|
||||||
|
class: "opacity-50",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn PasswordInput() -> Element {
|
||||||
|
rsx! {
|
||||||
|
FormInput {
|
||||||
|
label: "Password",
|
||||||
|
required: true,
|
||||||
|
r#type: "password",
|
||||||
|
|
||||||
|
lucide_dioxus::KeyRound {
|
||||||
|
class: "opacity-50",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,7 @@
|
|||||||
|
pub mod footer;
|
||||||
|
pub mod form;
|
||||||
|
pub mod form_input;
|
||||||
|
|
||||||
|
pub use footer::*;
|
||||||
|
pub use form::*;
|
||||||
|
pub use form_input::*;
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ use crate::app::App;
|
|||||||
fn tracing_setup() {
|
fn tracing_setup() {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
dioxus::logger::init(tracing::Level::DEBUG).expect("Failed to initialize tracing logger");
|
dioxus::logger::init(tracing::Level::DEBUG).expect("Failed to initialize tracing logger");
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
dioxus::logger::init(tracing::Level::INFO).expect("Failed to initialize tracing logger");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "server"))]
|
#[cfg(not(feature = "server"))]
|
||||||
@@ -28,9 +31,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
|
pub mod user;
|
||||||
|
|||||||
147
src/models/user.rs
Normal file
147
src/models/user.rs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
//! Various user types. Some types marked server-only to help prevent
|
||||||
|
//! leaking passwords to the frontend
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Standard informational user type, contains no password information
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[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
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
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()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
122
src/server/auth.rs
Normal file
122
src/server/auth.rs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
use axum_login::{AuthManagerLayer, AuthUser, AuthnBackend, UserId};
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel_async::RunQueryDsl;
|
||||||
|
use tower_sessions_redis_store::RedisStore;
|
||||||
|
|
||||||
|
use crate::models::user::{DbUser, HashedUserCredentials, UserCredentials};
|
||||||
|
use crate::server::{
|
||||||
|
database::{DbConn, DbPool},
|
||||||
|
key_val_store::KeyValPool,
|
||||||
|
};
|
||||||
|
use crate::util::error::{Contextualize, Error, Result};
|
||||||
|
|
||||||
|
pub type AuthLayer = AuthManagerLayer<AuthBackend, RedisStore<KeyValPool>>;
|
||||||
|
pub type AuthSession = axum_login::AuthSession<AuthBackend>;
|
||||||
|
|
||||||
|
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 create_user(
|
||||||
|
db_conn: &mut DbConn,
|
||||||
|
credentials: &HashedUserCredentials,
|
||||||
|
) -> Result<DbUser> {
|
||||||
|
diesel::insert_into(crate::schema::users::table)
|
||||||
|
.values(credentials)
|
||||||
|
.get_result(db_conn)
|
||||||
|
.await
|
||||||
|
.err_context("Error creating user")
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create the authentication middleware layer
|
||||||
|
pub fn build_auth_layer(db_pool: DbPool, key_val_pool: KeyValPool, use_secure: bool) -> AuthLayer {
|
||||||
|
use axum_login::{AuthManagerLayerBuilder, tower_sessions::SessionManagerLayer};
|
||||||
|
use tower_sessions_redis_store::RedisStore;
|
||||||
|
|
||||||
|
let auth_session_store = RedisStore::new(key_val_pool);
|
||||||
|
let session_layer = SessionManagerLayer::new(auth_session_store).with_secure(use_secure);
|
||||||
|
|
||||||
|
let auth_backend = AuthBackend { db_pool };
|
||||||
|
|
||||||
|
AuthManagerLayerBuilder::new(auth_backend, session_layer).build()
|
||||||
|
}
|
||||||
@@ -1,5 +1,51 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
/// Enable secure cookies by default only in release mode
|
||||||
|
/// (Secure cookies can't be set over HTTP. Rough assumption: development is done over HTTP)
|
||||||
|
const DEFAULT_COOKIES_SECURE: bool = cfg!(not(debug_assertions));
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct AuthConfig {
|
||||||
|
pub open_signup: bool,
|
||||||
|
pub cookies_secure: 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 +85,76 @@ 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)]
|
||||||
|
pub struct ServerConfig {
|
||||||
|
pub public_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
#[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,
|
||||||
|
pub server: ServerConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse configuration from the expected files and environment variables
|
/// Parse configuration from the expected files and environment variables
|
||||||
@@ -82,6 +165,9 @@ 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)?
|
||||||
|
.set_default("auth.cookies_secure", DEFAULT_COOKIES_SECURE)?
|
||||||
|
.set_default("server.public_path", default_public_dir())?
|
||||||
.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))
|
||||||
@@ -89,3 +175,19 @@ pub fn load_config() -> Result<Config, config::ConfigError> {
|
|||||||
.build()?
|
.build()?
|
||||||
.try_deserialize()
|
.try_deserialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Provide a sane default for the public path, using the same sources as Dioxus does internally.
|
||||||
|
/// Checks the `DIOXUS_PUBLIC_PATH` environment variable, then tries relative to the path of this
|
||||||
|
/// executable
|
||||||
|
fn default_public_dir() -> Option<String> {
|
||||||
|
std::env::var("DIOXUS_PUBLIC_PATH").ok().or_else(|| {
|
||||||
|
std::env::current_exe().ok().and_then(|path| {
|
||||||
|
path.parent()
|
||||||
|
.expect("current executable path must have a parent")
|
||||||
|
.join("public")
|
||||||
|
.into_os_string()
|
||||||
|
.into_string()
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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...");
|
||||||
|
|||||||
29
src/server/key_val_store.rs
Normal file
29
src/server/key_val_store.rs
Normal 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)
|
||||||
|
}
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
use tokio::runtime::Runtime;
|
use dioxus::{
|
||||||
|
fullstack::axum::{Router, middleware::from_fn},
|
||||||
|
server::axum::Extension,
|
||||||
|
};
|
||||||
|
use tower_http::services::ServeFile;
|
||||||
|
|
||||||
use crate::App;
|
use crate::App;
|
||||||
use crate::server::{config, database};
|
use crate::app::LOGO_ICO;
|
||||||
|
use crate::server::{
|
||||||
|
auth::build_auth_layer,
|
||||||
|
config::{self, Config},
|
||||||
|
database, key_val_store,
|
||||||
|
require_auth_mw::require_auth_middleware,
|
||||||
|
};
|
||||||
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}");
|
||||||
}
|
}
|
||||||
@@ -14,22 +24,54 @@ pub fn main() -> Result<()> {
|
|||||||
.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
|
// Dioxus doesn't expose a way to configure the public path other than this environment
|
||||||
// allowed. Therefore, this function can't be made async, and we must manually create a runtime
|
// variable, and also doesn't provide a way to read what the public path is. As a workaround we
|
||||||
// for any async setup tasks
|
// expose a config option and set this variable that Dioxus expects.
|
||||||
tracing::debug!("Starting setup runtime...");
|
// SAFETY: "This function is safe to call in a single-threaded program."
|
||||||
let setup_rt = Runtime::new()
|
unsafe {
|
||||||
.map_err(|e| Error::message_here(e.to_string()))
|
std::env::set_var("DIOXUS_PUBLIC_PATH", config.server.public_path.clone());
|
||||||
.err_context("Failed to create tokio runtime for server setup")?;
|
}
|
||||||
|
|
||||||
let _db_pool = setup_rt.block_on(async {
|
// `Ok(...?)` is because `dioxus::serve` expects an `anyhow::Result`
|
||||||
database::setup(config.database.connection_uri())
|
dioxus::serve(move || {
|
||||||
.await
|
let config = config.clone();
|
||||||
.err_context("Failed database setup")
|
async move { Ok(router_setup(config).await?) }
|
||||||
})?;
|
});
|
||||||
|
}
|
||||||
tracing::info!("Setup complete, launching web server...");
|
|
||||||
dioxus::launch(App);
|
/// Set up the axum Router
|
||||||
|
async fn router_setup(config: Config) -> Result<Router> {
|
||||||
Err(Error::message_here("Web server exited"))
|
let db_pool = database::setup(config.database.connection_uri())
|
||||||
|
.await
|
||||||
|
.err_context("Failed database setup")?;
|
||||||
|
|
||||||
|
let key_val_pool = key_val_store::setup(&config.key_val_store.connection_uri())
|
||||||
|
.await
|
||||||
|
.err_context("Failed key-value store setup")?;
|
||||||
|
|
||||||
|
let favicon_path = {
|
||||||
|
// Resolve the favicon path
|
||||||
|
let asset_path = LOGO_ICO.resolve();
|
||||||
|
|
||||||
|
// If the asset path starts with "/", strip it. Otherwise it behaves as an "absolute path"
|
||||||
|
// and replaces the base path in the join operation. This is necessary because Dioxus will
|
||||||
|
// produce a path like "/assets/" in the call to `resolve`
|
||||||
|
let asset_path_rel = asset_path.strip_prefix("/").unwrap_or(&asset_path);
|
||||||
|
|
||||||
|
config.server.public_path.join(asset_path_rel)
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::debug!("Favicon path: {}", favicon_path.display());
|
||||||
|
|
||||||
|
let auth_layer = build_auth_layer(db_pool.clone(), key_val_pool, config.auth.cookies_secure);
|
||||||
|
|
||||||
|
let router = dioxus::server::router(App)
|
||||||
|
.layer(from_fn(require_auth_middleware))
|
||||||
|
.layer(Extension(config))
|
||||||
|
.layer(Extension(db_pool))
|
||||||
|
.layer(auth_layer)
|
||||||
|
.nest_service("/favicon.ico", ServeFile::new(favicon_path));
|
||||||
|
|
||||||
|
tracing::info!("Setup complete, returning Router...");
|
||||||
|
Ok(router)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
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 mod require_auth_mw;
|
||||||
|
|
||||||
pub use main::main;
|
pub use main::main;
|
||||||
|
|||||||
63
src/server/require_auth_mw.rs
Normal file
63
src/server/require_auth_mw.rs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
use dioxus::fullstack::{
|
||||||
|
axum::{body::Body, extract::Request, middleware::Next},
|
||||||
|
extract::FromRequestParts,
|
||||||
|
http::Response,
|
||||||
|
};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
use crate::server::auth::AuthSession;
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
const ALLOWED_PATH_PREFIX: [&str; 1] = ["/assets/"];
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
const ALLOWED_PATH_PREFIX: [&str; 2] = ["/assets/", "/wasm/"];
|
||||||
|
|
||||||
|
const ALLOWED_PATHS: [&str; 6] = [
|
||||||
|
"/login",
|
||||||
|
"/signup",
|
||||||
|
"/api/v1/auth/login",
|
||||||
|
"/api/v1/auth/signup",
|
||||||
|
"/api/v1/auth/user",
|
||||||
|
"/api/v1/auth/open-signup",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Axum middleware to redirect an unauthenticated request to /login unless it matches one of the allowed paths
|
||||||
|
pub async fn require_auth_middleware(
|
||||||
|
req: Request,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<Response<Body>, (StatusCode, &'static str)> {
|
||||||
|
let path = req.uri().path();
|
||||||
|
|
||||||
|
if ALLOWED_PATH_PREFIX
|
||||||
|
.iter()
|
||||||
|
.any(|prefix| path.starts_with(prefix))
|
||||||
|
|| ALLOWED_PATHS.contains(&path)
|
||||||
|
{
|
||||||
|
let response = next.run(req).await;
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (mut parts, body) = req.into_parts();
|
||||||
|
|
||||||
|
let auth_session = AuthSession::from_request_parts(&mut parts, &()).await?;
|
||||||
|
|
||||||
|
if auth_session.user.is_none() {
|
||||||
|
let response = Response::builder()
|
||||||
|
.status(StatusCode::TEMPORARY_REDIRECT)
|
||||||
|
.header("Location", "/login")
|
||||||
|
.body(Body::empty())
|
||||||
|
.map_err(|_| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to build response",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
let req = Request::from_parts(parts, body);
|
||||||
|
let response = next.run(req).await;
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
@@ -43,7 +43,21 @@ 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>;
|
||||||
|
|
||||||
|
/// Generate a random string to use as an id for the modal
|
||||||
|
/// This allows multiple toast/modal to be present
|
||||||
|
fn rand_modal_id() -> String {
|
||||||
|
use rand::RngExt;
|
||||||
|
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
|
||||||
|
let random_str = (0..5)
|
||||||
|
.map(|_| rng.sample(rand::distr::Alphanumeric) as char)
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
format!("err-modal-{random_str}")
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, thiserror::Error)]
|
#[derive(Debug, Clone, Deserialize, Serialize, thiserror::Error)]
|
||||||
pub struct Error {
|
pub struct Error {
|
||||||
@@ -97,6 +111,12 @@ impl Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the original cause of this error
|
||||||
|
/// Like `std::error::Error::source` but doesn't return an `Option`
|
||||||
|
pub fn source(&self) -> &ErrorType {
|
||||||
|
&self.source
|
||||||
|
}
|
||||||
|
|
||||||
/// Display this error as a modal dialog activated by a checkbox with the given id
|
/// Display this error as a modal dialog activated by a checkbox with the given id
|
||||||
pub fn as_modal(&self, id: String) -> Element {
|
pub fn as_modal(&self, id: String) -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
@@ -173,42 +193,46 @@ impl Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert this error into an alert label, tied to the given modal id
|
||||||
|
fn as_alert_for(&self, modal_id: String) -> Element {
|
||||||
|
rsx! {
|
||||||
|
label {
|
||||||
|
class: "alert alert-error alert-soft cursor-pointer",
|
||||||
|
role: "alert",
|
||||||
|
r#for: modal_id,
|
||||||
|
lucide_dioxus::CircleAlert {}
|
||||||
|
|
||||||
|
p {
|
||||||
|
class: "max-w-120 text-ellipsis line-clamp-3",
|
||||||
|
{self.top_message()}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert this error to a toast message, which opens a modal when clicked
|
/// Convert this error to a toast message, which opens a modal when clicked
|
||||||
pub fn as_toast(&self) -> Element {
|
pub fn as_toast(&self) -> Element {
|
||||||
// Generate a random string to use as an id for the modal
|
let modal_id = rand_modal_id();
|
||||||
// This allows multiple toast/modal to be present
|
|
||||||
let modal_id = {
|
|
||||||
use rand::RngExt;
|
|
||||||
|
|
||||||
let mut rng = rand::rng();
|
|
||||||
|
|
||||||
let random_str = (0..5)
|
|
||||||
.map(|_| rng.sample(rand::distr::Alphanumeric) as char)
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
format!("err-modal-{random_str}")
|
|
||||||
};
|
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
{self.as_modal(modal_id.clone())}
|
{self.as_modal(modal_id.clone())}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: "toast",
|
class: "toast z-99",
|
||||||
|
{self.as_alert_for(modal_id)}
|
||||||
label {
|
|
||||||
class: "alert alert-error alert-soft cursor-pointer",
|
|
||||||
role: "alert",
|
|
||||||
r#for: modal_id,
|
|
||||||
lucide_dioxus::CircleAlert {}
|
|
||||||
|
|
||||||
p {
|
|
||||||
class: "max-w-120 text-ellipsis line-clamp-3",
|
|
||||||
{self.top_message()}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert this error into an alert label, which opens a modal when clicked
|
||||||
|
pub fn as_alert(&self) -> Element {
|
||||||
|
let modal_id = rand_modal_id();
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
{self.as_modal(modal_id.clone())}
|
||||||
|
{self.as_alert_for(modal_id)}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
@@ -239,12 +263,14 @@ impl From<ServerFnError> for Error {
|
|||||||
impl dioxus_fullstack::AsStatusCode for Error {
|
impl dioxus_fullstack::AsStatusCode for Error {
|
||||||
fn as_status_code(&self) -> StatusCode {
|
fn as_status_code(&self) -> StatusCode {
|
||||||
match &self.source {
|
match &self.source {
|
||||||
|
ErrorType::Auth(AuthError::InvalidCredentials | AuthError::Unauthorized) => {
|
||||||
|
StatusCode::UNAUTHORIZED
|
||||||
|
}
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,6 +309,9 @@ impl<T, E: Into<Error>> Contextualize<Result<T>> for E {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, thiserror::Error, Deserialize, Serialize)]
|
#[derive(Debug, Clone, thiserror::Error, Deserialize, Serialize)]
|
||||||
pub enum ErrorType {
|
pub enum ErrorType {
|
||||||
|
#[error("Authentication error: {0}")]
|
||||||
|
Auth(AuthError),
|
||||||
|
|
||||||
// Using string to represent Diesel errors, because Diesel's Error type is not `Serialize`,
|
// Using string to represent Diesel errors, because Diesel's Error type is not `Serialize`,
|
||||||
// and Diesel is only available on the server
|
// and Diesel is only available on the server
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
@@ -293,6 +322,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 +343,33 @@ 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}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, thiserror::Error, Deserialize, Serialize)]
|
||||||
|
pub enum AuthError {
|
||||||
|
#[error("Invalid credentials")]
|
||||||
|
InvalidCredentials,
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
Error(String),
|
||||||
|
|
||||||
|
#[error("Unauthorized")]
|
||||||
|
Unauthorized,
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,4 +21,9 @@
|
|||||||
@apply hover:text-base-content/70;
|
@apply hover:text-base-content/70;
|
||||||
@apply active:text-primary active:translate-y-[.5px] active:shadow-(--btn-shadow);
|
@apply active:text-primary active:translate-y-[.5px] active:shadow-(--btn-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input:focus-within {
|
||||||
|
outline-width: 1px;
|
||||||
|
outline-offset: 0px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user