diff --git a/.vscode/settings.json b/.vscode/settings.json index e61586a..07c5f07 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,4 +2,8 @@ "rust-analyzer.diagnostics.disabled": [ "macro-error" ], + "rust-analyzer.cargo.noDefaultFeatures": false, + "rust-analyzer.cargo.features": [ + "sqlite" + ], } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index bdb3166..b748a35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,6 +146,24 @@ dependencies = [ "event-listener", ] +[[package]] +name = "async-process" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02111fd8655a613c25069ea89fc8d9bb89331fa77486eb3bc059ee757cfa481c" +dependencies = [ + "async-io", + "autocfg", + "blocking", + "cfg-if 1.0.0", + "event-listener", + "futures-lite", + "libc", + "once_cell", + "signal-hook", + "winapi", +] + [[package]] name = "async-session" version = "3.0.0" @@ -164,7 +182,7 @@ dependencies = [ "rand", "serde", "serde_json", - "sha2", + "sha2 0.9.9", ] [[package]] @@ -177,6 +195,7 @@ dependencies = [ "async-global-executor", "async-io", "async-lock", + "async-process", "crossbeam-utils", "futures-channel", "futures-core", @@ -210,6 +229,15 @@ dependencies = [ "syn", ] +[[package]] +name = "atoi" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.0.0" @@ -411,6 +439,12 @@ version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "bytes" version = "1.2.0" @@ -474,7 +508,7 @@ dependencies = [ "num-integer", "num-traits", "serde", - "time", + "time 0.1.44", "winapi", ] @@ -566,6 +600,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "cookie" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" +dependencies = [ + "time 0.3.14", + "version_check", +] + [[package]] name = "cpufeatures" version = "0.2.2" @@ -575,6 +619,31 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" + +[[package]] +name = "crossbeam-queue" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd42583b04998a5363558e5f9291ee5a5ff6b49944332103f251e7479a82aa7" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.10" @@ -667,6 +736,44 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dotenvy" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3db6fcad7c1fc4abdd99bf5276a4db30d6a819127903a709ed41e5ff016e84" +dependencies = [ + "dirs", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +dependencies = [ + "serde", +] + [[package]] name = "event-listener" version = "2.5.2" @@ -682,6 +789,18 @@ dependencies = [ "instant", ] +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "pin-project", + "spin 0.9.4", +] + [[package]] name = "fnv" version = "1.0.7" @@ -705,6 +824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -713,6 +833,28 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62007592ac46aa7c2b6416f7deb9a8a8f63a01e0f1d6e1787d5630170db2b63e" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.21" @@ -734,6 +876,17 @@ dependencies = [ "waker-fn", ] +[[package]] +name = "futures-rustls" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2411eed028cdf8c8034eaf21f9915f956b6c3abec4d4c7949ee67f0721127bd" +dependencies = [ + "futures-io", + "rustls", + "webpki", +] + [[package]] name = "futures-sink" version = "0.3.21" @@ -753,9 +906,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ "futures-core", + "futures-sink", "futures-task", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -858,6 +1013,18 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452c155cb93fecdfb02a73dd57b5d8e442c2063bd7aac72f1bc5e4263a43086" +dependencies = [ + "hashbrown", +] [[package]] name = "headers" @@ -884,6 +1051,15 @@ dependencies = [ "http", ] +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -893,6 +1069,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hmac" version = "0.11.0" @@ -967,6 +1149,17 @@ dependencies = [ "want", ] +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.9.1" @@ -986,6 +1179,15 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" @@ -1028,6 +1230,7 @@ dependencies = [ "axum-auth", "ciborium", "clap", + "cookie", "csv", "mime_guess", "recipe-store", @@ -1036,6 +1239,7 @@ dependencies = [ "rust-embed", "secrecy", "serde", + "sqlx", "tower", "tower-http", "tracing", @@ -1186,6 +1390,17 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "libsqlite3-sys" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libz-sys" version = "1.1.8" @@ -1197,6 +1412,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "lock_api" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.17" @@ -1321,6 +1546,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.13.0" @@ -1345,6 +1579,31 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + [[package]] name = "password-hash" version = "0.4.2" @@ -1502,6 +1761,26 @@ dependencies = [ "num-rational", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + [[package]] name = "regex" version = "1.6.0" @@ -1534,6 +1813,21 @@ dependencies = [ "gloo-net", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "rocksdb" version = "0.19.0" @@ -1574,7 +1868,7 @@ version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "756feca3afcbb1487a1d01f4ecd94cf8ec98ea074c55a69e7136d29fb6166029" dependencies = [ - "sha2", + "sha2 0.9.9", "walkdir", ] @@ -1584,6 +1878,27 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustls" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +dependencies = [ + "base64", +] + [[package]] name = "ryu" version = "1.0.10" @@ -1599,6 +1914,22 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "secrecy" version = "0.8.0" @@ -1676,6 +2007,17 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sha2" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899bf02746a2c92bf1053d9327dadb252b01af1f81f90cdb902411f518bc7215" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.3", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -1691,6 +2033,25 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.7" @@ -1716,12 +2077,136 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlformat" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b7922be017ee70900be125523f38bdd644f4f06a1b16e8fa5a8ee8c34bffd4" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "788841def501aabde58d3666fcea11351ec3962e6ea75dbcd05c84a71d68bcd1" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c21d3b5e7cadfe9ba7cdc1295f72cc556c750b4419c27c219c0693198901f8e" +dependencies = [ + "ahash", + "atoi", + "bitflags", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "hashlink", + "hex", + "indexmap", + "itoa 1.0.2", + "libc", + "libsqlite3-sys", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls", + "rustls-pemfile", + "serde", + "sha2 0.10.3", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "url", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4adfd2df3557bddd3b91377fc7893e8fa899e9b4061737cbade4e1bb85f1b45c" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.3", + "sqlx-core", + "sqlx-rt", + "syn", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be52fc7c96c136cedea840ed54f7d446ff31ad670c9dea95ebcb998530971a3" +dependencies = [ + "async-std", + "futures-rustls", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strsim" version = "0.10.0" @@ -1851,6 +2336,39 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" +dependencies = [ + "itoa 1.0.2", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + [[package]] name = "tokio" version = "1.20.0" @@ -2023,12 +2541,57 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + [[package]] name = "unicode-ident" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" +[[package]] +name = "unicode-normalization" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + [[package]] name = "uuid" version = "1.1.2" @@ -2184,6 +2747,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" +dependencies = [ + "webpki", +] + [[package]] name = "wepoll-ffi" version = "0.1.2" diff --git a/Makefile b/Makefile index 5241333..bbfc98d 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +sqlx-prepare: wasm kitches/src/*.rs + cd kitchen; cargo sqlx-prepare + kitchen: wasm kitchen/src/*.rs cd kitchen; cargo build diff --git a/flake.nix b/flake.nix index 747b0b6..1897b87 100644 --- a/flake.nix +++ b/flake.nix @@ -56,9 +56,7 @@ type = "app"; program = "${kitchen}/bin/kitchen"; }; - devShell = pkgs.mkShell { - buildInputs = [ rust-wasm ] ++ (with pkgs; [wasm-bindgen-cli wasm-pack]); - }; + devShell = pkgs.callPackage ./nix/devShell/default.nix { inherit rust-wasm; }; } ); } \ No newline at end of file diff --git a/kitchen/Cargo.toml b/kitchen/Cargo.toml index a03e8b4..648ecfd 100644 --- a/kitchen/Cargo.toml +++ b/kitchen/Cargo.toml @@ -17,9 +17,9 @@ mime_guess = "2.0.4" async-trait = "0.1.57" async-session = "3.0.0" ciborium = "0.2.0" -rocksdb = "0.19.0" tower = "0.4.13" serde = "1.0.144" +cookie = "0.16.0" [dependencies.argon2] version = "0.4.1" @@ -50,4 +50,18 @@ features = [ "cargo" ] [dependencies.async-std] version = "1.10.0" -features = ["tokio1"] \ No newline at end of file +features = ["tokio1"] + +[dependencies.sqlx] +version = "0.6.1" +features = ["sqlite", "runtime-async-std-rustls", "offline"] +optional = true + +[dependencies.rocksdb] +version = "0.19.0" +optional = true + +[features] +sqlite = ["dep:sqlx"] +rocksdb = ["dep:rocksdb"] +default = ["sqlite"] \ No newline at end of file diff --git a/kitchen/migrations/20220831185348_initial.down.sql b/kitchen/migrations/20220831185348_initial.down.sql new file mode 100644 index 0000000..84da0dd --- /dev/null +++ b/kitchen/migrations/20220831185348_initial.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here +drop table sessions; +drop table users; \ No newline at end of file diff --git a/kitchen/migrations/20220831185348_initial.up.sql b/kitchen/migrations/20220831185348_initial.up.sql new file mode 100644 index 0000000..bc17678 --- /dev/null +++ b/kitchen/migrations/20220831185348_initial.up.sql @@ -0,0 +1,3 @@ +-- Add migration script here +CREATE TABLE sessions(id TEXT PRIMARY KEY, session_value BLOB NOT NULL); +CREATE TABLE users(id TEXT PRIMARY KEY, password_hashed TEXT NOT NULL); \ No newline at end of file diff --git a/kitchen/sqlx-data.json b/kitchen/sqlx-data.json new file mode 100644 index 0000000..1f46458 --- /dev/null +++ b/kitchen/sqlx-data.json @@ -0,0 +1,79 @@ +{ + "db": "SQLite", + "104f07472670436d3eee1733578bbf0c92dc4f965d3d13f9bf4bfbc92958c5b6": { + "describe": { + "columns": [ + { + "name": "password_hashed", + "ordinal": 0, + "type_info": "Text" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "select password_hashed from users where id = ?" + }, + "5d743897fb0d8fd54c3708f1b1c6e416346201faa9e28823c1ba5a421472b1fa": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "insert into users (id, password_hashed) values (?, ?)" + }, + "7578157607967a6a4c60f12408c5d9900d15b429a49681a4cae4e02d31c524ec": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 1 + } + }, + "query": "delete from sessions where id = ?" + }, + "928a479ca0f765ec7715bf8784c5490e214486edbf5b78fd501823feb328375b": { + "describe": { + "columns": [ + { + "name": "session_value", + "ordinal": 0, + "type_info": "Blob" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "select session_value from sessions where id = ?" + }, + "9ad4acd9b9d32c9f9f441276aa71a17674fe4d65698848044778bd4aef77d42d": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "insert into sessions (id, session_value) values (?, ?)" + }, + "d84685a82585c5e4ae72c86ba1fe6e4a7241c4c3c9e948213e5849d956132bad": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 0 + } + }, + "query": "delete from sessions" + } +} \ No newline at end of file diff --git a/kitchen/src/main.rs b/kitchen/src/main.rs index dc05ea8..5582ee0 100644 --- a/kitchen/src/main.rs +++ b/kitchen/src/main.rs @@ -45,14 +45,14 @@ fn create_app<'a>() -> clap::App<'a> { (@subcommand serve => (about: "Serve the interface via the web") (@arg recipe_dir: -d --dir +takes_value "Directory containing recipe files to use") - (@arg session_dir: --sesion_dir + takes_value "Session store directory to use") + (@arg session_dir: --session_dir + takes_value "Session store directory to use") (@arg listen: --listen +takes_value "address and port to listen on 0.0.0.0:3030") ) (@subcommand add_user => (about: "add users to to the interface") (@arg user: -u --user +takes_value +required "username to add") (@arg pass: -p --pass +takes_value +required "password to add for this user") - (@arg session_dir: --sesion_dir +takes_value "Session store directory to use") + (@arg session_dir: --session_dir +takes_value "Session store directory to use") ) ) .setting(clap::AppSettings::SubcommandRequiredElseHelp) @@ -139,10 +139,13 @@ fn main() { }); } else if let Some(matches) = matches.subcommand_matches("add_user") { let session_store_path: PathBuf = get_session_store_path(matches); - web::add_user( - session_store_path, - matches.value_of("user").unwrap().to_owned(), - matches.value_of("pass").unwrap().to_owned(), - ); + async_std::task::block_on(async { + web::add_user( + session_store_path, + matches.value_of("user").unwrap().to_owned(), + matches.value_of("pass").unwrap().to_owned(), + ) + .await; + }); } } diff --git a/kitchen/src/web/auth.rs b/kitchen/src/web/auth.rs index ad594f4..9a30a35 100644 --- a/kitchen/src/web/auth.rs +++ b/kitchen/src/web/auth.rs @@ -20,22 +20,23 @@ use axum::{ response::IntoResponse, }; use axum_auth::AuthBasic; +use cookie::{Cookie, SameSite}; use secrecy::Secret; use tracing::{debug, info, instrument}; -use super::session; +use super::session::{self, AuthStore}; #[instrument(skip_all, fields(user=%auth.0.0))] pub async fn handler( auth: AuthBasic, - Extension(session_store): Extension, + Extension(session_store): Extension, ) -> impl IntoResponse { // NOTE(jwall): It is very important that you do **not** log the password // here. We convert the AuthBasic into UserCreds immediately to help prevent // that. Do not circumvent that protection. let auth = session::UserCreds::from(auth); info!("Handling authentication request"); - if let Ok(true) = session_store.check_user_creds(&auth) { + if let Ok(true) = session_store.check_user_creds(&auth).await { debug!("successfully authenticated user"); // 1. Create a session identifier. let mut session = Session::new(); @@ -44,17 +45,11 @@ pub async fn handler( let cookie_value = session_store.store_session(session).await.unwrap().unwrap(); let mut headers = HeaderMap::new(); // 3. Construct the Session Cookie. - // TODO(jwall): Find or Build a cookie builder. - headers.insert( - header::SET_COOKIE, - format!( - "{}={} SameSite=Strict Secure", - session::AXUM_SESSION_COOKIE_NAME, - cookie_value - ) - .parse() - .unwrap(), - ); + let cookie = Cookie::build(session::AXUM_SESSION_COOKIE_NAME, cookie_value) + .same_site(SameSite::Strict) + .secure(true) + .finish(); + headers.insert(header::SET_COOKIE, cookie.to_string().parse().unwrap()); // Respond with 200 OK (StatusCode::OK, headers, "Login Successful") } else { diff --git a/kitchen/src/web/mod.rs b/kitchen/src/web/mod.rs index 8764792..f3757b0 100644 --- a/kitchen/src/web/mod.rs +++ b/kitchen/src/web/mod.rs @@ -29,6 +29,8 @@ use tower::ServiceBuilder; use tower_http::trace::TraceLayer; use tracing::{debug, info, instrument}; +use session::AuthStore; + mod auth; mod session; @@ -113,8 +115,9 @@ async fn api_categories( result } -pub fn add_user(session_store_path: PathBuf, username: String, password: String) { - let session_store = session::RocksdbInnerStore::new(session_store_path) +pub async fn add_user(session_store_path: PathBuf, username: String, password: String) { + let session_store = session::SqliteStore::new(session_store_path) + .await .expect("Unable to create session_store"); let user_creds = session::UserCreds { id: session::UserId(username), @@ -122,6 +125,7 @@ pub fn add_user(session_store_path: PathBuf, username: String, password: String) }; session_store .store_user_creds(user_creds) + .await .expect("Failed to store user creds"); } @@ -133,7 +137,8 @@ pub async fn ui_main( ) { let store = Arc::new(recipe_store::AsyncFileStore::new(recipe_dir_path.clone())); //let dir_path = (&dir_path).clone(); - let session_store = session::RocksdbInnerStore::new(session_store_path) + let session_store = session::SqliteStore::new(session_store_path) + .await .expect("Unable to create session_store"); let router = Router::new() .route("/", get(|| async { Redirect::temporary("/ui/plan") })) diff --git a/kitchen/src/web/session.rs b/kitchen/src/web/session.rs index 23627aa..9ff4c1f 100644 --- a/kitchen/src/web/session.rs +++ b/kitchen/src/web/session.rs @@ -18,13 +18,16 @@ use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2, }; -use async_session::{async_trait, Session, SessionStore}; +use async_session::{Session, SessionStore}; +use async_trait::async_trait; use axum::{ extract::{Extension, FromRequest, RequestParts, TypedHeader}, headers::Cookie, http::StatusCode, }; use ciborium; +use cookie::{Cookie as CookieParse, SameSite}; +#[cfg(feature = "rocksdb")] use rocksdb::{ BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options, }; @@ -57,12 +60,40 @@ impl UserCreds { } } +fn make_id_key(cookie_value: &str) -> async_session::Result { + debug!("deserializing cookie"); + Ok(Session::id_from_cookie_value(cookie_value)?) +} + +#[instrument(skip_all, fields(hash=payload))] +fn check_pass(payload: &String, pass: &Secret) -> bool { + let parsed_hash = PasswordHash::new(&payload).expect("Invalid Password Hash"); + debug!(password_hash=?parsed_hash, "successfuly obtained password hash"); + let check = Argon2::default().verify_password(pass.expose_secret().as_bytes(), &parsed_hash); + if let Err(err) = &check { + debug!(err=?err, "Couldn't verify password"); + return false; + } + check.is_ok() +} + +#[async_trait] +pub trait AuthStore: SessionStore { + /// Check user credentials against the user store. + async fn check_user_creds(&self, user_creds: &UserCreds) -> async_session::Result; + + /// Insert or update user credentials in the user store. + async fn store_user_creds(&self, user_creds: UserCreds) -> async_session::Result<()>; +} + +#[cfg(feature = "rocksdb")] #[derive(Clone, Debug)] -pub struct RocksdbInnerStore { +pub struct RocksdbStore { db: Arc>, } -impl RocksdbInnerStore { +#[cfg(feature = "rocksdb")] +impl RocksdbStore { pub fn new>(name: P) -> Result { let session_cf_opts = Options::default(); let session_cf = ColumnFamilyDescriptor::new(SESSION_CF, session_cf_opts); @@ -87,57 +118,14 @@ impl RocksdbInnerStore { fn get_users_column_family_handle(&self) -> Option> { self.db.cf_handle(USER_CF) } - - fn make_id_key(cookie_value: &str) -> async_session::Result { - Ok(Session::id_from_cookie_value(cookie_value)?) - } - - #[instrument(fields(user=%user_creds.id.0), skip_all)] - pub fn check_user_creds(&self, user_creds: &UserCreds) -> async_session::Result { - info!("checking credentials for user"); - let cf_handle = self - .get_users_column_family_handle() - .expect(&format!("column family {} is missing", USER_CF)); - if let Some(payload) = self.db.get_cf(&cf_handle, user_creds.id.0.as_bytes())? { - debug!("Found user in credential store"); - let payload = String::from_utf8_lossy(payload.as_slice()).to_string(); - let parsed_hash = PasswordHash::new(&payload).expect("Invalid Password Hash"); - debug!(password_hash=?parsed_hash, "successfuly obtained password hash"); - let check = Argon2::default() - .verify_password(user_creds.pass.expose_secret().as_bytes(), &parsed_hash); - if let Err(err) = &check { - debug!(err=?err, "Couldn't verify password") - } - return Ok(check.is_ok()); - } - Ok(false) - } - - #[instrument(fields(user=%user_creds.id.0), skip_all)] - pub fn store_user_creds(&self, user_creds: UserCreds) -> async_session::Result<()> { - // TODO(jwall): Enforce a password length? - info!("storing credentials for user {}", user_creds.id.0); - let cf_handle = self - .get_users_column_family_handle() - .expect(&format!("column family {} is missing", USER_CF)); - let salt = SaltString::generate(&mut OsRng); - let password_hash = Argon2::default() - .hash_password(user_creds.pass.expose_secret().as_bytes(), &salt) - .expect("failed to hash password"); - self.db.put_cf( - &cf_handle, - user_creds.id.0.as_bytes(), - password_hash.to_string().as_bytes(), - )?; - Ok(()) - } } +#[cfg(feature = "rocksdb")] #[async_trait] -impl SessionStore for RocksdbInnerStore { +impl SessionStore for RocksdbStore { #[instrument] async fn load_session(&self, cookie_value: String) -> async_session::Result> { - let id = Self::make_id_key(&cookie_value)?; + let id = make_id_key(&cookie_value)?; let cf_handle = self .get_session_column_family_handle() .expect(&format!("column family {} is missing", SESSION_CF)); @@ -178,6 +166,49 @@ impl SessionStore for RocksdbInnerStore { } } +#[cfg(feature = "rocksdb")] +#[async_trait] +impl AuthStore for RocksdbStore { + #[instrument(fields(user=%user_creds.id.0), skip_all)] + async fn check_user_creds(&self, user_creds: &UserCreds) -> async_session::Result { + // TODO(jwall): Make this function asynchronous. + info!("checking credentials for user"); + let cf_handle = self + .get_users_column_family_handle() + .expect(&format!("column family {} is missing", USER_CF)); + if let Some(payload) = self + .db + .get_cf(&cf_handle, user_creds.user_id().as_bytes())? + { + debug!("Found user in credential store"); + let payload = String::from_utf8_lossy(payload.as_slice()).to_string(); + return Ok(check_pass(&payload, &user_creds.pass)); + } + Ok(false) + } + + // TODO(jwall): Make this function asynchronous. + #[instrument(fields(user=%user_creds.id.0), skip_all)] + async fn store_user_creds(&self, user_creds: UserCreds) -> async_session::Result<()> { + // TODO(jwall): Enforce a password length? + // TODO(jwall): Make this function asynchronous. + info!("storing credentials for user {}", user_creds.id.0); + let cf_handle = self + .get_users_column_family_handle() + .expect(&format!("column family {} is missing", USER_CF)); + let salt = SaltString::generate(&mut OsRng); + let password_hash = Argon2::default() + .hash_password(user_creds.pass.expose_secret().as_bytes(), &salt) + .expect("failed to hash password"); + self.db.put_cf( + &cf_handle, + user_creds.id.0.as_bytes(), + password_hash.to_string().as_bytes(), + )?; + Ok(()) + } +} + #[async_trait] impl FromRequest for UserIdFromSession where @@ -187,7 +218,7 @@ where #[instrument(skip_all)] async fn from_request(req: &mut RequestParts) -> Result { - let Extension(session_store) = Extension::::from_request(req) + let Extension(session_store) = Extension::::from_request(req) .await .expect("No Session store configured!"); let cookies = Option::>::from_request(req) @@ -197,20 +228,24 @@ where .as_ref() .and_then(|c| c.get(AXUM_SESSION_COOKIE_NAME)) { - if let Some(session) = session_store - .load_session(session_cookie.to_owned()) - .await - .unwrap() - { - if let Some(user_id) = session.get::("user_id") { - return Ok(Self::FoundUserId(user_id)); - } else { - error!("No user id found in session"); + debug!(?session_cookie, "processing session cookie"); + match session_store.load_session(session_cookie.to_owned()).await { + Ok(Some(session)) => { + if let Some(user_id) = session.get::("user_id") { + return Ok(Self::FoundUserId(user_id)); + } else { + error!("No user id found in session"); + return Ok(Self::NoUserId); + } + } + Ok(None) => { + debug!("no session defined in headers."); + return Ok(Self::NoUserId); + } + Err(e) => { + debug!(err=?e, "error deserializing session"); return Ok(Self::NoUserId); } - } else { - debug!("no session defined in headers."); - return Ok(Self::NoUserId); } } else { debug!("no cookies defined in headers."); @@ -218,3 +253,119 @@ where } } } + +#[cfg(feature = "sqlite")] +use sqlx::{ + self, + sqlite::{SqliteConnectOptions, SqliteJournalMode}, + SqlitePool, +}; +#[cfg(feature = "sqlite")] +use std::str::FromStr; + +#[cfg(feature = "sqlite")] +#[derive(Clone, Debug)] +pub struct SqliteStore { + pool: Arc, + url: String, +} + +#[cfg(feature = "sqlite")] +impl SqliteStore { + pub async fn new>(path: P) -> sqlx::Result { + let url = format!("sqlite://{}/store.db", path.as_ref().to_string_lossy()); + let options = SqliteConnectOptions::from_str(&url)?.journal_mode(SqliteJournalMode::Wal); + let pool = Arc::new(sqlx::SqlitePool::connect_with(options).await?); + Ok(Self { pool, url }) + } +} + +#[cfg(feature = "sqlite")] +#[async_trait] +impl SessionStore for SqliteStore { + #[instrument(fields(conn_string=self.url), skip_all)] + async fn load_session(&self, cookie_value: String) -> async_session::Result> { + let id = make_id_key(&cookie_value)?; + debug!(id, "fetching session from sqlite"); + if let Some(payload) = + sqlx::query_scalar!("select session_value from sessions where id = ?", id) + .fetch_optional(self.pool.as_ref()) + .await? + { + debug!(sesion_id = id, "found session key"); + let session: Session = ciborium::de::from_reader(payload.as_slice())?; + return Ok(Some(session)); + } + return Ok(None); + } + + #[instrument(fields(conn_string=self.url), skip_all)] + async fn store_session(&self, session: Session) -> async_session::Result> { + let id = session.id(); + let mut payload: Vec = Vec::new(); + ciborium::ser::into_writer(&session, &mut payload)?; + sqlx::query!( + "insert into sessions (id, session_value) values (?, ?)", + id, + payload + ) + .execute(self.pool.as_ref()) + .await?; + debug!(sesion_id = id, "successfully inserted session key"); + return Ok(session.into_cookie_value()); + } + + #[instrument(fields(conn_string=self.url), skip_all)] + async fn destroy_session(&self, session: Session) -> async_session::Result { + let id = session.id(); + sqlx::query!("delete from sessions where id = ?", id,) + .execute(self.pool.as_ref()) + .await?; + return Ok(()); + } + + #[instrument(fields(conn_string=self.url), skip_all)] + async fn clear_store(&self) -> async_session::Result { + sqlx::query!("delete from sessions") + .execute(self.pool.as_ref()) + .await?; + return Ok(()); + } +} + +#[cfg(feature = "sqlite")] +#[async_trait] +impl AuthStore for SqliteStore { + #[instrument(fields(user=%user_creds.id.0, conn_string=self.url), skip_all)] + async fn check_user_creds(&self, user_creds: &UserCreds) -> async_session::Result { + let id = user_creds.user_id().to_owned(); + if let Some(payload) = + sqlx::query_scalar!("select password_hashed from users where id = ?", id) + .fetch_optional(self.pool.as_ref()) + .await? + { + debug!("Testing password for user"); + return Ok(check_pass(&payload, &user_creds.pass)); + } + Ok(false) + } + + #[instrument(fields(user=%user_creds.id.0, conn_string=self.url), skip_all)] + async fn store_user_creds(&self, user_creds: UserCreds) -> async_session::Result<()> { + let salt = SaltString::generate(&mut OsRng); + let password_hash = Argon2::default() + .hash_password(user_creds.pass.expose_secret().as_bytes(), &salt) + .expect("failed to hash password"); + let id = user_creds.user_id().to_owned(); + let password_hashed = password_hash.to_string(); + debug!("adding password for user"); + sqlx::query!( + "insert into users (id, password_hashed) values (?, ?)", + id, + password_hashed, + ) + .execute(self.pool.as_ref()) + .await?; + Ok(()) + } +} diff --git a/nix/devShell/default.nix b/nix/devShell/default.nix new file mode 100644 index 0000000..49d1e22 --- /dev/null +++ b/nix/devShell/default.nix @@ -0,0 +1,6 @@ +{ pkgs, rust-wasm }: +with pkgs; +mkShell { + buildInputs = (if stdenv.isDarwin then [ pkgs.darwin.apple_sdk.frameworks.Security ] else [ ]) ++ (with pkgs; [wasm-bindgen-cli wasm-pack llvm clang rust-wasm]); + #buildInputs = with pkgs; [wasm-bindgen-cli wasm-pack]; +} \ No newline at end of file diff --git a/nix/kitchen/default.nix b/nix/kitchen/default.nix index bc93dc5..971c3a1 100644 --- a/nix/kitchen/default.nix +++ b/nix/kitchen/default.nix @@ -13,7 +13,7 @@ with pkgs; buildInputs = [ rust-wasm ]; # However the crate we are building has it's root in specific crate. src = root; - nativeBuildInputs = if stdenv.isDarwin then [ xcbuild ] else [ ]; + nativeBuildInputs = (if stdenv.isDarwin then [ xcbuild pkgs.darwin.apple_sdk.frameworks.Security ] else [ ]) ++ [llvm clang]; cargoBuildOptions = opts: opts ++ ["-p" "${pname}" ]; postPatch = '' mkdir -p web/dist diff --git a/web/src/pages/login.rs b/web/src/pages/login.rs index 2ab1d23..791dd5c 100644 --- a/web/src/pages/login.rs +++ b/web/src/pages/login.rs @@ -56,6 +56,7 @@ pub fn login_form() -> View { if username != "" && password != "" { spawn_local_in_scope(async move { debug!("authenticating against ui"); + // TODO(jwall): Navigate to plan if the below is successful. authenticate(username, password).await; }); }