diff --git a/Cargo.lock b/Cargo.lock index 1275639..c18b73c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 521654d..0729bed 100644 --- a/Cargo.toml +++ b/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"] } diff --git a/cookies.txt b/cookies.txt new file mode 100644 index 0000000..c31d989 --- /dev/null +++ b/cookies.txt @@ -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. + diff --git a/server.log b/server.log new file mode 100644 index 0000000..a09a536 --- /dev/null +++ b/server.log @@ -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 diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..8150884 --- /dev/null +++ b/src/error.rs @@ -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 From for AppError +where + E: Into, +{ + fn from(err: E) -> Self { + Self(err.into()) + } +} diff --git a/src/handlers/admin.rs b/src/handlers/admin.rs new file mode 100644 index 0000000..2d413cf --- /dev/null +++ b/src/handlers/admin.rs @@ -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, + pub error: Option<&'a str>, +} + +pub fn router() -> Router> { + 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>, + Extension(current_user): Extension, +) -> Result { + render_dashboard(&state, ¤t_user, None).await +} + +async fn render_dashboard( + state: &AppState, + current_user: &User, + error: Option<&str>, +) -> Result { + let users: Vec = 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, + pub role: String, +} + +pub async fn add_user( + State(state): State>, + Extension(current_user): Extension, + Form(payload): Form, +) -> Result { + 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>, + Extension(current_user): Extension, + Path(id): Path, +) -> Result { + 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>, + Extension(current_user): Extension, + Path(id): Path, + Form(payload): Form, +) -> Result { + 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()) +} diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs new file mode 100644 index 0000000..c2e3aa7 --- /dev/null +++ b/src/handlers/auth.rs @@ -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> { + 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>) -> Result { + // 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>, + Form(payload): Form, +) -> Result { + 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>, + cookie_jar: CookieJar, + Form(payload): Form, +) -> Result { + let user: Option = 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>, + cookie_jar: CookieJar, +) -> Result { + 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()) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..f8f0088 --- /dev/null +++ b/src/handlers/mod.rs @@ -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) -> Router> { + 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, + )) +} diff --git a/src/main.rs b/src/main.rs index 0ff42fb..d3d1364 100644 --- a/src/main.rs +++ b/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); - -impl IntoResponse for HtmlTemplate -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>) -> 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); diff --git a/src/middleware.rs b/src/middleware.rs new file mode 100644 index 0000000..14026d8 --- /dev/null +++ b/src/middleware.rs @@ -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>, + mut request: Request, + next: Next, +) -> Result { + 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 = 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 = 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()) +} diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..d005e08 --- /dev/null +++ b/src/models.rs @@ -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 { + 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 +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..3b384c2 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,23 @@ +use askama::Template; +use axum::{ + http::StatusCode, + response::{Html, IntoResponse, Response}, +}; + +pub struct HtmlTemplate(pub T); + +impl IntoResponse for HtmlTemplate +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(), + } + } +} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..f61ee70 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,139 @@ + + + + + + + {% block title %}Dungeon{% endblock %} + + + + +
+ {% block content %}{% endblock %} +
+ + + \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..e02230b --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} +{% block title %}Dungeon Dashboard{% endblock %} +{% block content %} +
+

Dungeon Dashboard

+
+ +
+
+ +{% if let Some(err) = error %} +
{{ err }}
+{% endif %} + +
+
+

Users

+
+ + + + + + + + + + + {% for user in users %} + + + + + + + {% endfor %} + +
IDUsernameRoleActions
{{ user.id }}{{ user.username }}{{ + user.role }} + {% if current_user.role == "admin" %} +
+ {% if user.id != current_user.id %} +
+ +
+ {% endif %} +
+ + +
+
+ {% endif %} +
+
+
+ + {% if current_user.role == "admin" %} +
+
+

Add User

+
+ + + + + + + +
+
+
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..33c064c --- /dev/null +++ b/templates/login.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% block title %}Dungeon Login{% endblock %} +{% block content %} +
+

Enter the Dungeon

+ {% if let Some(err) = error %} +
{{ err }}
+ {% endif %} +
+ + + + + +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/setup.html b/templates/setup.html new file mode 100644 index 0000000..e6b7a00 --- /dev/null +++ b/templates/setup.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}Setup Dungeon{% endblock %} +{% block content %} +
+

Initial Setup

+

Create the initial admin user to access the dungeon.

+ {% if let Some(err) = error %} +
{{ err }}
+ {% endif %} +
+ + + + + +
+
+{% endblock %} \ No newline at end of file