feat: implement initial user authentication, session management, and admin dashboard routing with
This commit is contained in:
600
Cargo.lock
generated
600
Cargo.lock
generated
@@ -2,12 +2,27 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "askama"
|
||||
version = "0.15.4"
|
||||
@@ -133,6 +148,28 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-extra"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"cookie",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
@@ -154,6 +191,19 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a0f5948f30df5f43ac29d310b7476793be97c50787e6ef4a63d960a0d0be827"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"blowfish",
|
||||
"getrandom 0.3.4",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
@@ -176,13 +226,39 @@ dependencies = [
|
||||
name = "blog_cms"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"askama",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
"base64",
|
||||
"bcrypt",
|
||||
"dotenvy",
|
||||
"rand 0.10.0",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blowfish"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
@@ -211,6 +287,27 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"rand_core 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@@ -226,6 +323,17 @@ version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -235,6 +343,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.4.0"
|
||||
@@ -286,6 +403,15 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -476,6 +602,32 @@ dependencies = [
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"rand_core 0.10.0",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
@@ -703,6 +855,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
@@ -732,6 +890,17 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -740,6 +909,16 @@ version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -749,6 +928,12 @@ dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.182"
|
||||
@@ -805,6 +990,15 @@ version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.8.4"
|
||||
@@ -844,6 +1038,15 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.6"
|
||||
@@ -855,11 +1058,17 @@ dependencies = [
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-traits",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"smallvec",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
@@ -994,6 +1203,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
@@ -1003,6 +1218,16 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -1021,6 +1246,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
@@ -1029,7 +1260,18 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom 0.4.1",
|
||||
"rand_core 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1039,7 +1281,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1048,9 +1290,15 @@ version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
@@ -1069,6 +1317,23 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.10"
|
||||
@@ -1082,7 +1347,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"pkcs1",
|
||||
"pkcs8",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
"signature",
|
||||
"spki",
|
||||
"subtle",
|
||||
@@ -1095,6 +1360,12 @@ version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
@@ -1107,6 +1378,12 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
@@ -1180,7 +1457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -1191,10 +1468,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -1208,7 +1494,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1369,7 +1655,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"rsa",
|
||||
"serde",
|
||||
"sha1",
|
||||
@@ -1407,7 +1693,7 @@ dependencies = [
|
||||
"md-5",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -1514,6 +1800,46 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.2"
|
||||
@@ -1592,6 +1918,22 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
@@ -1634,6 +1976,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1669,6 +2041,12 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -1687,6 +2065,23 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
|
||||
dependencies = [
|
||||
"getrandom 0.4.1",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
@@ -1705,12 +2100,109 @@ version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.6.1"
|
||||
@@ -1885,6 +2377,94 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.2"
|
||||
|
||||
10
Cargo.toml
10
Cargo.toml
@@ -4,8 +4,18 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.102"
|
||||
askama = "0.15.4"
|
||||
axum = "0.8.8"
|
||||
axum-extra = { version = "0.12.5", features = ["cookie"] }
|
||||
base64 = "0.22.1"
|
||||
bcrypt = "0.18.0"
|
||||
dotenvy = "0.15.7"
|
||||
rand = "0.10.0"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] }
|
||||
tokio = { version = "1.49.0", features = ["rt-multi-thread", "macros"] }
|
||||
tower-http = { version = "0.6.8", features = ["trace"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||
uuid = { version = "1.21.0", features = ["v4"] }
|
||||
|
||||
4
cookies.txt
Normal file
4
cookies.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
3
server.log
Normal file
3
server.log
Normal file
@@ -0,0 +1,3 @@
|
||||
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
|
||||
Running `target/debug/blog_cms`
|
||||
Server running at http://0.0.0.0:3000
|
||||
27
src/error.rs
Normal file
27
src/error.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
pub struct AppError(anyhow::Error);
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
tracing::error!("Application error: {:?}", self.0);
|
||||
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", self.0),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<E> for AppError
|
||||
where
|
||||
E: Into<anyhow::Error>,
|
||||
{
|
||||
fn from(err: E) -> Self {
|
||||
Self(err.into())
|
||||
}
|
||||
}
|
||||
142
src/handlers/admin.rs
Normal file
142
src/handlers/admin.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
extract::{Extension, Form, Path, State},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use bcrypt::{hash, DEFAULT_COST};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::models::{Role, User};
|
||||
use crate::utils::HtmlTemplate;
|
||||
use crate::AppState;
|
||||
use crate::error::AppError;
|
||||
use anyhow::Context;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "dashboard.html")]
|
||||
pub struct DashboardTemplate<'a> {
|
||||
pub current_user: &'a User,
|
||||
pub users: Vec<User>,
|
||||
pub error: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub fn router() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/", get(dashboard))
|
||||
.route("/users/add", post(add_user))
|
||||
.route("/users/delete/{id}", post(delete_user))
|
||||
.route("/users/password/{id}", post(change_password))
|
||||
}
|
||||
|
||||
pub async fn dashboard(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Extension(current_user): Extension<User>,
|
||||
) -> Result<Response, AppError> {
|
||||
render_dashboard(&state, ¤t_user, None).await
|
||||
}
|
||||
|
||||
async fn render_dashboard(
|
||||
state: &AppState,
|
||||
current_user: &User,
|
||||
error: Option<&str>,
|
||||
) -> Result<Response, AppError> {
|
||||
let users: Vec<User> = sqlx::query_as("SELECT * FROM users")
|
||||
.fetch_all(&state.db)
|
||||
.await
|
||||
.context("Failed to fetch users from database in render_dashboard")?;
|
||||
|
||||
Ok(HtmlTemplate(DashboardTemplate {
|
||||
current_user,
|
||||
users,
|
||||
error,
|
||||
})
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AddUserPayload {
|
||||
pub username: String,
|
||||
pub password: Option<String>,
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
pub async fn add_user(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Extension(current_user): Extension<User>,
|
||||
Form(payload): Form<AddUserPayload>,
|
||||
) -> Result<Response, AppError> {
|
||||
if current_user.role() != Role::Admin {
|
||||
return render_dashboard(&state, ¤t_user, Some("Permission denied")).await;
|
||||
}
|
||||
|
||||
let password = payload.password.unwrap_or_else(|| "password".to_string());
|
||||
let hashed = hash(&password, DEFAULT_COST).context("Failed to hash password")?;
|
||||
|
||||
let role = if payload.role == "admin" {
|
||||
"admin"
|
||||
} else {
|
||||
"readonly"
|
||||
};
|
||||
|
||||
sqlx::query("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)")
|
||||
.bind(&payload.username)
|
||||
.bind(&hashed)
|
||||
.bind(role)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.context("Failed to insert new user into database")?;
|
||||
|
||||
Ok(Redirect::temporary("/__dungeon").into_response())
|
||||
}
|
||||
|
||||
pub async fn delete_user(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Extension(current_user): Extension<User>,
|
||||
Path(id): Path<i64>,
|
||||
) -> Result<Response, AppError> {
|
||||
if current_user.role() != Role::Admin {
|
||||
return render_dashboard(&state, ¤t_user, Some("Permission denied")).await;
|
||||
}
|
||||
|
||||
if current_user.id == id {
|
||||
return render_dashboard(&state, ¤t_user, Some("Cannot delete yourself")).await;
|
||||
}
|
||||
|
||||
sqlx::query("DELETE FROM users WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.context(format!("Failed to delete user with id {}", id))?;
|
||||
|
||||
Ok(Redirect::temporary("/__dungeon").into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChangePasswordPayload {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
pub async fn change_password(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Extension(current_user): Extension<User>,
|
||||
Path(id): Path<i64>,
|
||||
Form(payload): Form<ChangePasswordPayload>,
|
||||
) -> Result<Response, AppError> {
|
||||
if current_user.role() != Role::Admin {
|
||||
return render_dashboard(&state, ¤t_user, Some("Permission denied")).await;
|
||||
}
|
||||
|
||||
let hashed = hash(&payload.password, DEFAULT_COST).context("Failed to hash password")?;
|
||||
|
||||
sqlx::query("UPDATE users SET password_hash = ? WHERE id = ?")
|
||||
.bind(&hashed)
|
||||
.bind(id)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.context(format!("Failed to update password for user with id {}", id))?;
|
||||
|
||||
Ok(Redirect::temporary("/__dungeon").into_response())
|
||||
}
|
||||
153
src/handlers/auth.rs
Normal file
153
src/handlers/auth.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
extract::{Form, State},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use axum_extra::extract::cookie::{Cookie, CookieJar};
|
||||
use bcrypt::{hash, verify, DEFAULT_COST};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::User;
|
||||
use crate::utils::HtmlTemplate;
|
||||
use crate::AppState;
|
||||
use crate::error::AppError;
|
||||
use anyhow::Context;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "setup.html")]
|
||||
pub struct SetupTemplate<'a> {
|
||||
pub error: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "login.html")]
|
||||
pub struct LoginTemplate<'a> {
|
||||
pub error: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub fn router() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/setup", get(setup_form).post(setup_submit))
|
||||
.route("/login", get(login_form).post(login_submit))
|
||||
.route("/logout", post(logout_submit))
|
||||
}
|
||||
|
||||
pub async fn setup_form(State(state): State<Arc<AppState>>) -> Result<Response, AppError> {
|
||||
// Check if users exist
|
||||
let count: (i64,) = sqlx::query_as("SELECT count(*) FROM users")
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.context("Failed to check user count for setup form")?;
|
||||
|
||||
if count.0 > 0 {
|
||||
return Ok(Redirect::temporary("/__dungeon/login").into_response());
|
||||
}
|
||||
|
||||
Ok(HtmlTemplate(SetupTemplate { error: None }).into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AuthPayload {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
pub async fn setup_submit(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Form(payload): Form<AuthPayload>,
|
||||
) -> Result<Response, AppError> {
|
||||
let count: (i64,) = sqlx::query_as("SELECT count(*) FROM users")
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.context("Failed to check user count for setup submit")?;
|
||||
|
||||
if count.0 > 0 {
|
||||
return Ok(Redirect::temporary("/__dungeon/login").into_response());
|
||||
}
|
||||
|
||||
let hashed = hash(&payload.password, DEFAULT_COST).context("Failed to hash password")?;
|
||||
|
||||
sqlx::query("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)")
|
||||
.bind(&payload.username)
|
||||
.bind(&hashed)
|
||||
.bind("admin")
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.context("Failed to insert initial admin user")?;
|
||||
|
||||
Ok(Redirect::temporary("/__dungeon/login").into_response())
|
||||
}
|
||||
|
||||
pub async fn login_form() -> impl IntoResponse {
|
||||
HtmlTemplate(LoginTemplate { error: None })
|
||||
}
|
||||
|
||||
pub async fn login_submit(
|
||||
State(state): State<Arc<AppState>>,
|
||||
cookie_jar: CookieJar,
|
||||
Form(payload): Form<AuthPayload>,
|
||||
) -> Result<Response, AppError> {
|
||||
let user: Option<User> = sqlx::query_as("SELECT * FROM users WHERE username = ?")
|
||||
.bind(&payload.username)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.context("Failed to fetch user during login")?;
|
||||
|
||||
if let Some(user) = user {
|
||||
let is_valid = verify(&payload.password, &user.password_hash).context("Failed to verify password")?;
|
||||
if is_valid {
|
||||
// Create session
|
||||
let session_id = Uuid::new_v4().to_string();
|
||||
let expires_at = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.context("Failed to get system time")?
|
||||
.as_secs() as i64
|
||||
+ 86400 * 30; // 30 days
|
||||
|
||||
sqlx::query("INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)")
|
||||
.bind(&session_id)
|
||||
.bind(user.id)
|
||||
.bind(expires_at)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.context("Failed to insert session into database")?;
|
||||
|
||||
let mut cookie = Cookie::new("dungeon_session", session_id);
|
||||
cookie.set_path("/");
|
||||
return Ok((cookie_jar.add(cookie), Redirect::temporary("/__dungeon")).into_response());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HtmlTemplate(LoginTemplate {
|
||||
error: Some("Invalid username or password"),
|
||||
})
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn logout_submit(
|
||||
State(state): State<Arc<AppState>>,
|
||||
cookie_jar: CookieJar,
|
||||
) -> Result<Response, AppError> {
|
||||
if let Some(cookie) = cookie_jar.get("dungeon_session") {
|
||||
sqlx::query("DELETE FROM sessions WHERE id = ?")
|
||||
.bind(cookie.value())
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.context("Failed to delete session on logout")?;
|
||||
}
|
||||
|
||||
// Cookie removal might need build or just empty cookie matching the name.
|
||||
// Setting max-age to 0 is an alternative, but jar.remove works with path and matching name.
|
||||
let remove_cookie = Cookie::build(("dungeon_session", "")).path("/").build();
|
||||
|
||||
Ok((
|
||||
cookie_jar.remove(remove_cookie),
|
||||
Redirect::temporary("/__dungeon/login"),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
19
src/handlers/mod.rs
Normal file
19
src/handlers/mod.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use crate::{middleware::auth_middleware, AppState};
|
||||
use axum::{middleware, Router};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod admin;
|
||||
pub mod auth;
|
||||
|
||||
pub fn router(state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
// Admin dashboard routes under /__dungeon
|
||||
.merge(admin::router())
|
||||
// Auth routes under /__dungeon
|
||||
.merge(auth::router())
|
||||
// Apply middleware to all /__dungeon routes
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
auth_middleware,
|
||||
))
|
||||
}
|
||||
73
src/main.rs
73
src/main.rs
@@ -1,7 +1,7 @@
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::{Html, IntoResponse},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
@@ -11,9 +11,15 @@ use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
use std::env;
|
||||
|
||||
pub mod models;
|
||||
pub mod middleware;
|
||||
pub mod utils;
|
||||
pub mod error;
|
||||
pub mod handlers;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
db: SqlitePool,
|
||||
pub struct AppState {
|
||||
pub db: SqlitePool,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
@@ -22,35 +28,25 @@ struct IndexTemplate {
|
||||
title: String,
|
||||
}
|
||||
|
||||
struct HtmlTemplate<T>(T);
|
||||
|
||||
impl<T> IntoResponse for HtmlTemplate<T>
|
||||
where
|
||||
T: Template,
|
||||
{
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
match self.0.render() {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
Err(err) => (
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to render template: {}", err),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn index(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
let template = IndexTemplate {
|
||||
title: "Coming Soon".to_string(),
|
||||
};
|
||||
HtmlTemplate(template)
|
||||
crate::utils::HtmlTemplate(template)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
"blog_cms=debug,tower_http=debug,axum=debug".into()
|
||||
}),
|
||||
)
|
||||
.init();
|
||||
|
||||
let db_url = env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite://data.db".to_string());
|
||||
let port = env::var("PORT").unwrap_or_else(|_| "3000".to_string());
|
||||
let addr = format!("0.0.0.0:{}", port);
|
||||
@@ -65,11 +61,42 @@ async fn main() {
|
||||
.await
|
||||
.expect("Failed to connect to SQLite database");
|
||||
|
||||
// Create schema
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'readonly'
|
||||
);
|
||||
"#,
|
||||
)
|
||||
.execute(&db_pool)
|
||||
.await
|
||||
.expect("Failed to create users table");
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
"#,
|
||||
)
|
||||
.execute(&db_pool)
|
||||
.await
|
||||
.expect("Failed to create sessions table");
|
||||
|
||||
let app_state = Arc::new(AppState { db: db_pool });
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(index))
|
||||
.with_state(app_state);
|
||||
.nest("/__dungeon", handlers::router(&app_state)) // I'll create a single `handlers::router`
|
||||
.with_state(app_state.clone())
|
||||
.layer(tower_http::trace::TraceLayer::new_for_http());
|
||||
|
||||
let listener = TcpListener::bind(&addr).await.unwrap();
|
||||
println!("Server running at http://{}", addr);
|
||||
|
||||
92
src/middleware.rs
Normal file
92
src/middleware.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
http::StatusCode,
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::models::{Session, User};
|
||||
use crate::AppState;
|
||||
|
||||
pub async fn auth_middleware(
|
||||
cookie_jar: CookieJar,
|
||||
State(state): State<Arc<AppState>>,
|
||||
mut request: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, Response> {
|
||||
let path = request.uri().path();
|
||||
|
||||
// Allow access to login and setup directly
|
||||
// Because this middleware is on a nested router, the path will have the `/__dungeon` prefix stripped.
|
||||
if path == "/login" || path == "/setup" || path == "/__dungeon/login" || path == "/__dungeon/setup" {
|
||||
return Ok(next.run(request).await);
|
||||
}
|
||||
|
||||
// Check if there are any users in the database
|
||||
let user_count: (i64,) = sqlx::query_as("SELECT count(*) FROM users")
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Database error: {}", e),
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
if user_count.0 == 0 {
|
||||
return Err(Redirect::temporary("/__dungeon/setup").into_response());
|
||||
}
|
||||
|
||||
// Check session
|
||||
let session_cookie = cookie_jar.get("dungeon_session");
|
||||
|
||||
if let Some(cookie) = session_cookie {
|
||||
let session_id = cookie.value();
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
|
||||
let session: Option<Session> = sqlx::query_as("SELECT * FROM sessions WHERE id = ? AND expires_at > ?")
|
||||
.bind(session_id)
|
||||
.bind(now)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Database error: {}", e),
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
if let Some(session) = session {
|
||||
// Get user
|
||||
let user: Option<User> = sqlx::query_as("SELECT * FROM users WHERE id = ?")
|
||||
.bind(session.user_id)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Database error: {}", e),
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
if let Some(user) = user {
|
||||
// Insert User into request extensions
|
||||
request.extensions_mut().insert(user);
|
||||
return Ok(next.run(request).await);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid session or user, redirect to login
|
||||
Err(Redirect::temporary("/__dungeon/login").into_response())
|
||||
}
|
||||
49
src/models.rs
Normal file
49
src/models.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::prelude::FromRow;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub enum Role {
|
||||
Admin,
|
||||
Readonly,
|
||||
}
|
||||
|
||||
impl ToString for Role {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Role::Admin => "admin".to_string(),
|
||||
Role::Readonly => "readonly".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Role {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"admin" => Ok(Role::Admin),
|
||||
"readonly" => Ok(Role::Readonly),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
pub username: String,
|
||||
pub password_hash: String,
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn role(&self) -> Role {
|
||||
self.role.parse().unwrap_or(Role::Readonly)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct Session {
|
||||
pub id: String,
|
||||
pub user_id: i64,
|
||||
pub expires_at: i64, // Unix timestamp in seconds
|
||||
}
|
||||
23
src/utils.rs
Normal file
23
src/utils.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
|
||||
pub struct HtmlTemplate<T>(pub T);
|
||||
|
||||
impl<T> IntoResponse for HtmlTemplate<T>
|
||||
where
|
||||
T: Template,
|
||||
{
|
||||
fn into_response(self) -> Response {
|
||||
match self.0.render() {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
Err(err) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to render template: {}", err),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
139
templates/base.html
Normal file
139
templates/base.html
Normal file
@@ -0,0 +1,139 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Dungeon{% endblock %}</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f172a;
|
||||
--surface: #1e293b;
|
||||
--primary: #3b82f6;
|
||||
--text: #f8fafc;
|
||||
--text-muted: #94a3b8;
|
||||
--border: #334155;
|
||||
--danger: #ef4444;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
max-width: 400px;
|
||||
margin: 4rem auto;
|
||||
}
|
||||
|
||||
.dashboard-container {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--primary);
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--danger);
|
||||
margin-bottom: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
th {
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
83
templates/dashboard.html
Normal file
83
templates/dashboard.html
Normal file
@@ -0,0 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dungeon Dashboard{% endblock %}
|
||||
{% block content %}
|
||||
<div class="header">
|
||||
<h1>Dungeon Dashboard</h1>
|
||||
<form method="POST" action="/__dungeon/logout" style="margin: 0;">
|
||||
<button type="submit" style="width: auto; margin: 0; padding: 0.5rem 1rem;">Logout ({{ current_user.username
|
||||
}})</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if let Some(err) = error %}
|
||||
<div class="error">{{ err }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="dashboard-container">
|
||||
<div style="flex: 2;">
|
||||
<h3>Users</h3>
|
||||
<div style="background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow: hidden;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Username</th>
|
||||
<th>Role</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.username }}</td>
|
||||
<td><span
|
||||
style="padding: 0.25rem 0.5rem; background: var(--bg); border-radius: 4px; font-size: 0.875rem;">{{
|
||||
user.role }}</span></td>
|
||||
<td>
|
||||
{% if current_user.role == "admin" %}
|
||||
<div class="flex gap-2" style="align-items: center;">
|
||||
{% if user.id != current_user.id %}
|
||||
<form method="POST" action="/__dungeon/users/delete/{{ user.id }}" style="margin: 0;">
|
||||
<button type="submit" class="btn-danger"
|
||||
style="padding: 0.25rem 0.5rem; margin: 0; font-size: 0.875rem;">Delete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="POST" action="/__dungeon/users/password/{{ user.id }}"
|
||||
style="margin: 0; display: flex; gap: 0.5rem;">
|
||||
<input type="password" name="password" placeholder="New Password" required
|
||||
style="margin: 0; padding: 0.25rem 0.5rem; width: 120px;">
|
||||
<button type="submit"
|
||||
style="padding: 0.25rem 0.5rem; margin: 0; font-size: 0.875rem; width: auto;">Change</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if current_user.role == "admin" %}
|
||||
<div style="flex: 1;">
|
||||
<div class="card" style="margin: 0; max-width: 100%;">
|
||||
<h3 style="margin-top: 0;">Add User</h3>
|
||||
<form method="POST" action="/__dungeon/users/add">
|
||||
<label>Username</label>
|
||||
<input type="text" name="username" required>
|
||||
<label>Password</label>
|
||||
<input type="password" name="password" required>
|
||||
<label>Role</label>
|
||||
<select name="role">
|
||||
<option value="readonly">Read Only</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
<button type="submit">Create User</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
17
templates/login.html
Normal file
17
templates/login.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dungeon Login{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h2>Enter the Dungeon</h2>
|
||||
{% if let Some(err) = error %}
|
||||
<div class="error">{{ err }}</div>
|
||||
{% endif %}
|
||||
<form method="POST" action="/__dungeon/login">
|
||||
<label>Username</label>
|
||||
<input type="text" name="username" required autocomplete="username">
|
||||
<label>Password</label>
|
||||
<input type="password" name="password" required autocomplete="current-password">
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
18
templates/setup.html
Normal file
18
templates/setup.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Setup Dungeon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h2>Initial Setup</h2>
|
||||
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">Create the initial admin user to access the dungeon.</p>
|
||||
{% if let Some(err) = error %}
|
||||
<div class="error">{{ err }}</div>
|
||||
{% endif %}
|
||||
<form method="POST" action="/__dungeon/setup">
|
||||
<label>Username</label>
|
||||
<input type="text" name="username" required autocomplete="off">
|
||||
<label>Password</label>
|
||||
<input type="password" name="password" required>
|
||||
<button type="submit">Create Admin</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user