diff --git a/Cargo.lock b/Cargo.lock index 09fecbe..507f70a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "version_check", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -88,7 +99,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -135,16 +146,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", -] - -[[package]] -name = "atoi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5" -dependencies = [ - "num-traits", + "syn 2.0.18", ] [[package]] @@ -164,9 +166,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.6.17" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b70caf9f1b0c045f7da350636435b775a9733adf2df56e8aa2a29210fbc335d4" +checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39" dependencies = [ "async-trait", "axum-core", @@ -194,7 +196,6 @@ dependencies = [ "tower", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -212,7 +213,6 @@ dependencies = [ "rustversion", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -255,7 +255,7 @@ dependencies = [ "secrecy", "serde", "serde_json", - "sqlx 0.6.3", + "sqlx", "tokio", "tower", "tower-http 0.3.5", @@ -271,7 +271,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -290,12 +290,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "base-x" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" - [[package]] name = "base64" version = "0.13.1" @@ -304,9 +298,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" [[package]] name = "base64ct" @@ -344,7 +338,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -382,9 +376,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.1" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "byteorder" @@ -429,22 +423,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - -[[package]] -name = "const_fn" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" - [[package]] name = "constant_time_eq" version = "0.1.5" @@ -457,13 +435,13 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" dependencies = [ - "base64 0.21.0", + "base64 0.21.2", "hmac 0.12.1", "percent-encoding", "rand", "sha2 0.10.6", "subtle", - "time 0.3.21", + "time", "version_check", ] @@ -482,30 +460,15 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49fc9a695bca7f35f5f4c15cddc84415f66a74ea78eef08e90c5024f2b540e23" -dependencies = [ - "crc-catalog 1.1.1", -] - [[package]] name = "crc" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" dependencies = [ - "crc-catalog 2.2.0", + "crc-catalog", ] -[[package]] -name = "crc-catalog" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403" - [[package]] name = "crc-catalog" version = "2.2.0" @@ -561,50 +524,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "cxx" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.15", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.15", -] - [[package]] name = "digest" version = "0.9.0" @@ -616,27 +535,15 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "crypto-common", "subtle", ] -[[package]] -name = "discard" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" - -[[package]] -name = "dotenv" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" - [[package]] name = "dotenvy" version = "0.15.7" @@ -765,7 +672,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -819,40 +726,28 @@ dependencies = [ "wasi", ] -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -dependencies = [ - "ahash", -] - [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.3", ] [[package]] name = "hashlink" -version = "0.7.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +checksum = "0761a1b9491c4f2e3d66aa0f62d0fba0af9a0e2852e4d48ea506632a4b56e6aa" dependencies = [ - "hashbrown 0.11.2", -] - -[[package]] -name = "hashlink" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" -dependencies = [ - "hashbrown 0.12.3", + "hashbrown 0.13.2", ] [[package]] @@ -868,7 +763,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha1 0.10.5", + "sha1", ] [[package]] @@ -920,7 +815,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -1011,12 +906,11 @@ dependencies = [ [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] @@ -1071,9 +965,9 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" dependencies = [ "wasm-bindgen", ] @@ -1097,15 +991,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.142" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "libm" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "libsqlite3-sys" @@ -1118,15 +1012,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "link-cplusplus" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] - [[package]] name = "lock_api" version = "0.4.9" @@ -1139,12 +1024,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if 1.0.0", -] +checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" [[package]] name = "matchers" @@ -1191,14 +1073,13 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "eebffdb73fe72e917997fad08bdbf31ac50b0fa91cec93e69a0662e4264d454c" dependencies = [ "libc", - "log", "wasi", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -1341,22 +1222,22 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pin-project" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.18", ] [[package]] @@ -1373,9 +1254,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "ppv-lite86" @@ -1383,26 +1264,20 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -1448,11 +1323,11 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.1" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" dependencies = [ - "regex-syntax 0.7.1", + "regex-syntax 0.7.2", ] [[package]] @@ -1472,9 +1347,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "ring" @@ -1491,28 +1366,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver", -] - -[[package]] -name = "rustls" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" -dependencies = [ - "base64 0.13.1", - "log", - "ring", - "sct 0.6.1", - "webpki 0.21.4", -] - [[package]] name = "rustls" version = "0.20.8" @@ -1521,8 +1374,8 @@ checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" dependencies = [ "log", "ring", - "sct 0.7.0", - "webpki 0.22.0", + "sct", + "webpki", ] [[package]] @@ -1531,7 +1384,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64 0.21.0", + "base64 0.21.2", ] [[package]] @@ -1552,22 +1405,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "scratch" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" - -[[package]] -name = "sct" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sct" version = "0.7.0" @@ -1587,39 +1424,24 @@ dependencies = [ "zeroize", ] -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "serde" -version = "1.0.160" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -1654,15 +1476,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" -dependencies = [ - "sha1_smol", -] - [[package]] name = "sha1" version = "0.10.5" @@ -1671,15 +1484,9 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] -[[package]] -name = "sha1_smol" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" - [[package]] name = "sha2" version = "0.9.9" @@ -1701,7 +1508,7 @@ checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -1762,17 +1569,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "sqlformat" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b7922be017ee70900be125523f38bdd644f4f06a1b16e8fa5a8ee8c34bffd4" -dependencies = [ - "itertools", - "nom", - "unicode_categories", -] - [[package]] name = "sqlformat" version = "0.2.1" @@ -1784,72 +1580,14 @@ dependencies = [ "unicode_categories", ] -[[package]] -name = "sqlx" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551873805652ba0d912fec5bbb0f8b4cdd96baf8e2ebf5970e5671092966019b" -dependencies = [ - "sqlx-core 0.5.13", - "sqlx-macros 0.5.13", -] - [[package]] name = "sqlx" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8de3b03a925878ed54a954f621e64bf55a3c1bd29652d0d1a17830405350188" dependencies = [ - "sqlx-core 0.6.3", - "sqlx-macros 0.6.3", -] - -[[package]] -name = "sqlx-core" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48c61941ccf5ddcada342cd59e3e5173b007c509e1e8e990dafc830294d9dc5" -dependencies = [ - "ahash", - "atoi 0.4.0", - "bitflags", - "byteorder", - "bytes", - "chrono", - "crc 2.1.0", - "crossbeam-queue", - "either", - "event-listener", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "hashlink 0.7.0", - "hex", - "indexmap", - "itoa", - "libc", - "libsqlite3-sys", - "log", - "memchr", - "once_cell", - "paste", - "percent-encoding", - "rustls 0.19.1", - "sha2 0.10.6", - "smallvec", - "sqlformat 0.1.8", - "sqlx-rt 0.5.13", - "stringprep", - "thiserror", - "time 0.2.27", - "tokio-stream", - "url", - "uuid 0.8.2", - "webpki 0.21.4", - "webpki-roots 0.21.1", + "sqlx-core", + "sqlx-macros", ] [[package]] @@ -1858,12 +1596,13 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" dependencies = [ - "ahash", - "atoi 1.0.0", + "ahash 0.7.6", + "atoi", "bitflags", "byteorder", "bytes", - "crc 3.0.1", + "chrono", + "crc", "crossbeam-queue", "dotenvy", "either", @@ -1874,7 +1613,7 @@ dependencies = [ "futures-executor", "futures-intrusive", "futures-util", - "hashlink 0.8.1", + "hashlink", "hex", "indexmap", "itoa", @@ -1885,36 +1624,19 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rustls 0.20.8", + "rustls", "rustls-pemfile", "sha2 0.10.6", "smallvec", - "sqlformat 0.2.1", - "sqlx-rt 0.6.3", + "sqlformat", + "sqlx-rt", "stringprep", "thiserror", + "time", "tokio-stream", "url", - "webpki-roots 0.22.6", -] - -[[package]] -name = "sqlx-macros" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0fba2b0cae21fc00fe6046f8baa4c7fcb49e379f0f592b04696607f69ed2e1" -dependencies = [ - "dotenv", - "either", - "heck", - "once_cell", - "proc-macro2", - "quote", - "sha2 0.10.6", - "sqlx-core 0.5.13", - "sqlx-rt 0.5.13", - "syn 1.0.109", - "url", + "uuid", + "webpki-roots", ] [[package]] @@ -1930,23 +1652,12 @@ dependencies = [ "proc-macro2", "quote", "sha2 0.10.6", - "sqlx-core 0.6.3", - "sqlx-rt 0.6.3", + "sqlx-core", + "sqlx-rt", "syn 1.0.109", "url", ] -[[package]] -name = "sqlx-rt" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4db708cd3e459078f85f39f96a00960bd841f66ee2a669e90bf36907f5a79aae" -dependencies = [ - "once_cell", - "tokio", - "tokio-rustls 0.22.0", -] - [[package]] name = "sqlx-rt" version = "0.6.3" @@ -1955,67 +1666,9 @@ checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" dependencies = [ "once_cell", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls", ] -[[package]] -name = "standback" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" -dependencies = [ - "version_check", -] - -[[package]] -name = "stdweb" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" -dependencies = [ - "discard", - "rustc_version", - "stdweb-derive", - "stdweb-internal-macros", - "stdweb-internal-runtime", - "wasm-bindgen", -] - -[[package]] -name = "stdweb-derive" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "serde_derive", - "syn 1.0.109", -] - -[[package]] -name = "stdweb-internal-macros" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" -dependencies = [ - "base-x", - "proc-macro2", - "quote", - "serde", - "serde_derive", - "serde_json", - "sha1 0.6.1", - "syn 1.0.109", -] - -[[package]] -name = "stdweb-internal-runtime" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" - [[package]] name = "stringprep" version = "0.1.2" @@ -2045,9 +1698,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", @@ -2060,15 +1713,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" version = "1.0.40" @@ -2086,7 +1730,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -2099,21 +1743,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "time" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" -dependencies = [ - "const_fn", - "libc", - "standback", - "stdweb", - "time-macros 0.1.1", - "version_check", - "winapi", -] - [[package]] name = "time" version = "0.3.21" @@ -2123,7 +1752,7 @@ dependencies = [ "itoa", "serde", "time-core", - "time-macros 0.2.9", + "time-macros", ] [[package]] @@ -2132,16 +1761,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" -[[package]] -name = "time-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" -dependencies = [ - "proc-macro-hack", - "time-macros-impl", -] - [[package]] name = "time-macros" version = "0.2.9" @@ -2151,19 +1770,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "time-macros-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" -dependencies = [ - "proc-macro-hack", - "proc-macro2", - "quote", - "standback", - "syn 1.0.109", -] - [[package]] name = "tinyvec" version = "1.6.0" @@ -2181,9 +1787,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.0" +version = "1.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f" +checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" dependencies = [ "autocfg", "bytes", @@ -2195,6 +1801,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.48.0", ] @@ -2206,18 +1813,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", -] - -[[package]] -name = "tokio-rustls" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" -dependencies = [ - "rustls 0.19.1", - "tokio", - "webpki 0.21.4", + "syn 2.0.18", ] [[package]] @@ -2226,9 +1822,9 @@ version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ - "rustls 0.20.8", + "rustls", "tokio", - "webpki 0.22.0", + "webpki", ] [[package]] @@ -2310,10 +1906,11 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.38" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9cf6a813d3f40c88b0b6b6f29a5c95c6cdbf97c1f9cc53fb820200f5ad814d" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ + "cfg-if 1.0.0", "log", "pin-project-lite", "tracing-attributes", @@ -2328,14 +1925,14 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", "valuable", @@ -2399,9 +1996,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-normalization" @@ -2418,12 +2015,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - [[package]] name = "unicode_categories" version = "0.1.1" @@ -2455,15 +2046,9 @@ checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" [[package]] name = "uuid" -version = "0.8.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" - -[[package]] -name = "uuid" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b55a3fef2a1e3b3a00ce878640918820d3c51081576ac657d23af9fc7928fdb" +checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" dependencies = [ "getrandom", "serde", @@ -2505,9 +2090,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -2515,24 +2100,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.18", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2540,43 +2125,33 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.18", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" dependencies = [ "js-sys", "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki" version = "0.22.0" @@ -2587,22 +2162,13 @@ dependencies = [ "untrusted", ] -[[package]] -name = "webpki-roots" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" -dependencies = [ - "webpki 0.21.4", -] - [[package]] name = "webpki-roots" version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" dependencies = [ - "webpki 0.22.0", + "webpki", ] [[package]] @@ -2621,15 +2187,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2784,6 +2341,7 @@ dependencies = [ "argon2", "askama", "askama_axum", + "async-session", "axum", "axum-login", "axum-macros", @@ -2791,7 +2349,7 @@ dependencies = [ "password-hash", "rand_core", "serde", - "sqlx 0.5.13", + "sqlx", "thiserror", "tokio", "tower", @@ -2800,7 +2358,7 @@ dependencies = [ "tracing-subscriber", "unicode-segmentation", "urlencoding", - "uuid 1.3.1", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 49fb315..6d27745 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,23 +4,25 @@ version = "0.0.1" edition = "2021" [dependencies] -axum = { version = "0.6", features = ["macros", "tracing"] } +axum = { version = "0.6", features = ["macros", "headers"] } askama = { version = "0.12", features = ["with-axum"] } askama_axum = "0.3" axum-macros = "0.3" -tokio = { version = "1", features = ["full"] } +tokio = { version = "1", features = ["full", "tracing"], default-features = false } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tower = { version = "0.4", features = ["util", "timeout"] } +tower = { version = "0.4", features = ["util", "timeout"], default-features = false } tower-http = { version = "0.4", features = ["add-extension", "trace"] } -uuid = { version = "1.3", features = ["serde", "v4"] } +uuid = { version = "1", features = ["serde", "v4"] } serde = { version = "1", features = ["derive"] } -sqlx = { version = "0.5.10", features = ["runtime-tokio-rustls", "any", "sqlite", "chrono", "time", "uuid"] } +sqlx = { version = "0.6", default-features = false, features = ["runtime-tokio-rustls", "any", "sqlite", "chrono", "time", "uuid"] } argon2 = "0.5" rand_core = { version = "0.6", features = ["getrandom"] } -thiserror = "1.0.40" -justerror = "1.1.0" -password-hash = { version = "0.5.0", features = ["std", "getrandom"] } -axum-login = { version = "0.5.0", features = ["sqlite", "sqlx"] } -unicode-segmentation = "1.10.1" -urlencoding = "2.1.2" +thiserror = "1" +justerror = "1" +password-hash = { version = "0.5", features = ["std", "getrandom"] } +axum-login = { version = "0.5", features = ["sqlite", "sqlx"] } +unicode-segmentation = "1" +urlencoding = "2" +async-session = "3" + diff --git a/src/db.rs b/src/db.rs index c9a55d9..e3ccc45 100644 --- a/src/db.rs +++ b/src/db.rs @@ -20,12 +20,12 @@ pub async fn get_pool() -> SqlitePool { let conn_opts = SqliteConnectOptions::new() .foreign_keys(true) .auto_vacuum(sqlx::sqlite::SqliteAutoVacuum::Incremental) - .filename(&db_filename); + .filename(&db_filename) + .busy_timeout(Duration::from_secs(TIMEOUT)); // setup connection pool SqlitePoolOptions::new() .max_connections(MAX_CONNS) - .connect_timeout(Duration::from_secs(TIMEOUT)) .connect_with(conn_opts) .await .expect("can't connect to database") diff --git a/src/form.rs b/src/form.rs deleted file mode 100644 index a8b7842..0000000 --- a/src/form.rs +++ /dev/null @@ -1,54 +0,0 @@ -use axum::{extract::Form, response::Html}; -use serde::Deserialize; - -pub(crate) async fn show_form() -> Html<&'static str> { - Html( - r#" - - - - -
- - - - - -
- - - "#, - ) -} - -#[derive(Deserialize, Debug)] -#[allow(dead_code)] -pub(crate) struct Input { - name: String, - email: String, -} - -pub(crate) async fn accept_form(Form(input): Form) -> Html { - let Input { name, email: _ } = input; - let html = format!( - r#" - - - - -

Hi, {}

- - - -"#, - name - ); - - Html(html) -} diff --git a/src/generic_handlers.rs b/src/generic_handlers.rs new file mode 100644 index 0000000..1e21dc2 --- /dev/null +++ b/src/generic_handlers.rs @@ -0,0 +1,15 @@ +use axum::response::{IntoResponse, Redirect}; + +use crate::AuthContext; + +pub async fn handle_slash_redir() -> impl IntoResponse { + Redirect::temporary("/") +} + +pub async fn handle_slash(auth: AuthContext) -> impl IntoResponse { + if let Some(user) = auth.current_user { + tracing::debug!("Logged in as: {user}"); + } else { + tracing::debug!("Not logged in.") + } +} diff --git a/src/handlers.rs b/src/handlers.rs deleted file mode 100644 index 5ba9bd4..0000000 --- a/src/handlers.rs +++ /dev/null @@ -1,55 +0,0 @@ -use axum::{ - async_trait, - extract::{FromRef, FromRequestParts, State}, - http::{request::Parts, StatusCode}, -}; -use sqlx::SqlitePool; - -pub async fn using_connection_pool_extractor( - State(pool): State, -) -> Result { - sqlx::query_scalar("select 'hello world from sqlite get'") - .fetch_one(&pool) - .await - .map_err(internal_error) -} - -// we can also write a custom extractor that grabs a connection from the pool -// which setup is appropriate depends on your application -pub struct DatabaseConnection(sqlx::pool::PoolConnection); - -#[async_trait] -impl FromRequestParts for DatabaseConnection -where - SqlitePool: FromRef, - S: Send + Sync, -{ - type Rejection = (StatusCode, String); - - async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result { - let pool = SqlitePool::from_ref(state); - - let conn = pool.acquire().await.map_err(internal_error)?; - - Ok(Self(conn)) - } -} - -pub async fn using_connection_extractor( - DatabaseConnection(conn): DatabaseConnection, -) -> Result { - let mut conn = conn; - sqlx::query_scalar("select 'hello world from sqlite post'") - .fetch_one(&mut conn) - .await - .map_err(internal_error) -} - -/// Utility function for mapping any error into a `500 Internal Server Error` -/// response. -fn internal_error(err: E) -> (StatusCode, String) -where - E: std::error::Error, -{ - (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) -} diff --git a/src/lib.rs b/src/lib.rs index f97526d..bc43291 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,17 @@ #[macro_use] extern crate justerror; +use axum_login::SqliteStore; +pub use users::User; +use uuid::Uuid; + pub mod db; -pub mod handlers; +pub mod generic_handlers; +pub mod login; +pub mod session_store; +pub mod signup; pub(crate) mod templates; pub mod users; +pub(crate) mod util; + +pub type AuthContext = axum_login::extractors::AuthContext>; diff --git a/src/login.rs b/src/login.rs new file mode 100644 index 0000000..d9d3603 --- /dev/null +++ b/src/login.rs @@ -0,0 +1,107 @@ +use argon2::{ + password_hash::{PasswordHash, PasswordVerifier}, + Argon2, +}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Redirect, Response}, + Form, +}; +use sqlx::SqlitePool; + +use crate::{ + templates::{LoginGet, LoginPost}, + util::form_decode, + AuthContext, User, +}; + +//-************************************************************************ +// Constants +//-************************************************************************ + +const LAST_SEEN_QUERY: &str = "update witches set last_seen = (select unixepoch()) where id = $1"; + +//-************************************************************************ +// Login error and success types +//-************************************************************************ + +#[Error] +pub struct LoginError(#[from] LoginErrorKind); + +#[Error] +#[non_exhaustive] +pub enum LoginErrorKind { + Internal, + BadPassword, + BadUsername, + Unknown, +} + +impl IntoResponse for LoginError { + fn into_response(self) -> Response { + match self.0 { + LoginErrorKind::Unknown => ( + StatusCode::INTERNAL_SERVER_ERROR, + "An unknown error occurred; you cursed, brah?", + ) + .into_response(), + _ => (StatusCode::BAD_REQUEST, format!("{self}")).into_response(), + } + } +} + +//-************************************************************************ +// Login handlers +//-************************************************************************ + +/// Handle login queries +#[axum::debug_handler] +pub async fn post_login( + mut auth: AuthContext, + State(pool): State, + Form(login): Form, +) -> Result { + let username = form_decode(&login.username, LoginErrorKind::BadUsername)?; + let username = username.trim(); + + let pw = form_decode(&login.password, LoginErrorKind::BadPassword)?; + let pw = pw.trim(); + + let user = User::get(username, &pool) + .await + .map_err(|_| LoginErrorKind::Unknown)?; + + let verifier = Argon2::default(); + let hash = PasswordHash::new(&user.pwhash).map_err(|_| LoginErrorKind::Internal)?; + match verifier.verify_password(pw.as_bytes(), &hash) { + Ok(_) => { + // log them in and set a session cookie + auth.login(&user) + .await + .map_err(|_| LoginErrorKind::Internal)?; + + // update last_seen; maybe this is ok to fail? + sqlx::query(LAST_SEEN_QUERY) + .bind(user.id) + .execute(&pool) + .await + .map_err(|_| LoginErrorKind::Internal)?; + + Ok(Redirect::temporary("/")) + } + _ => Err(LoginErrorKind::BadPassword.into()), + } +} + +pub async fn get_login() -> impl IntoResponse { + LoginGet::default() +} + +pub async fn get_logout() -> impl IntoResponse { + todo!() +} + +pub async fn post_logout() -> impl IntoResponse { + todo!() +} diff --git a/src/main.rs b/src/main.rs index 51e79c6..39903e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,16 @@ use std::net::SocketAddr; use axum::{routing::get, Router}; +use axum_login::{axum_sessions::SessionLayer, AuthLayer, SqliteStore}; +use rand_core::{OsRng, RngCore}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use witch_watch::{ db, - users::{get_create_user, handle_signup_success, post_create_user}, + generic_handlers::{handle_slash, handle_slash_redir}, + login::{get_login, get_logout, post_login, post_logout}, + session_store::SqliteSessionStore, + signup::{get_create_user, handle_signup_success, post_create_user}, + User, }; #[tokio::main] @@ -19,13 +25,37 @@ async fn main() { let pool = db::get_pool().await; - // build our application with some routes + let secret = { + let mut bytes = [0u8; 128]; + let mut rng = OsRng; + rng.fill_bytes(&mut bytes); + bytes + }; + + let session_layer = { + let store = SqliteSessionStore::from_client(pool.clone()); + store.migrate().await.expect("Could not migrate session DB"); + SessionLayer::new(store, &secret).with_secure(true) + }; + + let auth_layer = { + const QUERY: &str = "select * from witches where id = $1"; + let store = SqliteStore::::new(pool.clone()).with_query(QUERY); + AuthLayer::new(store, &secret) + }; + let app = Router::new() + .route("/", get(handle_slash).post(handle_slash)) .route("/signup", get(get_create_user).post(post_create_user)) .route( "/signup_success/:id", get(handle_signup_success).post(handle_signup_success), ) + .route("/login", get(get_login).post(post_login)) + .route("/logout", get(get_logout).post(post_logout)) + .fallback(handle_slash_redir) + .layer(auth_layer) + .layer(session_layer) .with_state(pool); tracing::debug!("binding to 0.0.0.0:3000"); diff --git a/src/session_store.rs b/src/session_store.rs new file mode 100644 index 0000000..b97768b --- /dev/null +++ b/src/session_store.rs @@ -0,0 +1,507 @@ +use async_session::{async_trait, chrono::Utc, log, serde_json, Result, Session, SessionStore}; +use sqlx::{pool::PoolConnection, sqlite::SqlitePool, Sqlite}; + +// NOTE! This code was straight stolen from +// https://github.com/jbr/async-sqlx-session/blob/30d00bed44ab2034082698f098eba48b21600f36/src/sqlite.rs +// and used under the terms of the MIT license: + +/* +Copyright 2022 Jacob Rothstein + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the “Software”), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/// sqlx sqlite session store for async-sessions +/// +/// ```rust +/// use witch_watch::session_store::SqliteSessionStore; +/// use async_session::{Session, SessionStore, Result}; +/// use std::time::Duration; +/// +/// # #[tokio::main] +/// # async fn main() -> Result { +/// let store = SqliteSessionStore::new("sqlite::memory:").await?; +/// store.migrate().await?; +/// +/// let mut session = Session::new(); +/// session.insert("key", vec![1,2,3]); +/// +/// let cookie_value = store.store_session(session).await?.unwrap(); +/// let session = store.load_session(cookie_value).await?.unwrap(); +/// assert_eq!(session.get::>("key").unwrap(), vec![1,2,3]); +/// # Ok(()) } + +#[derive(Clone, Debug)] +pub struct SqliteSessionStore { + client: SqlitePool, + table_name: String, +} + +impl SqliteSessionStore { + /// constructs a new SqliteSessionStore from an existing + /// sqlx::SqlitePool. the default table name for this session + /// store will be "async_sessions". To override this, chain this + /// with [`with_table_name`](crate::SqliteSessionStore::with_table_name). + /// + /// ```rust + /// # use witch_watch::session_store::SqliteSessionStore; + /// # use async_session::Result; + /// # #[tokio::main] + /// # async fn main() -> Result { + /// let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap(); + /// let store = SqliteSessionStore::from_client(pool) + /// .with_table_name("custom_table_name"); + /// store.migrate().await; + /// # Ok(()) } + /// ``` + pub fn from_client(client: SqlitePool) -> Self { + Self { + client, + table_name: "async_sessions".into(), + } + } + + /// Constructs a new SqliteSessionStore from a sqlite: database url. note + /// that this documentation uses the special `:memory:` sqlite + /// database for convenient testing, but a real application would + /// use a path like `sqlite:///path/to/database.db`. The default + /// table name for this session store will be "async_sessions". To + /// override this, either chain with + /// [`with_table_name`](crate::SqliteSessionStore::with_table_name) or + /// use + /// [`new_with_table_name`](crate::SqliteSessionStore::new_with_table_name) + /// + /// ```rust + /// # use witch_watch::session_store::SqliteSessionStore; + /// # use async_session::Result; + /// # #[tokio::main] + /// # async fn main() -> Result { + /// let store = SqliteSessionStore::new("sqlite::memory:").await?; + /// store.migrate().await; + /// # Ok(()) } + /// ``` + pub async fn new(database_url: &str) -> sqlx::Result { + Ok(Self::from_client(SqlitePool::connect(database_url).await?)) + } + + /// constructs a new SqliteSessionStore from a sqlite: database url. the + /// default table name for this session store will be + /// "async_sessions". To override this, either chain with + /// [`with_table_name`](crate::SqliteSessionStore::with_table_name) or + /// use + /// [`new_with_table_name`](crate::SqliteSessionStore::new_with_table_name) + /// + /// ```rust + /// # use witch_watch::session_store::SqliteSessionStore; + /// # use async_session::Result; + /// # #[tokio::main] + /// # async fn main() -> Result { + /// let store = SqliteSessionStore::new_with_table_name("sqlite::memory:", "custom_table_name").await?; + /// store.migrate().await; + /// # Ok(()) } + /// ``` + pub async fn new_with_table_name(database_url: &str, table_name: &str) -> sqlx::Result { + Ok(Self::new(database_url).await?.with_table_name(table_name)) + } + + /// Chainable method to add a custom table name. This will panic + /// if the table name is not `[a-zA-Z0-9_-]+`. + /// ```rust + /// # use witch_watch::session_store::SqliteSessionStore; + /// # use async_session::Result; + /// # #[tokio::main] + /// # async fn main() -> Result { + /// let store = SqliteSessionStore::new("sqlite::memory:").await? + /// .with_table_name("custom_name"); + /// store.migrate().await; + /// # Ok(()) } + /// ``` + /// + /// ```should_panic + /// # use witch_watch::session_store::SqliteSessionStore; + /// # use async_session::Result; + /// # #[tokio::main] + /// # async fn main() -> Result { + /// let store = SqliteSessionStore::new("sqlite::memory:").await? + /// .with_table_name("johnny (); drop users;"); + /// # Ok(()) } + /// ``` + pub fn with_table_name(mut self, table_name: impl AsRef) -> Self { + let table_name = table_name.as_ref(); + if table_name.is_empty() + || !table_name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') + { + panic!( + "table name must be [a-zA-Z0-9_-]+, but {} was not", + table_name + ); + } + + self.table_name = table_name.to_owned(); + self + } + + /// Creates a session table if it does not already exist. If it + /// does, this will noop, making it safe to call repeatedly on + /// store initialization. In the future, this may make + /// exactly-once modifications to the schema of the session table + /// on breaking releases. + /// ```rust + /// # use witch_watch::session_store::SqliteSessionStore; + /// # use async_session::{Result, SessionStore, Session}; + /// # #[tokio::main] + /// # async fn main() -> Result { + /// let store = SqliteSessionStore::new("sqlite::memory:").await?; + /// assert!(store.count().await.is_err()); + /// store.migrate().await?; + /// store.store_session(Session::new()).await?; + /// store.migrate().await?; // calling it a second time is safe + /// assert_eq!(store.count().await?, 1); + /// # Ok(()) } + /// ``` + pub async fn migrate(&self) -> sqlx::Result<()> { + log::info!("migrating sessions on `{}`", self.table_name); + + let mut conn = self.client.acquire().await?; + sqlx::query(&self.substitute_table_name( + r#" + CREATE TABLE IF NOT EXISTS %%TABLE_NAME%% ( + id TEXT PRIMARY KEY NOT NULL, + expires INTEGER NULL, + session TEXT NOT NULL + ) + "#, + )) + .execute(&mut conn) + .await?; + Ok(()) + } + + // private utility function because sqlite does not support + // parametrized table names + fn substitute_table_name(&self, query: &str) -> String { + query.replace("%%TABLE_NAME%%", &self.table_name) + } + + /// retrieve a connection from the pool + async fn connection(&self) -> sqlx::Result> { + self.client.acquire().await + } + + /// Performs a one-time cleanup task that clears out stale + /// (expired) sessions. You may want to call this from cron. + /// ```rust + /// # use witch_watch::session_store::SqliteSessionStore; + /// # use async_session::{chrono::{Utc,Duration}, Result, SessionStore, Session}; + /// # #[tokio::main] + /// # async fn main() -> Result { + /// let store = SqliteSessionStore::new("sqlite::memory:").await?; + /// store.migrate().await?; + /// let mut session = Session::new(); + /// session.set_expiry(Utc::now() - Duration::seconds(5)); + /// store.store_session(session).await?; + /// assert_eq!(store.count().await?, 1); + /// store.cleanup().await?; + /// assert_eq!(store.count().await?, 0); + /// # Ok(()) } + /// ``` + pub async fn cleanup(&self) -> sqlx::Result<()> { + let mut connection = self.connection().await?; + sqlx::query(&self.substitute_table_name( + r#" + DELETE FROM %%TABLE_NAME%% + WHERE expires < ? + "#, + )) + .bind(Utc::now().timestamp()) + .execute(&mut connection) + .await?; + + Ok(()) + } + + /// retrieves the number of sessions currently stored, including + /// expired sessions + /// + /// ```rust + /// # use witch_watch::session_store::SqliteSessionStore; + /// # use async_session::{Result, SessionStore, Session}; + /// # use std::time::Duration; + /// # #[tokio::main] + /// # async fn main() -> Result { + /// let store = SqliteSessionStore::new("sqlite::memory:").await?; + /// store.migrate().await?; + /// assert_eq!(store.count().await?, 0); + /// store.store_session(Session::new()).await?; + /// assert_eq!(store.count().await?, 1); + /// # Ok(()) } + /// ``` + pub async fn count(&self) -> sqlx::Result { + let (count,) = + sqlx::query_as(&self.substitute_table_name("SELECT COUNT(*) FROM %%TABLE_NAME%%")) + .fetch_one(&mut self.connection().await?) + .await?; + + Ok(count) + } +} + +#[async_trait] +impl SessionStore for SqliteSessionStore { + async fn load_session(&self, cookie_value: String) -> Result> { + let id = Session::id_from_cookie_value(&cookie_value)?; + let mut connection = self.connection().await?; + + let result: Option<(String,)> = sqlx::query_as(&self.substitute_table_name( + r#" + SELECT session FROM %%TABLE_NAME%% + WHERE id = ? AND (expires IS NULL OR expires > ?) + "#, + )) + .bind(&id) + .bind(Utc::now().timestamp()) + .fetch_optional(&mut connection) + .await?; + + Ok(result + .map(|(session,)| serde_json::from_str(&session)) + .transpose()?) + } + + async fn store_session(&self, session: Session) -> Result> { + let id = session.id(); + let string = serde_json::to_string(&session)?; + let mut connection = self.connection().await?; + + sqlx::query(&self.substitute_table_name( + r#" + INSERT INTO %%TABLE_NAME%% + (id, session, expires) VALUES (?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + expires = excluded.expires, + session = excluded.session + "#, + )) + .bind(id) + .bind(&string) + .bind(session.expiry().map(|expiry| expiry.timestamp())) + .execute(&mut connection) + .await?; + + Ok(session.into_cookie_value()) + } + + async fn destroy_session(&self, session: Session) -> Result { + let id = session.id(); + let mut connection = self.connection().await?; + sqlx::query(&self.substitute_table_name( + r#" + DELETE FROM %%TABLE_NAME%% WHERE id = ? + "#, + )) + .bind(id) + .execute(&mut connection) + .await?; + + Ok(()) + } + + async fn clear_store(&self) -> Result { + let mut connection = self.connection().await?; + sqlx::query(&self.substitute_table_name( + r#" + DELETE FROM %%TABLE_NAME%% + "#, + )) + .execute(&mut connection) + .await?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::*; + + async fn test_store() -> SqliteSessionStore { + let store = SqliteSessionStore::new("sqlite::memory:") + .await + .expect("building a sqlite :memory: SqliteSessionStore"); + store + .migrate() + .await + .expect("migrating a brand new :memory: SqliteSessionStore"); + store + } + + #[tokio::test] + async fn creating_a_new_session_with_no_expiry() -> Result { + let store = test_store().await; + let mut session = Session::new(); + session.insert("key", "value")?; + let cloned = session.clone(); + let cookie_value = store.store_session(session).await?.unwrap(); + + let (id, expires, serialized, count): (String, Option, String, i64) = + sqlx::query_as("select id, expires, session, count(*) from async_sessions") + .fetch_one(&mut store.connection().await?) + .await?; + + assert_eq!(1, count); + assert_eq!(id, cloned.id()); + assert_eq!(expires, None); + + let deserialized_session: Session = serde_json::from_str(&serialized)?; + assert_eq!(cloned.id(), deserialized_session.id()); + assert_eq!("value", &deserialized_session.get::("key").unwrap()); + + let loaded_session = store.load_session(cookie_value).await?.unwrap(); + assert_eq!(cloned.id(), loaded_session.id()); + assert_eq!("value", &loaded_session.get::("key").unwrap()); + + assert!(!loaded_session.is_expired()); + Ok(()) + } + + #[tokio::test] + async fn updating_a_session() -> Result { + let store = test_store().await; + let mut session = Session::new(); + let original_id = session.id().to_owned(); + + session.insert("key", "value")?; + let cookie_value = store.store_session(session).await?.unwrap(); + + let mut session = store.load_session(cookie_value.clone()).await?.unwrap(); + session.insert("key", "other value")?; + assert_eq!(None, store.store_session(session).await?); + + let session = store.load_session(cookie_value.clone()).await?.unwrap(); + assert_eq!(session.get::("key").unwrap(), "other value"); + + let (id, count): (String, i64) = sqlx::query_as("select id, count(*) from async_sessions") + .fetch_one(&mut store.connection().await?) + .await?; + + assert_eq!(1, count); + assert_eq!(original_id, id); + + Ok(()) + } + + #[tokio::test] + async fn updating_a_session_extending_expiry() -> Result { + let store = test_store().await; + let mut session = Session::new(); + session.expire_in(Duration::from_secs(10)); + let original_id = session.id().to_owned(); + let original_expires = session.expiry().unwrap().clone(); + let cookie_value = store.store_session(session).await?.unwrap(); + + let mut session = store.load_session(cookie_value.clone()).await?.unwrap(); + assert_eq!(session.expiry().unwrap(), &original_expires); + session.expire_in(Duration::from_secs(20)); + let new_expires = session.expiry().unwrap().clone(); + store.store_session(session).await?; + + let session = store.load_session(cookie_value.clone()).await?.unwrap(); + assert_eq!(session.expiry().unwrap(), &new_expires); + + let (id, expires, count): (String, i64, i64) = + sqlx::query_as("select id, expires, count(*) from async_sessions") + .fetch_one(&mut store.connection().await?) + .await?; + + assert_eq!(1, count); + assert_eq!(expires, new_expires.timestamp()); + assert_eq!(original_id, id); + + Ok(()) + } + + #[tokio::test] + async fn creating_a_new_session_with_expiry() -> Result { + let store = test_store().await; + let mut session = Session::new(); + session.expire_in(Duration::from_secs(1)); + session.insert("key", "value")?; + let cloned = session.clone(); + + let cookie_value = store.store_session(session).await?.unwrap(); + + let (id, expires, serialized, count): (String, Option, String, i64) = + sqlx::query_as("select id, expires, session, count(*) from async_sessions") + .fetch_one(&mut store.connection().await?) + .await?; + + assert_eq!(1, count); + assert_eq!(id, cloned.id()); + assert!(expires.unwrap() > Utc::now().timestamp()); + + let deserialized_session: Session = serde_json::from_str(&serialized)?; + assert_eq!(cloned.id(), deserialized_session.id()); + assert_eq!("value", &deserialized_session.get::("key").unwrap()); + + let loaded_session = store.load_session(cookie_value.clone()).await?.unwrap(); + assert_eq!(cloned.id(), loaded_session.id()); + assert_eq!("value", &loaded_session.get::("key").unwrap()); + + assert!(!loaded_session.is_expired()); + + tokio::time::sleep(Duration::from_secs(1)).await; + assert_eq!(None, store.load_session(cookie_value).await?); + + Ok(()) + } + + #[tokio::test] + async fn destroying_a_single_session() -> Result { + let store = test_store().await; + for _ in 0..3i8 { + store.store_session(Session::new()).await?; + } + + let cookie = store.store_session(Session::new()).await?.unwrap(); + assert_eq!(4, store.count().await?); + let session = store.load_session(cookie.clone()).await?.unwrap(); + store.destroy_session(session.clone()).await.unwrap(); + assert_eq!(None, store.load_session(cookie).await?); + assert_eq!(3, store.count().await?); + + // // attempting to destroy the session again is not an error + // assert!(store.destroy_session(session).await.is_ok()); + Ok(()) + } + + #[tokio::test] + async fn clearing_the_whole_store() -> Result { + let store = test_store().await; + for _ in 0..3i8 { + store.store_session(Session::new()).await?; + } + + assert_eq!(3, store.count().await?); + store.clear_store().await.unwrap(); + assert_eq!(0, store.count().await?); + + Ok(()) + } +} diff --git a/src/signup.rs b/src/signup.rs new file mode 100644 index 0000000..f32a47f --- /dev/null +++ b/src/signup.rs @@ -0,0 +1,220 @@ +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Argon2, +}; +use askama::Template; +use axum::{ + extract::{Form, Path, State}, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use sqlx::{query_as, SqlitePool}; +use unicode_segmentation::UnicodeSegmentation; +use uuid::Uuid; + +use crate::{templates::CreateUser, User}; + +const CREATE_QUERY: &str = + "insert into witches (id, username, displayname, email, pwhash) values ($1, $2, $3, $4, $5)"; +const ID_QUERY: &str = "select * from witches where id = $1"; + +//-************************************************************************ +// Result types for user creation +//-************************************************************************ + +#[derive(Debug, Clone, Template)] +#[template(path = "signup_success.html")] +pub struct CreateUserSuccess(User); + +#[Error(desc = "Could not create user.")] +#[non_exhaustive] +pub struct CreateUserError(#[from] CreateUserErrorKind); + +impl IntoResponse for CreateUserError { + fn into_response(self) -> askama_axum::Response { + match self.0 { + CreateUserErrorKind::UnknownDBError => { + (StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response() + } + _ => (StatusCode::BAD_REQUEST, format!("{self}")).into_response(), + } + } +} + +#[Error] +#[non_exhaustive] +pub enum CreateUserErrorKind { + AlreadyExists, + #[error(desc = "Usernames must be between 1 and 20 non-whitespace characters long")] + BadUsername, + PasswordMismatch, + #[error(desc = "Password must have at least 4 and at most 50 characters")] + BadPassword, + #[error(desc = "Display name must be less than 100 characters long")] + BadDisplayname, + BadEmail, + MissingFields, + UnknownDBError, +} + +//-************************************************************************ +// User creation route handlers +//-************************************************************************ + +/// Get Handler: displays the form to create a user +pub async fn get_create_user() -> CreateUser { + CreateUser::default() +} + +/// Post Handler: validates form values and calls the actual, private user +/// creation function +#[axum::debug_handler] +pub async fn post_create_user( + State(pool): State, + Form(signup): Form, +) -> Result { + let username = &signup.username; + let displayname = &signup.displayname; + let email = &signup.email; + let password = &signup.password; + let verify = &signup.pw_verify; + let username = username.trim(); + + let name_len = username.graphemes(true).size_hint().1.unwrap(); + // we are not ascii exclusivists around here + if !(1..=20).contains(&name_len) { + return Err(CreateUserErrorKind::BadUsername.into()); + } + + if password != verify { + return Err(CreateUserErrorKind::PasswordMismatch.into()); + } + + let password = urlencoding::decode(password) + .map_err(|_| CreateUserErrorKind::BadPassword)? + .to_string(); + let password = password.trim(); + let password = password.as_bytes(); + if !(4..=50).contains(&password.len()) { + return Err(CreateUserErrorKind::BadPassword.into()); + } + + let displayname = if let Some(dn) = displayname { + let dn = urlencoding::decode(dn) + .map_err(|_| CreateUserErrorKind::BadDisplayname)? + .to_string() + .trim() + .to_string(); + if dn.graphemes(true).size_hint().1.unwrap() > 100 { + return Err(CreateUserErrorKind::BadDisplayname.into()); + } + Some(dn) + } else { + None + }; + let displayname = &displayname; + + // TODO(2023-05-17): validate email + let email = if let Some(email) = email { + let email = urlencoding::decode(email) + .map_err(|_| CreateUserErrorKind::BadEmail)? + .to_string(); + Some(email) + } else { + None + }; + let email = &email; + + let user = create_user(username, displayname, email, password, &pool).await?; + tracing::debug!("created {user:?}"); + let id = user.id.as_simple().to_string(); + let location = format!("/signup_success/{id}"); + + let resp = axum::response::Redirect::temporary(&location); + + Ok(resp) +} + +/// Generic handler for successful signup +pub async fn handle_signup_success( + Path(id): Path, + State(pool): State, +) -> Response { + let id = id.trim(); + let user: User = { + let id = Uuid::try_parse(id).unwrap_or_default(); + query_as(ID_QUERY) + .bind(id) + .fetch_one(&pool) + .await + .unwrap_or_default() + }; + + let mut resp = CreateUserSuccess(user.clone()).into_response(); + + if user.username.is_empty() || id.is_empty() { + // redirect to front page if we got here without a valid witch ID + *resp.status_mut() = StatusCode::TEMPORARY_REDIRECT; + resp.headers_mut().insert("Location", "/".parse().unwrap()); + } + + resp +} + +//-************************************************************************ +// private fns +//-************************************************************************ + +async fn create_user( + username: &str, + displayname: &Option, + email: &Option, + password: &[u8], + pool: &SqlitePool, +) -> Result { + // Argon2 with default params (Argon2id v19) + let argon2 = Argon2::default(); + let salt = SaltString::generate(&mut OsRng); + let pwhash = argon2 + .hash_password(password, &salt) + .unwrap() // safe to unwrap, we know the salt is valid + .to_string(); + + let id = Uuid::new_v4(); + let res = sqlx::query(CREATE_QUERY) + .bind(id) + .bind(username) + .bind(displayname) + .bind(email) + .bind(&pwhash) + .execute(pool) + .await; + + match res { + Ok(_) => { + let user = User { + id, + username: username.to_string(), + displayname: displayname.to_owned(), + email: email.to_owned(), + last_seen: None, + pwhash, + }; + Ok(user) + } + Err(sqlx::Error::Database(db)) => { + if let Some(exit) = db.code() { + let exit = exit.parse().unwrap_or(0u32); + // https://www.sqlite.org/rescode.html codes for unique constraint violations: + if exit == 2067u32 || exit == 1555 { + Err(CreateUserErrorKind::AlreadyExists.into()) + } else { + Err(CreateUserErrorKind::UnknownDBError.into()) + } + } else { + Err(CreateUserErrorKind::UnknownDBError.into()) + } + } + _ => Err(CreateUserErrorKind::UnknownDBError.into()), + } +} diff --git a/src/templates.rs b/src/templates.rs index afbc50d..f8ad15f 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,7 +1,7 @@ use askama::Template; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Default, Template, Deserialize)] +#[derive(Debug, Default, Template, Deserialize, Serialize)] #[template(path = "signup.html")] pub struct CreateUser { pub username: String, @@ -10,3 +10,17 @@ pub struct CreateUser { pub password: String, pub pw_verify: String, } + +#[derive(Debug, Default, Template, Deserialize, Serialize)] +#[template(path = "login_post.html")] +pub struct LoginPost { + pub username: String, + pub password: String, +} + +#[derive(Debug, Default, Template, Deserialize, Serialize)] +#[template(path = "login_get.html")] +pub struct LoginGet { + pub username: String, + pub password: String, +} diff --git a/src/users.rs b/src/users.rs index a140739..4ef50fd 100644 --- a/src/users.rs +++ b/src/users.rs @@ -1,33 +1,19 @@ use std::fmt::Display; -use argon2::{ - password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, - Argon2, -}; -use askama::Template; -use axum::{ - extract::{Form, Path, State}, - http::StatusCode, - response::{IntoResponse, Response}, -}; -use sqlx::{sqlite::SqliteRow, Row, SqlitePool}; -use unicode_segmentation::UnicodeSegmentation; +use axum_login::{secrecy::SecretVec, AuthUser}; +use sqlx::SqlitePool; use uuid::Uuid; -use crate::templates::CreateUser; +const USERNAME_QUERY: &str = "select * from witches where username = $1"; -const CREATE_QUERY: &str = - "insert into witches (id, username, displayname, email, pwhash) values ($1, $2, $3, $4, $5)"; - -const ID_QUERY: &str = "select * from witches where id = $1"; - -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq, sqlx::FromRow)] pub struct User { - id: Uuid, - username: String, - displayname: Option, - email: Option, - last_seen: Option, + pub id: Uuid, + pub username: String, + pub displayname: Option, + pub email: Option, + pub last_seen: Option, + pub(crate) pwhash: String, } impl Display for User { @@ -43,208 +29,21 @@ impl Display for User { } } -#[derive(Debug, Clone, Template)] -#[template(path = "signup_success.html")] -pub struct CreateUserSuccess(User); +impl AuthUser for User { + fn get_id(&self) -> Uuid { + self.id + } -impl sqlx::FromRow<'_, SqliteRow> for User { - fn from_row(row: &SqliteRow) -> Result { - let bytes: Vec = row.get("id"); - let bytes = bytes.as_slice(); - let bytes: [u8; 16] = bytes.try_into().unwrap(); - let id = Uuid::from_bytes_le(bytes); - let username: String = row.get("username"); - let displayname: Option = row.get("displayname"); - let last_seen: Option = row.get("last_seen"); - let email: Option = row.get("email"); - - Ok(Self { - id, - username, - displayname, - email, - last_seen, - }) + fn get_password_hash(&self) -> SecretVec { + SecretVec::new(self.pwhash.as_bytes().to_vec()) } } -/// Get Handler: displays the form to create a user -pub async fn get_create_user() -> CreateUser { - CreateUser::default() -} - -/// Post Handler: validates form values and calls the actual, private user -/// creation function -#[axum::debug_handler] -pub async fn post_create_user( - State(pool): State, - Form(signup): Form, -) -> Result { - let username = &signup.username; - let displayname = &signup.displayname; - let email = &signup.email; - let password = &signup.password; - let verify = &signup.pw_verify; - let username = username.trim(); - - let name_len = username.graphemes(true).size_hint().1.unwrap(); - // we are not ascii exclusivists around here - if !(1..=20).contains(&name_len) { - return Err(CreateUserErrorKind::BadUsername.into()); - } - - if let Some(ref dn) = displayname { - if dn.len() > 50 { - return Err(CreateUserErrorKind::BadDisplayname.into()); - } - } - - if password != verify { - return Err(CreateUserErrorKind::PasswordMismatch.into()); - } - - let password = urlencoding::decode(password) - .map_err(|_| CreateUserErrorKind::BadPassword)? - .to_string(); - let password = password.as_bytes(); - - let displayname = if let Some(dn) = displayname { - let dn = urlencoding::decode(dn) - .map_err(|_| CreateUserErrorKind::BadDisplayname)? - .to_string(); - Some(dn) - } else { - None - }; - let displayname = &displayname; - - // TODO(2023-05-17): validate email - let email = if let Some(email) = email { - let email = urlencoding::decode(email) - .map_err(|_| CreateUserErrorKind::BadEmail)? - .to_string(); - Some(email) - } else { - None - }; - let email = &email; - - let user = create_user(username, displayname, email, password, &pool).await?; - tracing::debug!("created {user:?}"); - let id = user.id.simple().to_string(); - let location = format!("/signup_success/{id}"); - - let resp = axum::response::Redirect::temporary(&location).into_response(); - - Ok(resp) -} - -/// Get handler for successful signup -pub async fn handle_signup_success( - Path(id): Path, - State(pool): State, -) -> Response { - let user: User = { - let id = id; - let id = Uuid::try_parse(&id).unwrap_or_default(); - let id_bytes = id.to_bytes_le(); - sqlx::query_as(ID_QUERY) - .bind(id_bytes.as_slice()) - .fetch_one(&pool) +impl User { + pub async fn get(username: &str, db: &SqlitePool) -> Result { + sqlx::query_as(USERNAME_QUERY) + .bind(username) + .fetch_one(db) .await - .unwrap_or_default() - }; - - let mut resp = CreateUserSuccess(user.clone()).into_response(); - - if user.username.is_empty() { - // redirect to front page if we got here without a valid witch header - *resp.status_mut() = StatusCode::TEMPORARY_REDIRECT; - resp.headers_mut().insert("Location", "/".parse().unwrap()); - } - resp -} - -async fn create_user( - username: &str, - displayname: &Option, - email: &Option, - password: &[u8], - pool: &SqlitePool, -) -> Result { - // Argon2 with default params (Argon2id v19) - let argon2 = Argon2::default(); - let salt = SaltString::generate(&mut OsRng); - let pwhash = argon2 - .hash_password(password, &salt) - .unwrap() // safe to unwrap, we know the salt is valid - .to_string(); - - let id = Uuid::new_v4(); - let id_bytes = id.to_bytes_le(); - let id_bytes = id_bytes.as_slice(); - let res = sqlx::query(CREATE_QUERY) - .bind(id_bytes) - .bind(username) - .bind(displayname) - .bind(email) - .bind(pwhash) - .execute(pool) - .await; - - match res { - Ok(_) => { - let user = User { - id, - username: username.to_string(), - displayname: displayname.to_owned(), - email: email.to_owned(), - last_seen: None, - }; - Ok(user) - } - Err(sqlx::Error::Database(db)) => { - if let Some(exit) = db.code() { - let exit = exit.parse().unwrap_or(0u32); - // https://www.sqlite.org/rescode.html codes for unique constraint violations: - if exit == 2067u32 || exit == 1555 { - Err(CreateUserErrorKind::AlreadyExists.into()) - } else { - Err(CreateUserErrorKind::UnknownDBError.into()) - } - } else { - Err(CreateUserErrorKind::UnknownDBError.into()) - } - } - _ => Err(CreateUserErrorKind::UnknownDBError.into()), } } - -#[Error(desc = "Could not create user.")] -#[non_exhaustive] -pub struct CreateUserError(#[from] CreateUserErrorKind); - -impl IntoResponse for CreateUserError { - fn into_response(self) -> askama_axum::Response { - match self.0 { - CreateUserErrorKind::UnknownDBError => { - (StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response() - } - _ => (StatusCode::BAD_REQUEST, format!("{self}")).into_response(), - } - } -} - -#[Error] -#[non_exhaustive] -pub enum CreateUserErrorKind { - AlreadyExists, - #[error(desc = "Usernames must be between 1 and 20 non-whitespace characters long")] - BadUsername, - PasswordMismatch, - BadPassword, - BadDisplayname, - BadEmail, - MissingFields, - UnknownDBError, -} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..1b15b86 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,3 @@ +pub fn form_decode(input: &str, err: E) -> Result { + Ok(urlencoding::decode(input).map_err(|_| err)?.into_owned()) +} diff --git a/templates/login_get.html b/templates/login_get.html new file mode 100644 index 0000000..ea7a1a7 --- /dev/null +++ b/templates/login_get.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block title %}Login to Witch Watch, Bish{% endblock %} + +{% block content %} + +

+

+ +
+ +
+ +
+

+ +{% endblock %} diff --git a/templates/login_post.html b/templates/login_post.html new file mode 100644 index 0000000..e69de29