From 8f5bc070709410ae05c2898c0895810c57be7aa9 Mon Sep 17 00:00:00 2001 From: CanadianBaconBoi Date: Thu, 20 Aug 2026 19:08:36 +0200 Subject: [PATCH] sum bullshit --- .idea/dataSources.xml | 12 + Cargo.lock | 882 ++++++++++++++---- Cargo.toml | 48 +- assets/img/blinkiesCafe-HY.gif | Bin 0 -> 2584 bytes assets/img/blinkiesCafe-eX.gif | Bin 0 -> 7960 bytes assets/img/blinkiesCafe-gn.gif | Bin 0 -> 7719 bytes web/Cargo.toml | 28 +- web/assets/img/blinkiesCafe-HY.gif | Bin 0 -> 2584 bytes web/assets/img/blinkiesCafe-eX.gif | Bin 0 -> 7960 bytes web/assets/img/blinkiesCafe-gn.gif | Bin 0 -> 7719 bytes web/assets/img/github-mark.svg | 3 + web/build.rs | 5 + .../20260820090931_create_users.down.sql | 2 + .../20260820090931_create_users.up.sql | 11 + ...20260820102145_user_confirmations.down.sql | 3 + .../20260820102145_user_confirmations.up.sql | 12 + web/src/web/content/partials/footer.rs | 91 +- web/src/web/content/partials/header.rs | 76 +- web/src/web/content/partials/page.rs | 96 +- web/src/web/content/style/css/base.css | 36 +- web/src/web/mod.rs | 2 +- web/src/web/service/auth/confirmation.rs | 106 +++ web/src/web/service/auth/email.rs | 20 + web/src/web/service/auth/hash.rs | 59 ++ web/src/web/service/auth/mod.rs | 37 +- web/src/web/service/auth/session.rs | 21 + web/src/web/service/auth/user.rs | 67 +- web/src/web/service/extractors/authuser.rs | 40 + web/src/web/service/extractors/mod.rs | 3 +- web/src/web/service/mod.rs | 21 +- web/src/web/service/routes/auth/confirm.rs | 95 ++ web/src/web/service/routes/auth/login.rs | 47 +- .../web/service/routes/auth/loginprompt.rs | 16 +- web/src/web/service/routes/auth/logout.rs | 21 +- .../web/service/routes/auth/logoutprompt.rs | 10 +- web/src/web/service/routes/auth/mod.rs | 4 +- web/src/web/service/routes/auth/register.rs | 64 +- .../web/service/routes/auth/registerprompt.rs | 26 +- web/src/web/service/routes/mod.rs | 82 +- web/src/web/service/routes/root.rs | 5 +- 40 files changed, 1733 insertions(+), 318 deletions(-) create mode 100644 .idea/dataSources.xml create mode 100644 assets/img/blinkiesCafe-HY.gif create mode 100644 assets/img/blinkiesCafe-eX.gif create mode 100644 assets/img/blinkiesCafe-gn.gif create mode 100644 web/assets/img/blinkiesCafe-HY.gif create mode 100644 web/assets/img/blinkiesCafe-eX.gif create mode 100644 web/assets/img/blinkiesCafe-gn.gif create mode 100644 web/assets/img/github-mark.svg create mode 100644 web/build.rs create mode 100644 web/migrations/20260820090931_create_users.down.sql create mode 100644 web/migrations/20260820090931_create_users.up.sql create mode 100644 web/migrations/20260820102145_user_confirmations.down.sql create mode 100644 web/migrations/20260820102145_user_confirmations.up.sql create mode 100644 web/src/web/service/auth/confirmation.rs create mode 100644 web/src/web/service/auth/email.rs create mode 100644 web/src/web/service/auth/hash.rs create mode 100644 web/src/web/service/auth/session.rs create mode 100644 web/src/web/service/extractors/authuser.rs create mode 100644 web/src/web/service/routes/auth/confirm.rs diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..bec987f --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/postgres + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d0c97b7..8211efe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,10 +63,22 @@ dependencies = [ ] [[package]] -name = "async-compression" -version = "0.4.42" +name = "argon2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", +] + +[[package]] +name = "async-compression" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" dependencies = [ "compression-codecs", "compression-core", @@ -192,25 +204,6 @@ dependencies = [ "http", ] -[[package]] -name = "axum-login" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964ea6eb764a227baa8c3368e45c94d23b6863cc7b880c6c9e341c143c5a5ff7" -dependencies = [ - "axum", - "form_urlencoded", - "serde", - "subtle", - "thiserror", - "tower-cookies", - "tower-layer", - "tower-service", - "tower-sessions 0.14.0", - "tracing", - "urlencoding", -] - [[package]] name = "axum-macros" version = "0.5.1" @@ -228,6 +221,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64-simd" version = "0.7.0" @@ -237,6 +236,12 @@ dependencies = [ "simd-abstraction", ] +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "2.13.1" @@ -258,6 +263,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -370,12 +384,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cmov" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" - [[package]] name = "combine" version = "4.6.7" @@ -403,6 +411,12 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const-oid" version = "0.10.2" @@ -609,15 +623,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ctutils" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" -dependencies = [ - "cmov", -] - [[package]] name = "dashmap" version = "5.5.3" @@ -631,6 +636,20 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.11.0" @@ -646,6 +665,17 @@ dependencies = [ "matches", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -685,7 +715,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid 0.9.6", "crypto-common 0.1.6", + "subtle", ] [[package]] @@ -695,9 +727,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.1", - "const-oid", + "const-oid 0.10.2", "crypto-common 0.2.2", - "ctutils", ] [[package]] @@ -774,12 +805,13 @@ dependencies = [ [[package]] name = "etcetera" -version = "0.11.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" dependencies = [ "cfg-if", - "windows-sys 0.61.2", + "home", + "windows-sys 0.48.0", ] [[package]] @@ -825,9 +857,9 @@ dependencies = [ [[package]] name = "flume" -version = "0.12.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", @@ -840,6 +872,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -855,6 +893,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "forwarded-header-value" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" +dependencies = [ + "nonempty", + "thiserror 1.0.69", +] + [[package]] name = "fred" version = "10.1.0" @@ -986,6 +1034,12 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + [[package]] name = "futures-util" version = "0.3.33" @@ -1033,9 +1087,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1052,6 +1108,29 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "governor" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8" +dependencies = [ + "cfg-if", + "dashmap 6.2.1", + "futures-sink", + "futures-timer", + "futures-util", + "getrandom 0.3.4", + "hashbrown 0.16.1", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "rand 0.9.5", + "smallvec", + "spinning_top", + "web-time", +] + [[package]] name = "h2" version = "0.4.15" @@ -1086,6 +1165,17 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -1094,7 +1184,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -1105,11 +1195,11 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" -version = "0.11.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.16.1", + "hashbrown 0.15.5", ] [[package]] @@ -1126,20 +1216,29 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hkdf" -version = "0.13.0" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ "hmac", ] [[package]] name = "hmac" -version = "0.13.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.11.3", + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", ] [[package]] @@ -1231,7 +1330,20 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 1.0.9", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", ] [[package]] @@ -1240,7 +1352,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -1416,7 +1528,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror", + "thiserror 2.0.19", "walkdir", "windows-link", ] @@ -1479,6 +1591,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -1487,10 +1602,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] -name = "libsqlite3-sys" -version = "0.37.0" +name = "libm" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.9.2", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "pkg-config", "vcpkg", @@ -1507,7 +1640,7 @@ dependencies = [ "const-str", "cssparser", "cssparser-color", - "dashmap", + "dashmap 5.5.3", "data-encoding", "getrandom 0.3.4", "indexmap", @@ -1564,7 +1697,7 @@ checksum = "4543dd90f92818d8a73ba91ab02138bb6751689113e79ddd75130f3f7579883d" dependencies = [ "async-trait", "serde", - "thiserror", + "thiserror 2.0.19", ] [[package]] @@ -1636,12 +1769,12 @@ dependencies = [ [[package]] name = "md-5" -version = "0.11.0" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest 0.11.3", + "digest 0.10.7", ] [[package]] @@ -1665,10 +1798,10 @@ name = "microtld-web" version = "0.1.0" dependencies = [ "anyhow", - "async-trait", + "argon2", "axum", "axum-htmx", - "axum-login", + "base64 0.23.1", "fred", "inventory", "lightningcss", @@ -1678,13 +1811,21 @@ dependencies = [ "reqwest 0.13.4", "rust-embed", "serde", + "sha2 0.11.0", "sqlx", + "subtle", + "time", "tokio", "tower", + "tower-csrf", "tower-http 0.7.0", - "tower-sessions 0.15.0", + "tower-sessions", + "tower-sessions-sqlx-store", + "tower_governor", "tracing", "tracing-subscriber", + "urlencoding", + "uuid", ] [[package]] @@ -1740,6 +1881,18 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonempty" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1749,12 +1902,47 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.7", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1762,6 +1950,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1836,11 +2025,22 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pastey" version = "0.1.1" @@ -1853,6 +2053,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1940,18 +2149,71 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + [[package]] name = "potential_utf" version = "0.1.5" @@ -2023,6 +2285,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quinn" version = "0.11.11" @@ -2037,7 +2314,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2 0.6.5", - "thiserror", + "thiserror 2.0.19", "tokio", "tracing", "web-time", @@ -2060,7 +2337,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.19", "tinyvec", "tracing", "web-time", @@ -2192,6 +2469,15 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags", +] + [[package]] name = "rayon" version = "1.12.0" @@ -2235,6 +2521,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c93da5bb2c5d4e6c0ef7abeead62c89169a0a4882bfb83ac892f2423aea2fe" +dependencies = [ + "bitflags", +] + [[package]] name = "regex-automata" version = "0.4.16" @@ -2267,7 +2562,7 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "http", @@ -2296,7 +2591,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 1.0.9", ] [[package]] @@ -2305,7 +2600,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -2380,6 +2675,45 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rust-embed" version = "8.12.0" @@ -2654,13 +2988,13 @@ dependencies = [ [[package]] name = "sha1" -version = "0.11.0" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -2710,6 +3044,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "simd-abstraction" version = "0.7.1" @@ -2792,10 +3136,29 @@ dependencies = [ ] [[package]] -name = "sqlx" -version = "0.9.0" +name = "spinning_top" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2806,13 +3169,12 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.9.0" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "base64", + "base64 0.22.1", "bytes", - "cfg-if", "crc", "crossbeam-queue", "either", @@ -2821,28 +3183,33 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.16.1", + "hashbrown 0.15.5", "hashlink", "indexmap", "log", "memchr", + "once_cell", "percent-encoding", + "rustls", "serde", "serde_json", "sha2 0.10.9", "smallvec", - "thiserror", + "thiserror 2.0.19", + "time", "tokio", "tokio-stream", "tracing", "url", + "uuid", + "webpki-roots 0.26.11", ] [[package]] name = "sqlx-macros" -version = "0.9.0" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", @@ -2853,15 +3220,15 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.9.0" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ - "cfg-if", "dotenvy", "either", "heck", "hex", + "once_cell", "proc-macro2", "quote", "serde", @@ -2872,45 +3239,62 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn 2.0.119", - "thiserror", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.9.0" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ + "atoi", + "base64 0.22.1", "bitflags", "byteorder", "bytes", "crc", - "digest 0.11.3", + "digest 0.10.7", "dotenvy", "either", + "futures-channel", "futures-core", + "futures-io", "futures-util", "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", "log", + "md-5", + "memchr", + "once_cell", "percent-encoding", + "rand 0.8.7", + "rsa", "serde", "sha1", - "sha2 0.11.0", + "sha2 0.10.9", + "smallvec", "sqlx-core", - "thiserror", + "stringprep", + "thiserror 2.0.19", + "time", "tracing", + "uuid", + "whoami", ] [[package]] name = "sqlx-postgres" -version = "0.9.0" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "crc", @@ -2922,31 +3306,34 @@ dependencies = [ "hex", "hkdf", "hmac", + "home", "itoa", "log", "md-5", "memchr", - "rand 0.10.2", + "once_cell", + "rand 0.8.7", "serde", "serde_json", - "sha2 0.11.0", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.19", + "time", "tracing", + "uuid", "whoami", ] [[package]] name = "sqlx-sqlite" -version = "0.9.0" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "flume", - "form_urlencoded", "futures-channel", "futures-core", "futures-executor", @@ -2956,10 +3343,13 @@ dependencies = [ "log", "percent-encoding", "serde", + "serde_urlencoded", "sqlx-core", - "thiserror", + "thiserror 2.0.19", + "time", "tracing", "url", + "uuid", ] [[package]] @@ -3065,13 +3455,33 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -3212,6 +3622,35 @@ dependencies = [ "tokio", ] +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2 0.6.5", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.3" @@ -3220,9 +3659,12 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", + "indexmap", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -3244,6 +3686,19 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-csrf" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38be628f9ef990a4714803c31a9ecc45b20e4a11401ec9121a8109de541a4aa" +dependencies = [ + "http", + "thiserror 2.0.19", + "tower", + "tracing", + "url", +] + [[package]] name = "tower-http" version = "0.6.11" @@ -3308,24 +3763,7 @@ dependencies = [ "tower-cookies", "tower-layer", "tower-service", - "tower-sessions-core 0.14.0", - "tracing", -] - -[[package]] -name = "tower-sessions" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518dca34b74a17cadfcee06e616a09d2bd0c3984eff1769e1e76d58df978fc78" -dependencies = [ - "async-trait", - "http", - "time", - "tokio", - "tower-cookies", - "tower-layer", - "tower-service", - "tower-sessions-core 0.15.0", + "tower-sessions-core", "tower-sessions-memory-store", "tracing", ] @@ -3337,35 +3775,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b" dependencies = [ "async-trait", - "base64", + "axum-core", + "base64 0.22.1", "futures", "http", "parking_lot", "rand 0.8.7", "serde", "serde_json", - "thiserror", - "time", - "tokio", - "tracing", -] - -[[package]] -name = "tower-sessions-core" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "568531ec3dfcf3ffe493de1958ae5662a0284ac5d767476ecdb6a34ff8c6b06c" -dependencies = [ - "async-trait", - "axum-core", - "base64", - "futures", - "http", - "parking_lot", - "rand 0.9.5", - "serde", - "serde_json", - "thiserror", + "thiserror 2.0.19", "time", "tokio", "tracing", @@ -3373,14 +3791,45 @@ dependencies = [ [[package]] name = "tower-sessions-memory-store" -version = "0.15.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713fabf882b6560a831e2bbed6204048b35bdd60e50bbb722902c74f8df33460" +checksum = "fb05909f2e1420135a831dd5df9f5596d69196d0a64c3499ca474c4bd3d33242" dependencies = [ "async-trait", "time", "tokio", - "tower-sessions-core 0.15.0", + "tower-sessions-core", +] + +[[package]] +name = "tower-sessions-sqlx-store" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e054622079f57fc1a7d6a6089c9334f963d62028fe21dc9eddd58af9a78480b3" +dependencies = [ + "async-trait", + "rmp-serde", + "sqlx", + "thiserror 1.0.69", + "time", + "tower-sessions-core", +] + +[[package]] +name = "tower_governor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44de9b94d849d3c46e06a883d72d408c2de6403367b39df2b1c9d9e7b6736fe6" +dependencies = [ + "axum", + "forwarded-header-value", + "governor", + "http", + "pin-project", + "thiserror 2.0.19", + "tonic", + "tower", + "tracing", ] [[package]] @@ -3538,7 +3987,9 @@ version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ + "getrandom 0.4.3", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -3600,6 +4051,12 @@ 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.126" @@ -3684,6 +4141,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.9", +] + [[package]] name = "webpki-roots" version = "1.0.9" @@ -3695,9 +4161,29 @@ dependencies = [ [[package]] name = "whoami" -version = "2.1.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" @@ -3708,6 +4194,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" @@ -3743,13 +4235,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3761,34 +4262,67 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3801,24 +4335,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index fbfc0d5..5147b8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,24 +11,44 @@ edition = "2024" microtld-web = {path = "web"} microtld-backend = {path = "backend"} -tokio = {version = "1.53.1", features = ["full"]} -lmrc-cloudflare = {version = "0.3.16"} -anyhow = {version = "1.0.104"} -serde = {version = "1.0.229", features = ["derive"]} -derive_more = {version = "2.1.1", default-features = false, features = ["display"]} +tokio = {version = "1.53", features = ["full"]} +lmrc-cloudflare = {version = "0.3"} +anyhow = {version = "1.0"} +serde = {version = "1.0", features = ["derive"]} +derive_more = {version = "2.1", default-features = false, features = ["display"]} -axum = {version = "0.8.9", features = ["macros"]} -axum-htmx = {version = "0.8.1"} -axum-login = {version = "0.18.0"} -tower-sessions = {version = "0.15.0"} -fred = {version = "10.1.0"} -tracing = {version = "0.1.44"} -reqwest = {version = "0.13.4"} -maud = {version = "0.27.0", features = ["axum"]} -sqlx = {version = "0.9.0", features = ["runtime-tokio", "mysql"] } +axum = {version = "0.8", features = ["macros"]} +axum-htmx = {version = "0.8"} +fred = {version = "10.1"} +tracing = {version = "0.1"} +reqwest = {version = "0.13"} +maud = {version = "0.27", features = ["axum"]} async-trait = {version = "0.1.91"} tracing-subscriber = "0.3.23" +base64 = "0.23.1" +sha2 = "0.11" +subtle = "2.6" + +# Auth +tower-sessions = {version = "0.14"} +tower-sessions-sqlx-store = { version = "0.15", features = ["postgres"] } +tower-csrf = "0.1" +argon2 = "0.5" +sqlx = { version = "0.8", features = [ + "runtime-tokio", + "tls-rustls-ring-webpki", + "postgres", + "macros", + "migrate", + "uuid", + "time", + "json", +] } +time = "0.3" +uuid = { version = "1", features = ["v4", "serde"] } +tower_governor = "0.8" + # TODO: DIE :) \ No newline at end of file diff --git a/assets/img/blinkiesCafe-HY.gif b/assets/img/blinkiesCafe-HY.gif new file mode 100644 index 0000000000000000000000000000000000000000..de98db3986be6dd87a440b03fde306d81b139339 GIT binary patch literal 2584 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 F{|_Y_YGnWb literal 0 HcmV?d00001 diff --git a/assets/img/blinkiesCafe-gn.gif b/assets/img/blinkiesCafe-gn.gif new file mode 100644 index 0000000000000000000000000000000000000000..48445319daa26dc665aceba9a184c032590df0f8 GIT binary patch literal 7719 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 literal 0 HcmV?d00001 diff --git a/web/Cargo.toml b/web/Cargo.toml index 392ca04..66d229c 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -11,20 +11,34 @@ anyhow = {workspace = true} axum = {workspace = true } axum-htmx = {workspace = true } -axum-login = {workspace = true } -tower-sessions = {workspace = true } fred = {workspace = true } tracing = {workspace = true } reqwest = {workspace = true } maud = {workspace = true } -sqlx = {workspace = true} tracing-subscriber = {workspace = true, features = ["env-filter"]} serde = {workspace = true} -async-trait = {workspace = true} lightningcss = "1.0.0-alpha.72" inventory = "0.3.24" -tower = "0.5" tower-http = { version = "0.7.0", features = ["trace", "compression-gzip"] } -rust-embed = "8" -mime_guess = "2" \ No newline at end of file +base64 = {workspace = true} +sha2 = {workspace = true} +subtle = {workspace = true} + +#Auth +tower-sessions = {workspace = true} +tower-sessions-sqlx-store = {workspace = true} +tower-csrf = {workspace = true} +argon2 = {workspace = true} +sqlx = {workspace = true} +time = {workspace = true} +uuid = {workspace = true} +tower_governor = {workspace = true} +tower = "0.5.3" + +rust-embed = {version = "8.12.0", features = ["debug-embed"]} +mime_guess = "2" +urlencoding = "2.1.3" + +[profile.release] +debug = true \ No newline at end of file diff --git a/web/assets/img/blinkiesCafe-HY.gif b/web/assets/img/blinkiesCafe-HY.gif new file mode 100644 index 0000000000000000000000000000000000000000..de98db3986be6dd87a440b03fde306d81b139339 GIT binary patch literal 2584 zcmchZX;9Ni8ixZ!h=QW{&*4D022E5(5J8Yjge5?f2qUE^Is>8v$Hm2noPi-BfC=FU zfj~kagj_(vt%#g*FNxfOiXfh&f+#AE2RfOs>*((6Y;C!`Wt zb~}PtAOQ3dv^2mP8p@MVN>P;ZY1A%d6k=LgAD~o?zZHK!nY;xMY=8fvXMILEsAD#3DHf6C}{Vx=(aAkj2wcgagc z_o@O6+UIQrp?6={n}@Ex6Ve7EuIlE4mKE=v2btMK65N_>0VFslZ{E9cD~;@GVvowm zWaZ_f+)pz%+OnaLk`h25K!{;Nkr)D!7859is691OVRdOGysWC3f7zi1Md^xYsBCQM z5Dm0lfu8F%;a!bGR>&m7lD@Xd>d_iHoHyRJ(`;H%-h637G1q$!`snKBC;d-lLqjjq zHzBu$kc?Xc@0^FyU*pzwLe;lEY?G6G`L~Xzd@9R>y<1==2ro>8rAIGBUHY((;3nPXu#~&rxY? zmT-MGCx;;{FW})}srbv#gDC3-~E%h>=FdDGSBc5K7<+iVbE)3d%5M3M@GO1qD${R57Fr`;!Mj@V4! zib_6u;OKpJQJ(QT>kz&C zO^t{+#_+Q@j#J)@*$=TKauP-VRAgc@EsdTIOU+6Re!5$H^3uv^o7vc4%7OwwpJ-jI@sUR8*SG;iWrmd%Wn?I zFAh!iQHYeW8#}H+y>GP?QAch!D-_W#>$^-J&6wYN1^uPtg9GLyu2rvn1AkdjLiU#QMU zVxRNzH)#JKV)G5e?+BxPubXuDDHbj4!sCB5x1yj@;$Uv!DXEF%q)cW^28ETgon64q zc1Xz2ga`zHP>7JgL}Cd9B;%fmkLT6K$V4?|Rq*oa7HHE&^YdMlOI4lH_6~7HYky8; z42fHRrm~`R__DmEu}u*eNO z*>U;LpHuIT`r0SE{-q@?WnizGF=VK9t2%#4tDX6O4()s#?VR_mXM~~P^6@c7}qY@J75#*FiW)6^X+B;X@ z9>L|9Ea&rqk`lO<2qqAUmV=}yTOJE2f?_t-()mt;YDt?++TLD=%8P2G3LveO2Hk@r zgV63CWWAg)kyOz!Tq*9nQ&x5LnjyD#@{!@apODWg?!(9DU^A~zzj*j+bbkJcuPZF| zwy`atnY~%E3A!`&1KNdM7rF|Sl#uXfv_XsE9B}@?0@WY}rPp(i;dsonPtDBGX@fhA z)~#2tTLBHX71U?m>Gj1%UGi**`lZ!J0WBb4Gkg7(5OT;82c{!7NW?KwWQ?? zMUtH)DHMv-(Q?{O=lI>{>+}0upU)rX_q)#Zo$H!E=DKID`D5nse7)|sxuuz&zIQ%A z1wH}5#>NI?`%YIwa?Q;WDZ6%ig0Jv9H7w_dO z_Z`IGp;vMnc*|h#W4<=t#{!NX7dqx09PDkVu|r!&TSw=wi?*hYhm4tarkXo65HRTd zYS0t->pKB(gJdCbx#r1Kn*Ov@VX}B#q-+-!!Ju;NX!l@~56E<5VZ{^{{S*Trru52J zcvO^+)^e>Ht1u763Ci@vGl?cGyBQ=jzx_sjkZ$98XY>wJK-9g7UW(8pvN0GCb=V`o zBE*WDmY#9Q*8+!zqw=Ge1OgN5qaT@|f$|Ct&Ad`8A|xbHf3-o1MIaJ+e6WPsg(Lb@ zR3g1%^@Ek`9&j3(t^!R1&EhO`-bXAUq2FGd3}wtDzhY!pl_EE20O+Wtzm(?|fK!_T=r7YJNk;5ua*_@bMoCg`@fQh~IJqeJsOT z`xi0yRr%HjzXeRPOcm8n7x@)ayW@=ZBB^wp9gpQLoSpSQX8}tUJ=E}zH3e0CVE#G< zq4P6r!Nk)bD^A(NrmZ9m={$~1RvUN8KBGc<=yOu^vUh@d{$TTH`tFxCk7U^_J^L(I zzd93jgf-PGV6S?+qE1`0Nh)hmx?rjJfytz=oOT+FlyHfJ@=z{wOzKlID4S;{^RCk0 zT#QSwdxN>5-vTJ1`kNCG0Ktp_UraFjRUqi>UzA*?MBoMNfa)wo_4fdIiY7E1*Y+75&M;mV#A>a$%3zP(={>=mKdb!}a@(hYX) z+K~1aT@(y3T~D_c!R#eWGW+tK)(SuPJ&4~gJ0m4IMl&ceimB>%tTf6|bz&EpNSrAB z#g;qZx#HJt)LL!$Y)Mg4Z0p`;vBW^_M;8`mnvdI)Msrys)ZkXCW0KPLLnTP^RXver zHnzPUaX0^+zVq74lF;WiCdaA%><2-X7RQge#_Ha;K_+pRgDhE*AIrV%N0)nj2lhO? z>iheT7S~H{vNy_Fh9qFyT3)Q3abk|g;@xsSbs&^7FVloh7=Ni1YmJzl%Cx=c`mE)k z#^fajX!;NecIg0Bc_pnG;xNe@Jp11TQPr=qX zCVScX5RDtl-16%X-`1YCq+P@`{!_1KpSU$mCdxpsl>(<^9m@=_9(t01gs;81l&$=O zD=YD5m_8gNdhpVvoiup7XhbI!EBnQ|b}?tzh8Kf$Pf7MXD;4|nN?~QcrJYz%7atbg z{aXjm@FG)gu59m_n%C7H2`w|qeDZqjfz!?vxFbXhdjIKHzTFH@Hy<9XrlCrt4^h%_ z(?J=_rZbSI+<}YRVlm#{bfbh?(Lg@o=4?s|i zbtD0(gH}v01kB?E6Lh#uW0V;3|1sCi!5HM|p29ilXlSeRn}9dbLcAf^kmOCY5bw>Q3Ymx8LV6*?o8uJXN>Azb&$;4uSq0yqRywe?v1{jD z99)BhG~x-i0E9#~i)R6+VB_f(NncPYNCm;0mv`rzPh0PJg)u=k3qmji6kj1SX@5>? zuQ$iy4LTZ6B74UBL0nUODDcSrB1}^hk4m*f2}h&xIJYufl&c*-A@xdaosTHGypj=O zV2MDvMO|pD%x#Y<#gYYrtM~3Ji;1Z|P#91n4oBlq1{2(-F*q-G0`70pinpuQ?Ojal zAC!On8hE3O8sV1smqt!~IF1Yn%7HgVVqZ;58|R16yG2mw9SYMSl|8Y-MLPUi>oSx0 zRaYsFo%e)#NCrJ=Pyqu+qJ_R}ASLMYv7jx^bzqO+CCl_EdP04Bzb(t`s9u!AXH}Bi zaOrMYF~cwl0_D~ALzPx!uEz)T*T|mF$%>W%CVp~ZK)Rx{wopz-`Yp#`v-b3;CewjV>8|R$CpI7uqg26gGWH(m zC@5ALLF~D{IvB_E=?{?>k5A{ec+6k|=B7$dfMCEMIc##|>EmT1w^i0=kSb3;6zhil za<)Ocb!in9_nzBXa)wPNG);YcWob91`@$fhRq+9=1}CaP-(}}5`FV|V?e&eqll~q* zTZZDN&Jmo@jKpx{E6MUBZ9moj2%bt(yPs4UU3WP$Y2^)t-wX$YQdx3sC3X|I6*EX|8;?a z2IB=hx81=U>9?oI&wFR9>I@)E+IH-CwAl||aeF%8ppKq=%|@6wlDxNa8~VCyrIJ1h zGPYE2Te-T#B{SiPp1gFpD5+WX3cT4!5?uPe#H3&*&3PF*Z&(fZlgd#+gh#`i290xs z3ML@Vu$Yk2HX^|=0F?uD!(ofvG4SonVGIDST>%5+{;AgZ&EcBFIdz&HXsxXu=->*m z{WsX!%!1CUx=pr_=;DO*O}vong0pcJn!6$0n~M9JIyWKzr((!Bq&U`-4e5qv_J3N2 zfNygC?^*nhU!gV_43>o=F11%7yw*cEdt`B3&(|4Yzv@5-2pGN*LxV;G(EL3t3l%W{ zb!h>ky-fTSLBX2*>E;p}$B&1GGz_F5M5v0K^v^#`Wn}UJMNLjhd>o3TP1i~m)OpWJ8ju9T zvtT%3Yc_e*GDG`3{TfnDkGuOcZs6mG!avyna= zB;E3oDpuZ5lH_|x@j#xW`crMy$92khdD9gx4vv?m$j@tf^Z4!(<3_Id(3gogPIu(1 zm51(%;(O?vHi6>GST9Q-JrCXc6N-3UYS(+_TVg1z^(;je=uR#^aa26uYc#cs$-pg1ss=Bx_C zjB2g_&C})j+1EaCJ9MvCIpR*57s^s_9CL$*s`G%Nfo~znaM;?hPY=!=?FP{*8V50N z--zagXgU;$#%Z_KZt-yqe^`>|@|*@!31tfg-9@4iE{e=5_#F6F`ep_uSIGQ8u7hTT zsH5)Glv2UDU^jYt6!deJTU3t&aC~*IjaFcWM(mEPwMwbdRZst)j;)@pdT7dLz`)z0E;JEsUboQ_jTM7fz-%?*AO_tMYc-TjYv{3*y?iNUl`cV5 zAPuN*p!gqs)TXy9AVU%h9`3;^fmhTnI!9XeJHH~5FTzwGO52T8p}F)P^k?Va&B8}^ zRQwt}D#?ktJ=dhb>;f}?e;|bg`^T!J^%%dRkA<~QReey&jlt4N7#T3cPxqb&$V91e zSxF5ZgQr5(h=F(Bw+e;HHT+cDEx~D%0#!Y78TU$3NEk7fpK9Du6PI1>W2Oq(2c<|f zh7q885Cj@hLEzH>z+oAo@L+R3RtTt&g2VIx6u|t)LE2m&u4Wd5dH4h!bgIuUgBAxU zNGYKaP?@0o{JTcbo(~nvUQ16$ek)WjXvKJPZ)h_@P?n&QKvjd{1T7R$uAq`Z5rSd{ zZ4*$dHWv!0Pf(MfrQ;uG=kGrN1&L=zqe&*4AKjJAhUZ)RJDjQBR(5dhe}aTyR%mE& zg=69JStJ!IuNTh5Xi(D<#6J-A{&bDm43eub=?EFjKp-e+s4^lLOfC{frQ*0Eq3|pW zQdI1GT~d8RV-r6$klZGoj=`d_esBz`xHJ%fq;{i9!fUTydo(!2ufYY9+Q_6kg}2}+ ztWdz%3!Fz`NSWC3^6<*48I5N7&UoD#b2|W)F+uVxBoJm0LX`HWpVL!ykCrad+gh6z zww?OOXYeufs%(NJie6aJ$OGV02A93<1VA-RLZd9YnoPNAi>EmUy|$GfDmD)pPji`& zAK56=jT!hPL49$*a_?5nUCrLc#bCecqF)RgF zX0>i8lC-zVd}r~hP|deb;xKlel8j&Fb5J~dzv$O3enR>AMTgDw1dgg6BW|#c5irIy1-it=z8&zbI{=x8JEdfdPt78ri716n#VP%Qy`fA_(!P}&C*H}PlSZkS`KgnFC~%*Vg?qHb^O1JwvbVmGzUA&ld-ziF{HF@y zys%!H_Qld%OZZ6R&04fj|K#We{T3@#d+wfQKt^9oAOO)p71Q#MpSoBk%mmG2FbdkE z06Jf}evNcqO~AAKOrHU;470bdl?~mU&8Eeu>clNSHVy0f)vV zA2FV1F%*FmzBgGzi^xOB?ZQz1V~II}L7_k-f{n#ugut|57Mw^7#|x7XAReEAfS<2R z{;L`BK%4U~Az5 zl;6u@B$2wZGP?H8R7~u^y2+MCh*m^Sh<@zX)4zbNa}pJpPd(bFhjLY8&nH;n--EXG9M2zzNG= zE>sm-Eh#jONd>l8^tR&r(yldlk9bwPYHQ52c%Pr4s_DM5yO(*^N~I=|Tyhqx!QUqF zv2~wAn^?@S39xR^pN`j3UegJCHPQU!oTukE4p}$1<@uc(60uCy6!&fAfI0O=AijLm zcD7h!wW+os)l=>H&0zT;gB&J@@uWOQX^B3kAbR-gOU!4pBqI^s-PIU0stF({8Ewvj zKN#|nahjE2rG}_Fe4jVgt6R#;Cxs{b0we~*A!h69Q5#hjU-PAB#n8AAqdMABEXi+0 zWh5&c)ogcKS;l1u^*GJgHvQDn5<6XLQU5hJf?{-~R8*z;Y1#z~mLF$v`f)Z`?4add znv#GEs7Bbz7Z>3RJF6nU^0kYuRCDR84Z^C%F>qnlA^8xgO0G{#P4R%VZbjre3=_i& zGi@E*@is_w$mn-9%8CHi4a$N%)GmN-o|x=qfn4?j2vH0`iqC@wk^Z51{^p+WFY_7s z4F%<6x9$hpyEq#fYso5D?b1Erc{KNI9F&9P5H1t~2tQN=C=C#J$SZ^&>H?Grs0dIa zpdvtxfXqWn1;ic#|2M}B2nGphJP+Mf07(cqnW0FQRe<>a%xpn(T2Vh!Ii}d`0IwlcRzs`OfWe_6i(yf(^=pF&LK26ILQXh^5^qTeL8p_*WDs9QWTGf=p)v+YJlSq05?2EqGf=7jbIi~{#Nn>wUlS#t;QnHU zXvK%MILY&y(oxKTZ z()SX!%4m8_kz{(4iMur03hmu)rHedkVYLg##BMFywqZEmG2atwbDQP$NV(g2$gWT2 zt`;i$xn|;C<1e#yA}{a7%b)OCl;BD~@XG&lJ=X6bMa=7D!=LM7{=^FRiX1uV?b=r! zWU`~GkT<_HxFrYguQFUnWoIF=#H`uJ+n4U_Ir-UZLP5=E=6a~OjPuX7hM6|Y79q>|Ih;mS)9! z;iEA~N!H3y6v#6h5gGf|=A~q%HnwyG9W=UJ!Os}&E}8eW>@7=EW<27UepJ0IEkg}l z$w;(i!H&7=3an_2S8ByEtM)*XUw>V;|`)q0v?Y25@}H0v;K2CI^lN zM}cF9XWQy`1G6Fx7lD6#*!N$Pw=T2bYF_E!4Z$hTqYyPK&E1<8kJ~%{1@ZqHg&>9= z_Wn0Sh$#dSG73S2m_jZg#E@DDC4_j>C4};SJ7WLl^Z(Dui%T$?2h0ER(Ig}YdZXFGWWt49f5H(YIuL!52$Ca-q)TT>NzkF;RX#HH zNJbVBgF#aW=nLd?MS()X$o4j(aP=iregDIO_%<{qy$I7t9%;Wp1}}*e-M9yKQZV;P z)ZRz0-)A4WD~26u7S?N|cdVy4>K zog_1&?n-#Gn5oTN_Jb|<5k(y$eg;b4O8k(k*ZJUesnKlpkEC@TS~%$RhOGMF2UE+a zYwI}zw92{lJX>uYjALq18CvdGTH{xrbd4X2@??qLgoCh4KPi2?2bF+vL$oVBM(c>; z1BPUPW6ZqkUaM*PX}m+-gs;Aho7*w294sv=EqXzU)>!>rSP~R+PCXZE$UCBbLJbtR zW4~L>DL+Yp85K=ZN4*ulvOYdkxUd#CRCzWpz!h`0PiHB8zomOknYf4D954#g^3k&~ z9Jh`bp6?Ol3{7iVR(m>k`O+_#ZRGMs%T@f7TeJtlV<1Hv~`}^w4x3+7cnN^-GWuw9&&Nefz#dSm(a-+bOmc<`8f>VFIq@c_s`AjLujAL~S}V=>T$65f-9$lL^p8=NTt1qv#EA0E7X1 F{|_Y_YGnWb literal 0 HcmV?d00001 diff --git a/web/assets/img/blinkiesCafe-gn.gif b/web/assets/img/blinkiesCafe-gn.gif new file mode 100644 index 0000000000000000000000000000000000000000..48445319daa26dc665aceba9a184c032590df0f8 GIT binary patch literal 7719 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 literal 0 HcmV?d00001 diff --git a/web/assets/img/github-mark.svg b/web/assets/img/github-mark.svg new file mode 100644 index 0000000..17d1561 --- /dev/null +++ b/web/assets/img/github-mark.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/build.rs b/web/build.rs new file mode 100644 index 0000000..d506869 --- /dev/null +++ b/web/build.rs @@ -0,0 +1,5 @@ +// generated by `sqlx migrate build-script` +fn main() { + // trigger recompilation when a new migration is added + println!("cargo:rerun-if-changed=migrations"); +} diff --git a/web/migrations/20260820090931_create_users.down.sql b/web/migrations/20260820090931_create_users.down.sql new file mode 100644 index 0000000..f67aede --- /dev/null +++ b/web/migrations/20260820090931_create_users.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +DROP TABLE users; \ No newline at end of file diff --git a/web/migrations/20260820090931_create_users.up.sql b/web/migrations/20260820090931_create_users.up.sql new file mode 100644 index 0000000..f458e86 --- /dev/null +++ b/web/migrations/20260820090931_create_users.up.sql @@ -0,0 +1,11 @@ +-- Add up migration script here +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + username TEXT UNIQUE NOT NULL, + email TEXT UNIQUE NOT NULL, + email_confirmed_at TIMESTAMPTZ, + password_hash TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + two_factor_secret TEXT +); \ No newline at end of file diff --git a/web/migrations/20260820102145_user_confirmations.down.sql b/web/migrations/20260820102145_user_confirmations.down.sql new file mode 100644 index 0000000..64c4249 --- /dev/null +++ b/web/migrations/20260820102145_user_confirmations.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here +DROP INDEX idx_confirmations_user_id; +DROP TABLE confirmations; \ No newline at end of file diff --git a/web/migrations/20260820102145_user_confirmations.up.sql b/web/migrations/20260820102145_user_confirmations.up.sql new file mode 100644 index 0000000..73c7bba --- /dev/null +++ b/web/migrations/20260820102145_user_confirmations.up.sql @@ -0,0 +1,12 @@ +-- Add up migration script here +CREATE TABLE confirmations ( + identifier BYTEA PRIMARY KEY, + verifier_hash BYTEA NOT NULL, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + action_type TEXT NOT NULL, + details JSONB, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_confirmations_user_id ON confirmations(user_id); \ No newline at end of file diff --git a/web/src/web/content/partials/footer.rs b/web/src/web/content/partials/footer.rs index 2a884f5..c3b38fa 100644 --- a/web/src/web/content/partials/footer.rs +++ b/web/src/web/content/partials/footer.rs @@ -1,22 +1,87 @@ +use crate::web::content::style::CssFragment; use super::*; +inventory::submit! { + CssFragment(r#" + footer { + display: flex; + flex-direction: column; + align-items: end; + justify-content: center; + gap: 1rem; + padding: 1rem; + background: var(--footer-bg); + + > .upper { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 1rem; + width: 100%; + + > .accessibility { + display: flex; + .skip-link { + position: absolute; + top: -10000px; + left: -10000px; + width: 1px; + height: 1px; + overflow: hidden; + } + + .skip-link:focus { + position: fixed; + top: 10px; + left: 10px; + width: auto; + height: auto; + padding: 10px 20px; + background-color: #fff; + color: #000; + border: 2px solid #000; + z-index: 9999; + } + } + } + + > .lower { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 1rem; + width: 100%; + } + } + "# + ) +} + pub fn footer() -> Markup { html! { footer { - div class="accessibility" { - a href="#main-content" { "Skip to main content" } - a href="#footer" { "Skip to footer" } - a href="#navigation" { "Skip to navigation" } + div class="upper" { + div class="decoration" { + img src="/assets/img/blinkiesCafe-eX.gif"; + img src="/assets/img/blinkiesCafe-gn.gif"; + img src="/assets/img/blinkiesCafe-HY.gif"; + } + div class="accessibility" { + nav aria-label="Skip links" { + a.skip-link href="#main-content" tabindex="0" { "Skip to main content" } + a.skip-link href="#footer" tabindex="1" { "Skip to footer" } + a.skip-link href="#navigation" tabindex="2" { "Skip to navigation" } + } + } } - div class="decoration" { - img src="/static/img/banner-begay.gif"; - img src="/static/img/banner-docrime.gif"; - img src="/static/img/banner-hate.gif"; - } - div class="notice" { - span { - "Cookies are used to store your session." - "We do not use them to track you." + div class="lower" { + div class="notice" { + span { + "Cookies are used to store your session." + "We do not use them to track you." + } } } } diff --git a/web/src/web/content/partials/header.rs b/web/src/web/content/partials/header.rs index f624de3..bf2e709 100644 --- a/web/src/web/content/partials/header.rs +++ b/web/src/web/content/partials/header.rs @@ -1,23 +1,69 @@ +use crate::web::content::style::CssFragment; use super::*; +inventory::submit! { + CssFragment(r#" + .nav-bar { + display: flex; + flex-direction: row; + align-items: center; + gap: 1rem; + padding: 0.75rem 1.5rem; + background: var(--nav-bg); + + > .spacer { + flex-grow: 1; + } + + > .link { + color: var(--nav-link); + text-decoration: none; + + border: none; + padding: 0; + background: none; + + cursor: pointer; + font: inherit; + + > img { + max-height: 1.5rem; + } + } + + > .link.__active { + font-weight: 600; + color: var(--nav-link-active); + } + + > .title { + color: var(--nav-link); + + font-size: 1.5rem; + font-weight: 600; + + color: var(--nav-link); + } + } + "#) +} + pub fn header(is_auth: bool) -> Markup { html! { - nav class="navigation" { - ul { - li { a href="/" { "Home" } } - li { a href="/regdomain" { "Register a subdomain" }} - li { a href="/status" { "Status" }} - div class="spacer" {} - @if is_auth { - li { a href="/logout" class="auth-shown" hx-get="/auth/logoutprompt" hx-target="#popup" hx-swap="innerHTML" { "Logout" }} - } @else { - li { a href="/login" class="auth-hidden" hx-get="/auth/loginprompt" hx-target="#popup" hx-swap="innerHTML" { "Login" }} - li { a href="/register" class="auth-hidden" hx-get="/auth/registerprompt" hx-target="#popup" hx-swap="innerHTML" { "Register" }} - } - li { a href="https://github.com/microtld/microtld" { - img src="/static/img/github-mark.svg"; - }} + nav.nav-bar { + a.title href="/" { "MicroTLD" } + + a.link.__active href="/" { "Home" } + a.link href="/regdomain" { "Register a subdomain" } + a.link href="/status" { "Status" } + div class="spacer" {} + @if is_auth { + button.link href="/logout" class="auth-shown" hx-get="/auth/logoutprompt" hx-target="#modal-content" hx-swap="innerHTML" popovertarget="modal" { "Logout" } + } @else { + button.link href="/login" class="auth-hidden" hx-get="/auth/loginprompt" hx-target="#modal-content" hx-swap="innerHTML" popovertarget="modal" { "Login" } + button.link href="/register" class="auth-hidden" hx-get="/auth/registerprompt" hx-target="#modal-content" hx-swap="innerHTML" popovertarget="modal" { "Register" } } + a.link href="https://github.com/microtld/microtld" {img src="/assets/img/github-mark.svg";} } } } \ No newline at end of file diff --git a/web/src/web/content/partials/page.rs b/web/src/web/content/partials/page.rs index ff8b928..e2a4d2d 100644 --- a/web/src/web/content/partials/page.rs +++ b/web/src/web/content/partials/page.rs @@ -2,8 +2,97 @@ use maud::DOCTYPE; use crate::web::content::partials::footer::footer; use crate::web::content::partials::head::head; use crate::web::content::partials::header::header; +use crate::web::content::style::CssFragment; use super::*; +inventory::submit! { + CssFragment(r#" + .main-content { + padding: 1rem; + display: flex; + flex-direction: column; + flex-grow: 1; + } + + /* Style the built-in backdrop provided by the Popover API */ + #modal::backdrop { + background-color: rgba(0, 0, 0, 0.5); + } + + #modal { + /* Underlay covers entire screen. */ + top:0px; + bottom: 0px; + left:0px; + right:0px; + + background-color: color-mix(in srgb, var(--bg) 50%, transparent); + border: 1px solid var(--border); + + z-index: 1000; + color: var(--text); + + justify-content: center; + flex-direction:column; + align-items:center; + + > #modal-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + > form { + display: flex; + flex-direction: column; + + > label { + padding-top: 1rem; + } + + > button { + margin-top: 1rem; + } + } + } + + > .modal-close { + z-index: 1001; + background: rgba(200, 20, 20, 0.8); + + color: var(--text); + font-weight: 600; + + border: none; + + margin-top: 1rem; + + cursor: pointer; + font: inherit; + } + + > .modal-close:hover { + background: rgba(200, 20, 20, 0.6); + + transition: background 0.2s ease-in-out; + } + + padding: 1rem 3rem; + } + + #modal:popover-open { + display:flex; + } + "# + ) +} + +pub fn show_error(message: &str) -> Markup { + html! { + h1 { (message) } + } +} + pub fn page(page_title: &str, page_slug: &str, is_auth: bool, content: Markup) -> Markup { html! { (DOCTYPE) @@ -11,9 +100,14 @@ pub fn page(page_title: &str, page_slug: &str, is_auth: bool, content: Markup) - (head(page_title, page_slug)) (header(is_auth)) body { - div class="main-content" { + div class="main-content" tabindex="-1" { (content) } + + div popover id="modal" { + div id="modal-content" {} + button class="modal-close" popovertarget="modal" popovertargetaction="hide" { "Close" } + } } (footer()) } diff --git a/web/src/web/content/style/css/base.css b/web/src/web/content/style/css/base.css index ab4df6a..38f2fc9 100644 --- a/web/src/web/content/style/css/base.css +++ b/web/src/web/content/style/css/base.css @@ -1,3 +1,35 @@ -html { - background-color: #0e0e0e; +:root { + --text: #f3f3f3; + --text-muted: #cecece; + --bg: #0e0e0e; + --border: #5f2f5f; + --accent: #c11292; + --nav-bg: #1e1e1e; + --nav-link: #f3f3f3; + --nav-link-active: #ce9ece; +} + +html { + height: 100%; + width: 100%; + scroll-behavior: smooth; +} + +body { + font-family: 'Roboto', system-ui, -apple-system, sans-serif; + color: var(--text); + background: var(--bg); + margin: 0; + line-height: 1.6; + + background-color: #0e0e0e; + font-size: 16px; + padding: 0; + overflow-x: hidden; + overflow-y: auto; + height: 100vh; + width: 100vw; + + display: flex; + flex-direction: column; } diff --git a/web/src/web/mod.rs b/web/src/web/mod.rs index 3d74f9e..8331c9a 100644 --- a/web/src/web/mod.rs +++ b/web/src/web/mod.rs @@ -1,3 +1,3 @@ pub(crate) mod content; pub(crate) mod htmx; -pub(crate) mod service; \ No newline at end of file +pub(crate) mod service; diff --git a/web/src/web/service/auth/confirmation.rs b/web/src/web/service/auth/confirmation.rs new file mode 100644 index 0000000..b743558 --- /dev/null +++ b/web/src/web/service/auth/confirmation.rs @@ -0,0 +1,106 @@ +use argon2::password_hash::rand_core::{OsRng, RngCore}; +use sha2::{Digest, Sha256}; +use sqlx::{Error, PgPool}; +use sqlx::types::Json; +use time::{OffsetDateTime, Duration}; +use uuid::Uuid; + + +#[derive(Debug, Clone, sqlx::FromRow)] +pub struct Confirmation { + identifier: [u8; 16], + + #[sqlx(skip)] + verifier: Option<[u8; 16]>, + + pub verifier_hash: [u8; 32], + pub user_id: Uuid, + pub action_type: String, + details: Option>, + expires_at: OffsetDateTime, + created_at: OffsetDateTime, +} + +impl Confirmation { + pub fn new(user_id: Uuid, action_type: String, expiry_duration: Duration) -> Confirmation { + let mut rng = OsRng::default(); + let mut identifier = [0; 16]; + rng.fill_bytes(&mut identifier); + let mut verifier = [0; 16]; + rng.fill_bytes(&mut verifier); + + let mut hasher = Sha256::default(); + Digest::update(&mut hasher, &verifier); + let verifier_hash = hasher.finalize().0; + + let now = OffsetDateTime::now_utc(); + + Confirmation { + identifier, + verifier: Some(verifier), + verifier_hash, + user_id, + action_type, + details: None, + expires_at: now + expiry_duration, + created_at: now, + } + } + + /// for use in http://localhost:3000/auth/confirm?token= + pub fn get_url_token(&self) -> anyhow::Result { + Ok(base64::encode([self.identifier.as_ref(), self.verifier.ok_or_else(||anyhow::anyhow!("Cannot construct url token from non-original confirmation"))?.as_ref()].concat())) + } + + pub fn decode_url_token(token: &str) -> anyhow::Result<(Vec, Vec)> { + let decoded_token = base64::decode(token)?; + let (identifier, verifier) = decoded_token.split_at(16); + Ok((identifier.to_vec(), verifier.to_vec())) + } + + pub async fn submit(self, db: &PgPool) -> anyhow::Result { + let result = sqlx::query_as::<_, Confirmation>( + r#" + INSERT INTO confirmations (identifier, verifier_hash, user_id, action_type, details, expires_at, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING * + "#, + ) + .bind(&self.identifier) + .bind(&self.verifier_hash) + .bind(&self.user_id) + .bind(&self.action_type) + .bind(&self.details) + .bind(&self.expires_at) + .bind(&self.created_at) + .fetch_optional(db) + .await; + + match result { + Ok(c) => { + match c { + None => { + Err(anyhow::anyhow!("Confirmation didn't make it to the database.")) + } + Some(c) => { + Ok(c) + } + } + }, + Err(Error::Database(db_err)) if db_err.is_unique_violation() => { + Err(anyhow::anyhow!("Confirmation already exists")) + } + Err(e) => { + Err(anyhow::anyhow!("Database error: {}", e)) + } + } + } + + pub fn is_expired(&self) -> bool { + self.expires_at < OffsetDateTime::now_utc() + } + + pub fn is_valid(&self, verifier: &[u8; 16]) -> bool { + self.verifier.as_ref().map_or(false, |v| v == verifier) + } +} \ No newline at end of file diff --git a/web/src/web/service/auth/email.rs b/web/src/web/service/auth/email.rs new file mode 100644 index 0000000..858ef07 --- /dev/null +++ b/web/src/web/service/auth/email.rs @@ -0,0 +1,20 @@ +pub struct EmailHandler { + +} + +impl EmailHandler { + pub fn send_confirmation_email(&self, email: String, confirmation_token: String) { + } + + pub fn send_password_reset_email(&self, email: String, reset_token: String) { + } + + pub fn send_attempted_registration_email(&self, email: String) { + } + + pub fn send_attempted_login_email(&self, email: String) { + } + + pub fn send_password_changed_email(&self, email: String) { + } +} \ No newline at end of file diff --git a/web/src/web/service/auth/hash.rs b/web/src/web/service/auth/hash.rs new file mode 100644 index 0000000..cbabe8f --- /dev/null +++ b/web/src/web/service/auth/hash.rs @@ -0,0 +1,59 @@ +use std::sync::Arc; +use argon2::{ + password_hash::{ + rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString, + }, + Algorithm, Argon2, Params, Version, +}; +use tokio::task; +// fn build_hasher() -> Argon2<'static> { +// let params = Params::new( +// 64 * 1024, // 64 MiB memory cost +// 2, // 2 iterations +// 1, // 1 degree of parallelism +// None, // default output length (32 bytes) +// ) +// .expect("valid argon2 params"); +// +// Argon2::new(Algorithm::Argon2id, Version::V0x13, params) +// } + +fn build_hasher_with_pepper(pepper: &[u8]) -> Argon2<'_> { + let params = Params::new(64 * 1024, 2, 1, None).expect("valid argon2 params"); + Argon2::new_with_secret(pepper, Algorithm::Argon2id, Version::V0x13, params) + .expect("valid argon2 secret") +} + + + +pub fn hash_password(password: &str, pepper: &[u8]) -> Result { + let salt = SaltString::generate(&mut OsRng); + let hash = build_hasher_with_pepper(pepper).hash_password(password.as_bytes(), &salt)?; + Ok(hash.to_string()) +} + +pub fn verify_password( + password: &str, + stored_hash: &str, + pepper: &[u8] +) -> Result<(), argon2::password_hash::Error> { + let parsed = PasswordHash::new(stored_hash)?; + build_hasher_with_pepper(pepper).verify_password(password.as_bytes(), &parsed) +} + +pub async fn hash_password_async(password: String, pepper: Arc<[u8]>) -> Result { + task::spawn_blocking(move || hash_password(&password, pepper.as_ref())) + .await? + .map_err(|e| anyhow::anyhow!("Failed to hash password: {}", e)) +} + +pub async fn verify_password_async( + password: String, + stored_hash: String, + pepper: Arc<[u8]> +) -> Result<(), anyhow::Error> { + task::spawn_blocking(move || verify_password(&password, &stored_hash, pepper.as_ref())) + .await? + .map_err(|e|anyhow::anyhow!("Failed to verify password: {}", e)) +} + diff --git a/web/src/web/service/auth/mod.rs b/web/src/web/service/auth/mod.rs index 2cb2bd9..4ae34f7 100644 --- a/web/src/web/service/auth/mod.rs +++ b/web/src/web/service/auth/mod.rs @@ -1,32 +1,5 @@ -use std::fmt::{Display, Formatter}; -use axum_login::{AuthnBackend, UserId}; -use crate::web::service::WebService; - -mod user; - -impl AuthnBackend for WebService { - type User = user::User; - type Credentials = user::Credentials; - type Error = AuthError; - - async fn authenticate(&self, creds: Self::Credentials) -> Result, Self::Error> { - todo!() - } - async fn get_user(&self, user_id: &UserId) -> Result, Self::Error> { - todo!() - } -} - -#[derive(Debug)] -pub struct AuthError { - username: String, - reason: String, -} - -impl Display for AuthError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "AuthError for user {}: {}", self.username, self.reason) - } -} - -impl std::error::Error for AuthError {} \ No newline at end of file +pub mod user; +pub mod hash; +pub mod session; +pub mod confirmation; +pub mod email; \ No newline at end of file diff --git a/web/src/web/service/auth/session.rs b/web/src/web/service/auth/session.rs new file mode 100644 index 0000000..c8d1eef --- /dev/null +++ b/web/src/web/service/auth/session.rs @@ -0,0 +1,21 @@ +use sqlx::{PgPool}; +use tower_sessions::{ExpiredDeletion, Expiry, SessionManagerLayer}; +use tower_sessions_sqlx_store::{PostgresStore}; +use time::Duration; + +pub async fn session_layer(pool: PgPool) -> SessionManagerLayer { + let store = PostgresStore::new(pool); + store.migrate().await.expect("session table migration failed"); + + // Clean up expired sessions every 60 seconds + tokio::task::spawn( + store + .clone() + .continuously_delete_expired(tokio::time::Duration::from_secs(60)), + ); + + SessionManagerLayer::new(store) + .with_secure(false)//todo: set to true + .with_expiry(Expiry::OnInactivity(Duration::hours(24))) +} + diff --git a/web/src/web/service/auth/user.rs b/web/src/web/service/auth/user.rs index 8d01e98..ee287a9 100644 --- a/web/src/web/service/auth/user.rs +++ b/web/src/web/service/auth/user.rs @@ -1,5 +1,9 @@ -use axum_login::AuthUser; use serde::{Deserialize, Serialize}; +use sqlx::{Error, PgPool}; +use time::{Duration, OffsetDateTime}; +use uuid::Uuid; +use crate::web::service::auth::confirmation::Confirmation; +use crate::web::service::auth::email::EmailHandler; #[derive(Clone, Deserialize)] pub struct Credentials { @@ -7,24 +11,63 @@ pub struct Credentials { pub password: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct User { - pub id: u64, + pub id: Uuid, pub username: String, pub email: String, + pub email_confirmed_at: Option, pub password_hash: String, - pub two_factor_enabled: bool, + pub created_at: OffsetDateTime, + pub updated_at: OffsetDateTime, pub two_factor_secret: Option, } -impl AuthUser for User { - type Id = u64; +impl User { + pub async fn register(username: String, email: String, password_hash: String, db: &PgPool) -> Result { + // ON CONFLICT DO NOTHING prevents errors on duplicate email + // without revealing whether the email already exists + let result = sqlx::query_as::<_, User>( + r#" + INSERT INTO users (username, email, password_hash) + VALUES ($1, $2, $3) + ON CONFLICT (email) DO NOTHING + RETURNING * + "#, + ) + .bind(&username) + .bind(&email) + .bind(&password_hash) + .fetch_optional(db) + .await; - fn id(&self) -> u64 { - self.id - } - - fn session_auth_hash(&self) -> &[u8] { - self.password_hash.as_bytes() + match result { + Ok(u) => { + if let Some(u) = u { + let email_handler = EmailHandler{}; + let confirmation = Confirmation::new(u.id, "email_confirmation".into(), Duration::hours(24)); + let confirmation_token = confirmation.get_url_token().map_err(|e|RegistrationError::AnyhowError(e))?; + println!("http://localhost:3000/auth/confirm?token={}", urlencoding::encode(&confirmation_token)); + email_handler.send_confirmation_email(email, confirmation_token); + confirmation.submit(db).await.map_err(|e|RegistrationError::AnyhowError(e))?; + Ok(u) + } else { + Err(RegistrationError::EmailExists) + } + } + Err(Error::Database(db_err)) if db_err.is_unique_violation() => { + Err(RegistrationError::UsernameExists) + } + Err(e) => { + Err(RegistrationError::DatabaseError(e)) + } + } } } + +pub enum RegistrationError { + UsernameExists, + EmailExists, + DatabaseError(Error), + AnyhowError(anyhow::Error), +} \ No newline at end of file diff --git a/web/src/web/service/extractors/authuser.rs b/web/src/web/service/extractors/authuser.rs new file mode 100644 index 0000000..3589b39 --- /dev/null +++ b/web/src/web/service/extractors/authuser.rs @@ -0,0 +1,40 @@ +use axum::{ + extract::FromRequestParts, + http::{request::Parts, StatusCode}, +}; +use sqlx::PgPool; +use tower_sessions::Session; +use uuid::Uuid; +use crate::web::service::AppState; +use crate::web::service::auth::user::User; + +pub struct AuthUser(pub User); + +impl FromRequestParts for AuthUser { + type Rejection = StatusCode; + + async fn from_request_parts( + parts: &mut Parts, + state: &AppState, + ) -> Result { + let session = Session::from_request_parts(parts, state) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let user_id: Uuid = session + .get("user_id") + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .ok_or(StatusCode::UNAUTHORIZED)?; + + let user: User = sqlx::query_as("SELECT * FROM users WHERE id = $1") + .bind(user_id) + .fetch_optional(&state.db) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .ok_or(StatusCode::UNAUTHORIZED)?; + + Ok(AuthUser(user)) + } +} + diff --git a/web/src/web/service/extractors/mod.rs b/web/src/web/service/extractors/mod.rs index 40e3e84..95ce1a7 100644 --- a/web/src/web/service/extractors/mod.rs +++ b/web/src/web/service/extractors/mod.rs @@ -1 +1,2 @@ -pub mod dns; \ No newline at end of file +pub mod dns; +mod authuser; \ No newline at end of file diff --git a/web/src/web/service/mod.rs b/web/src/web/service/mod.rs index 35d86e4..ae2676c 100644 --- a/web/src/web/service/mod.rs +++ b/web/src/web/service/mod.rs @@ -2,7 +2,9 @@ pub mod auth; pub mod routes; pub mod extractors; -use sqlx::Pool; +use std::sync::Arc; +use sqlx::{PgPool, Pool}; +use sqlx::postgres::PgPoolOptions; use tokio::signal; use tracing_subscriber::EnvFilter; use microtld_backend::MicroTld; @@ -15,7 +17,7 @@ pub struct WebService { #[derive(Debug, Clone)] pub struct AppState { - db: Pool, + db: PgPool, config: AppConfig, micro_tld: MicroTld } @@ -24,6 +26,7 @@ pub struct AppState { pub struct AppConfig { database_url: String, listen_url: String, + pepper: Arc<[u8]> } impl WebService { @@ -31,12 +34,22 @@ impl WebService { let config = AppConfig { database_url, listen_url, + pepper: Arc::new([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]), }; + let pool = PgPoolOptions::new() + .max_connections(5) + .connect(&config.database_url) + .await?; + + sqlx::migrate!() + .run(&pool) + .await?; + let state = AppState { - db: Pool::connect(&config.database_url).await?, + db: pool, config, micro_tld, }; - let router = routes::apply_routes(axum::Router::new()).with_state(state.clone()); + let router = routes::apply_routes(axum::Router::new(), state.db.clone()).await.with_state(state.clone()); Ok(WebService {router, state}) } diff --git a/web/src/web/service/routes/auth/confirm.rs b/web/src/web/service/routes/auth/confirm.rs new file mode 100644 index 0000000..12342c4 --- /dev/null +++ b/web/src/web/service/routes/auth/confirm.rs @@ -0,0 +1,95 @@ +use axum::extract::{Query, State}; +use axum::response::IntoResponse; +use serde::Deserialize; +use sha2::Digest; +use subtle::ConstantTimeEq; +use time::OffsetDateTime; +use tower_sessions::Session; +use crate::web::service::AppState; +use crate::web::service::auth::confirmation::Confirmation; +use crate::web::service::auth::user::User; +use crate::web::service::routes::Route; + +#[derive(Deserialize, Default)] +pub(crate) struct Confirm { + token: String +} + +impl Route<'_, Query> for Confirm { + const PATH: &'static str = "/confirm"; + + async fn process(session: Session, state: State, query: Query) -> impl IntoResponse { + if session.is_empty().await { + return "Not logged in".into_response(); + } + + let (identifier, verifier) = Confirmation::decode_url_token(&query.token).unwrap(); + let verifier_hash = sha2::Sha256::digest(&verifier).0; + + let confirmation: Option = sqlx::query_as("SELECT * FROM confirmations WHERE identifier = $1") + .bind(&identifier) + .fetch_optional(&state.db) + .await + .unwrap_or(None); + + match confirmation { + Some(confirmation) => { + if confirmation.action_type != "email_confirmation" { + return "Invalid confirmation type".into_response(); + } + + if confirmation.is_expired() { + return "Confirmation expired".into_response(); + } + + match verifier_hash.ct_eq(&confirmation.verifier_hash).unwrap_u8() { + 1 => { + let user: Option = sqlx::query_as("SELECT * FROM users WHERE id = $1") + .bind(confirmation.user_id) + .fetch_optional(&state.db) + .await.unwrap_or(None); + + match user { + Some(mut user) => { + user.email_confirmed_at = Some(OffsetDateTime::now_utc()); + match sqlx::query("UPDATE users SET email_confirmed_at = $1 WHERE id = $2") + .bind(&user.email_confirmed_at) + .bind(&user.id) + .execute(&state.db) + .await { + Ok(_) => { + match sqlx::query("DELETE FROM confirmations WHERE identifier = $1") + .bind(&identifier) + .execute(&state.db) + .await { + Ok(_) => { + "Email confirmed".into_response() + } + Err(e) => { + "Error confirming email".into_response() + } + } + }, + Err(e) => { + "Error confirming email".into_response() + } + } + + + }, + None => { + "User not found".into_response() + } + } + } + _ => { + "Invalid token".into_response() + } + } + } + None => { + "Confirmation not found".into_response() + } + } + } +} diff --git a/web/src/web/service/routes/auth/login.rs b/web/src/web/service/routes/auth/login.rs index def68fb..b34e59f 100644 --- a/web/src/web/service/routes/auth/login.rs +++ b/web/src/web/service/routes/auth/login.rs @@ -1,9 +1,13 @@ use axum::extract::{Form, State}; -use axum::response::IntoResponse; -use reqwest::StatusCode; +use axum::http::{HeaderValue, StatusCode}; +use axum::response::{IntoResponse, Redirect}; use serde::Deserialize; +use tower_sessions::Session; +use crate::web::content::partials::page::show_error; use crate::web::service::AppState; +use crate::web::service::auth::user::User; use crate::web::service::routes::Route; +use crate::web::service::auth::hash::{hash_password_async, verify_password_async}; #[derive(Deserialize, Default)] pub(crate) struct Login { @@ -14,8 +18,41 @@ pub(crate) struct Login { impl Route<'_, Form> for Login { const PATH: &'static str = "/login"; - async fn get_content(state: State, Form(login_data): Form) -> impl IntoResponse { - todo!("implement login"); - StatusCode::OK + async fn process(session: Session, state: State, Form(form): Form) -> impl IntoResponse { + let user: Option = sqlx::query_as("SELECT * FROM users WHERE username = $1") + .bind(&form.username) + .fetch_optional(&state.db) + .await + .unwrap_or(None); + + let Some(user) = user else { + println!("User not found"); + // Run a dummy hash to prevent timing-based user enumeration + let _ = hash_password_async("dummy-password".to_string(), state.config.pepper.clone()).await; + return show_error("Invalid email or password").into_response(); + }; + + if verify_password_async(form.password, user.password_hash.clone(), state.config.pepper.clone()) + .await + .is_err() + { + println!("Password verification failed"); + return show_error("Invalid email or password").into_response(); + } + + // Prevent session fixation: generate a new session ID, preserving data + session.cycle_id().await.expect("failed to cycle session ID"); + + // Store user identity in the session + session + .insert("user_id", user.id) + .await + .expect("failed to insert session data"); + + // Validate redirect target if using a ?next= parameter. + // Only allow relative paths. Reject absolute URLs to prevent open redirects. + let mut response = StatusCode::OK.into_response(); + response.headers_mut().insert("HX-Redirect", HeaderValue::from_str("/").unwrap()); + response } } \ No newline at end of file diff --git a/web/src/web/service/routes/auth/loginprompt.rs b/web/src/web/service/routes/auth/loginprompt.rs index 1238ab4..804b248 100644 --- a/web/src/web/service/routes/auth/loginprompt.rs +++ b/web/src/web/service/routes/auth/loginprompt.rs @@ -1,6 +1,7 @@ use axum::extract::State; use axum::response::IntoResponse; use maud::html; +use tower_sessions::Session; use crate::web::service::AppState; use crate::web::service::routes::Route; @@ -9,12 +10,19 @@ pub(crate) struct LoginPrompt; impl Route<'_, ()> for LoginPrompt { const PATH: &'static str = "/loginprompt"; - async fn get_content(_state: State, _query: ()) -> impl IntoResponse { + async fn process(_session: Session, _state: State, _query: ()) -> impl IntoResponse { html! { - form { + h1 { "Login" } + form hx-post="/auth/login" hx-target="#modal-content" hx-swap="innerHTML" { + label for="username" { "Username" } input type="text" name="username" placeholder="Username" {} - input type="password" name="password" placeholder="" {} - input type="submit" value="Login" {} + + label for="password" { "Password" } + input type="password" name="password" id="password" + required minlength="10" maxlength="128" + autocomplete="new-password"; + + button type="submit" { "Login" } } } } diff --git a/web/src/web/service/routes/auth/logout.rs b/web/src/web/service/routes/auth/logout.rs index c433116..3301379 100644 --- a/web/src/web/service/routes/auth/logout.rs +++ b/web/src/web/service/routes/auth/logout.rs @@ -1,21 +1,20 @@ use axum::extract::{Form, State}; -use axum::response::IntoResponse; -use reqwest::StatusCode; +use axum::http::{HeaderValue, Response, StatusCode}; +use axum::response::{IntoResponse, Redirect}; use serde::Deserialize; +use tower_sessions::Session; use crate::web::service::AppState; use crate::web::service::routes::Route; -#[derive(Deserialize, Default)] -pub(crate) struct Logout { - username: String, - session_id: String -} +pub(crate) struct Logout; -impl Route<'_, Form> for Logout { +impl Route<'_, ()> for Logout { const PATH: &'static str = "/logout"; - async fn get_content(state: State, Form(query): Form) -> impl IntoResponse { - todo!("implement logout"); - StatusCode::OK + async fn process(session: Session, state: State, _: ()) -> impl IntoResponse { + session.flush().await.expect("failed to flush session"); + let mut response = StatusCode::OK.into_response(); + response.headers_mut().insert("HX-Redirect", HeaderValue::from_str("/").unwrap()); + response } } \ No newline at end of file diff --git a/web/src/web/service/routes/auth/logoutprompt.rs b/web/src/web/service/routes/auth/logoutprompt.rs index 8522472..cbafaea 100644 --- a/web/src/web/service/routes/auth/logoutprompt.rs +++ b/web/src/web/service/routes/auth/logoutprompt.rs @@ -1,6 +1,7 @@ use axum::extract::State; use axum::response::IntoResponse; use maud::html; +use tower_sessions::Session; use crate::web::service::AppState; use crate::web::service::routes::Route; @@ -9,11 +10,12 @@ pub(crate) struct LogoutPrompt; impl Route<'_, ()> for LogoutPrompt { const PATH: &'static str = "/logoutprompt"; - async fn get_content(_state: State, _query: ()) -> impl IntoResponse { + async fn process(_session: Session, _state: State, _query: ()) -> impl IntoResponse { html! { - p { "Are you sure you want to logout?" } - form { - input type="submit" value="Logout" {} + h1 { "Logout" } + form hx-post="/auth/logout" hx-target="#modal-content" hx-swap="innerHTML" { + p { "Are you sure you want to logout?" } + button type="submit" { "Logout" } } } } diff --git a/web/src/web/service/routes/auth/mod.rs b/web/src/web/service/routes/auth/mod.rs index 273e95c..50c7b67 100644 --- a/web/src/web/service/routes/auth/mod.rs +++ b/web/src/web/service/routes/auth/mod.rs @@ -4,6 +4,7 @@ pub mod logoutprompt; pub mod login; pub mod register; pub mod logout; +mod confirm; pub(crate) const PATH: &'static str = "/auth"; @@ -12,4 +13,5 @@ pub(crate) use registerprompt::RegisterPrompt; pub(crate) use logoutprompt::LogoutPrompt; pub(crate) use login::Login; pub(crate) use register::Register; -pub(crate) use logout::Logout; \ No newline at end of file +pub(crate) use logout::Logout; +pub(crate) use confirm::Confirm; \ No newline at end of file diff --git a/web/src/web/service/routes/auth/register.rs b/web/src/web/service/routes/auth/register.rs index dc859e3..3201f35 100644 --- a/web/src/web/service/routes/auth/register.rs +++ b/web/src/web/service/routes/auth/register.rs @@ -1,23 +1,75 @@ use axum::extract::{Form, State}; use axum::response::IntoResponse; -use reqwest::StatusCode; +use maud::html; use serde::Deserialize; +use tower_sessions::Session; +use crate::web::content::partials::page::show_error; use crate::web::service::AppState; use crate::web::service::routes::Route; +use crate::web::service::auth::hash::hash_password_async; +use crate::web::service::auth::user::{RegistrationError, User}; -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, Debug)] pub(crate) struct Register { username: String, email: String, password: String, - password_confirm: String, + password_confirmation: String, } impl Route<'_, Form> for Register { const PATH: &'static str = "/register"; - async fn get_content(state: State, Form(query): Form) -> impl IntoResponse { - todo!("implement register"); - StatusCode::OK + async fn process(_session: Session, State(state): State, Form(form): Form) -> impl IntoResponse { + if form.password != form.password_confirmation { + return show_error("Passwords do not match").into_response(); + } + if form.password.len() < 10 || form.password.len() > 128 { + return show_error("Password must be 10 to 128 characters").into_response(); + } + + let password_hash = match hash_password_async(form.password, state.config.pepper.clone()).await { + Ok(hash) => hash, + Err(_) => return show_error("Registration failed").into_response(), + }; + + match User::register(form.username, form.email, password_hash, &state.db).await { + Ok(_) => { + html! { + h1 { "Check your email" } + p { "If this email can be used for an account, you will receive further instructions." } + }.into_response() + }, + Err(error) => { + match error { + RegistrationError::UsernameExists => { + html! { + h1 { "Username taken" } + p { "This username has already been taken. Please choose another." } + }.into_response() + }, + RegistrationError::EmailExists => { + html! { + h1 { "Check your email" } + p { "If this email can be used for an account, you will receive further instructions." } + }.into_response() + } + RegistrationError::DatabaseError(e) => { + eprintln!("Database error: {}", e); + html! { + h1 { "An error occurred" } + p { "Please try again later." } + }.into_response() + } + RegistrationError::AnyhowError(e) => { + eprintln!("Anyhow error: {}", e); + html! { + h1 { "An error occurred" } + p { "Please try again later." } + }.into_response() + } + } + } + } } } \ No newline at end of file diff --git a/web/src/web/service/routes/auth/registerprompt.rs b/web/src/web/service/routes/auth/registerprompt.rs index 854e74a..05010b3 100644 --- a/web/src/web/service/routes/auth/registerprompt.rs +++ b/web/src/web/service/routes/auth/registerprompt.rs @@ -1,6 +1,7 @@ use axum::extract::State; use axum::response::IntoResponse; use maud::html; +use tower_sessions::Session; use crate::web::service::AppState; use crate::web::service::routes::Route; @@ -9,14 +10,27 @@ pub(crate) struct RegisterPrompt; impl Route<'_, ()> for RegisterPrompt { const PATH: &'static str = "/registerprompt"; - async fn get_content(_state: State, _query: ()) -> impl IntoResponse { + async fn process(_session: Session, _state: State, _query: ()) -> impl IntoResponse { html! { - form { + h1 { "Create an account" } + form hx-post="/auth/register" hx-target="#modal-content" hx-swap="innerHTML" { + label for="username" { "Username" } input type="text" name="username" placeholder="Username" {} - input type="email" name="email" placeholder="Email" {} - input type="password" name="password" placeholder="" {} - input type="password" name="password_confirm" placeholder="Confirm password" {} - input type="submit" value="Register" {} + + label for="email" { "Email" } + input type="email" name="email" id="email" required; + + label for="password" { "Password" } + input type="password" name="password" id="password" + required minlength="10" maxlength="128" + autocomplete="new-password"; + + label for="password_confirmation" { "Confirm password" } + input type="password" name="password_confirmation" + id="password_confirmation" required + autocomplete="new-password"; + + button type="submit" { "Register" } } } } diff --git a/web/src/web/service/routes/mod.rs b/web/src/web/service/routes/mod.rs index e9ccec3..c3bab35 100644 --- a/web/src/web/service/routes/mod.rs +++ b/web/src/web/service/routes/mod.rs @@ -1,47 +1,99 @@ mod root; mod auth; -use axum::extract::State; -use axum::http::header; -use axum::response::IntoResponse; -use axum::Router; +use std::sync::Arc; +use std::time::Instant; +use axum::extract::{Request, State}; +use axum::http::{header, StatusCode}; +use axum::middleware::Next; +use axum::response::{IntoResponse, Response}; +use axum::{middleware, Router}; +use axum::error_handling::HandleErrorLayer; use axum::routing::{get, post}; +use sqlx::{PgPool}; +use tower_csrf::{CrossOriginProtectionLayer, ProtectionError}; +use tower_governor::governor::GovernorConfig; use tower_http::compression::CompressionLayer; use tower_http::trace::TraceLayer; +use tower_sessions::Session; use crate::web::content::assets::static_handler; use crate::web::content::style; +use crate::web::service::auth::session::session_layer; use crate::web::service::{ AppState }; +use tower_governor::GovernorLayer; pub(crate) trait Route<'a, AD> { const PATH: &'static str; const NAME: &'static str = "MicroTLD"; - async fn get_content(state: State, query: AD) -> impl IntoResponse { + async fn process(session: Session, state: State, query: AD) -> impl IntoResponse { "You should fill out get_content for this route." } } -fn auth_routes() -> Router { - Router::new() - .route(auth::LoginPrompt::PATH, get(auth::LoginPrompt::get_content)) - .route(auth::LogoutPrompt::PATH, get(auth::LogoutPrompt::get_content)) - .route(auth::RegisterPrompt::PATH, get(auth::RegisterPrompt::get_content)) - .route(auth::Login::PATH, post(auth::Login::get_content)) - .route(auth::Logout::PATH, post(auth::Logout::get_content)) - .route(auth::Register::PATH, post(auth::Register::get_content)) +async fn my_custom_middleware( + req: Request, + next: Next, +) -> Response { + // Perform actions BEFORE the route handler runs + let start_time = Instant::now(); + let path = req.uri().path().to_string(); + + // Pass the request to the rest of the application + let response = next.run(req).await; + + // Perform actions AFTER the route handler runs + let duration = start_time.elapsed(); + println!("Request to '{}' took {:?}", path, duration); + + response } -pub fn apply_routes(router: axum::Router) -> Router { + + + +fn auth_routes() -> Router { + let governor_config = GovernorConfig::default(); // 1 request per 500ms per IP + let governor_layer = GovernorLayer::new(Arc::new(governor_config)); + + Router::new() + .layer(governor_layer) + .route(auth::LoginPrompt::PATH, get(auth::LoginPrompt::process)) + .route(auth::LogoutPrompt::PATH, get(auth::LogoutPrompt::process)) + .route(auth::RegisterPrompt::PATH, get(auth::RegisterPrompt::process)) + .route(auth::Login::PATH, post(auth::Login::process)) + .route(auth::Logout::PATH, post(auth::Logout::process)) + .route(auth::Register::PATH, post(auth::Register::process)) + .route(auth::Confirm::PATH, get(auth::Confirm::process)) +} + +pub async fn apply_routes(router: axum::Router, pool: PgPool) -> Router { let css = style::stylesheet(); - router.route(root::Root::PATH, get(root::Root::get_content)) + let csrf_layer = tower::ServiceBuilder::new() + .layer(HandleErrorLayer::new( + |error: Box| async move { + if error.downcast_ref::().is_some() { + (StatusCode::FORBIDDEN, "Cross-origin request blocked").into_response() + } else { + StatusCode::INTERNAL_SERVER_ERROR.into_response() + } + }, + )) + .layer(CrossOriginProtectionLayer::default()); + + router + .route(root::Root::PATH, get(root::Root::process)) .route(&css.route, get(css_handler)) .nest(auth::PATH, auth_routes()) .route("/assets/{*path}", get(static_handler)) + .layer(csrf_layer) + .layer(session_layer(pool).await) .layer(CompressionLayer::new()) .layer(TraceLayer::new_for_http()) + .route_layer(middleware::from_fn(my_custom_middleware)) } async fn css_handler() -> impl IntoResponse { diff --git a/web/src/web/service/routes/root.rs b/web/src/web/service/routes/root.rs index 91ae1b2..a5146f4 100644 --- a/web/src/web/service/routes/root.rs +++ b/web/src/web/service/routes/root.rs @@ -1,6 +1,7 @@ use axum::extract::State; use axum::response::IntoResponse; use maud::html; +use tower_sessions::Session; use crate::web::content::partials::page::page; use crate::web::service::AppState; use crate::web::service::routes::Route; @@ -10,8 +11,8 @@ pub(crate) struct Root; impl Route<'_, ()> for Root { const PATH: &'static str = "/"; - async fn get_content(State(_app_state): State, _query: ()) -> impl IntoResponse { - page(Self::NAME, Self::PATH, false, html! { + async fn process(session: Session, State(_app_state): State, _query: ()) -> impl IntoResponse { + page(Self::NAME, Self::PATH, !session.is_empty().await, html! { "Hello, world!" }) }