commit bfc5cbf624edec635364f6a3827d89e277d6e542 Author: Avii Date: Sat Oct 12 14:36:36 2024 +0200 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5db2a06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/node_modules +/target +/.env \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a271808 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,19 @@ +{ + "rust-analyzer.procMacro.ignored": { + "leptos_macro": [ + // optional: + // "component", + "server" + ], + }, + // if code that is cfg-gated for the `ssr` feature is shown as inactive, + // you may want to tell rust-analyzer to enable the `ssr` feature by default + // + // you can also use `rust-analyzer.cargo.allFeatures` to enable all features + "rust-analyzer.cargo.features": [ + "ssr" + ], + "files.associations": { + "input.scss": "tailwindcss" + } +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d623f51 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4603 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "attribute-derive" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1ee502851995027b06f99f5ffbeffa1406b38d0b318a1ebfa469332c6cbafd" +dependencies = [ + "attribute-derive-macro", + "derive-where", + "manyhow", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3601467f634cfe36c4780ca9c75dea9a5b34529c1f2810676a337e7e0997f954" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils 0.8.0", + "proc-macro2", + "quote", + "quote-use", + "syn 2.0.79", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "avam" +version = "0.1.0" +dependencies = [ + "anyhow", + "argon2", + "axum", + "axum-macros", + "axum_session", + "axum_session_sqlx", + "derive_more", + "dotenvy", + "http 1.1.0", + "leptos", + "leptos_axum", + "leptos_meta", + "leptos_router", + "lettre", + "rand", + "serde", + "sqlx", + "tera", + "thiserror", + "tokio", + "tower 0.4.13", + "tower-http", + "tower-layer", + "tracing", + "tracing-subscriber", + "uuid", + "validator", +] + +[[package]] +name = "avam-wasm" +version = "0.1.0" +dependencies = [ + "avam", + "console_error_panic_hook", + "leptos", + "tracing-subscriber", + "tracing-subscriber-wasm", + "wasm-bindgen", +] + +[[package]] +name = "axum" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.1.0", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower 0.5.1", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.1", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "axum_session" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219e632039c0e9c7f53f135d7f0aebbfccc39c3a18af2034f789cd1cf0f0a463" +dependencies = [ + "aes-gcm", + "async-trait", + "axum", + "base64", + "bytes", + "chrono", + "cookie", + "dashmap 6.1.0", + "forwarded-header-value", + "futures", + "hmac", + "http 1.1.0", + "http-body", + "rand", + "serde", + "serde_json", + "sha2", + "thiserror", + "tokio", + "tower-layer", + "tower-service", + "tracing", + "uuid", +] + +[[package]] +name = "axum_session_sqlx" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1724c65bcbe5a8e9a238313ea07a74a6da59a05c9fd3e899a6b55add770a13c3" +dependencies = [ + "async-trait", + "axum_session", + "chrono", + "sqlx", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "cached" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90eb5776f28a149524d1d8623035760b4454ec881e8cf3838fa8d7e1b11254b3" +dependencies = [ + "cached_proc_macro", + "cached_proc_macro_types", + "hashbrown 0.13.2", + "instant", + "once_cell", + "thiserror", +] + +[[package]] +name = "cached_proc_macro" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c878c71c2821aa2058722038a59a67583a4240524687c6028571c9b395ded61f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" + +[[package]] +name = "cc" +version = "1.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.6", +] + +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.5", + "stacker", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "collection_literals" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "config" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +dependencies = [ + "convert_case", + "lazy_static", + "nom", + "pathdiff", + "serde", + "toml", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_format" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c655d81ff1114fb0dcdea9225ea9f0cc712a6f8d189378e82bdf62a473a64b" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff1a44b93f47b1bac19a27932f5c591e43d1ba357ee4f61526c8a25603f0eb1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "aes-gcm", + "base64", + "percent-encoding", + "rand", + "subtle", + "time", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive-where" +version = "1.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.79", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "drain_filter_polyfill" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] + +[[package]] +name = "email-encoding" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d1d33cdaede7e24091f039632eb5d3c7469fe5b066a985281a34fc70fa317f" +dependencies = [ + "base64", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +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", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "globset" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[package]] +name = "gloo" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d" +dependencies = [ + "gloo-console", + "gloo-dialogs", + "gloo-events", + "gloo-file", + "gloo-history", + "gloo-net 0.3.1", + "gloo-render", + "gloo-storage", + "gloo-timers", + "gloo-utils 0.1.7", + "gloo-worker", +] + +[[package]] +name = "gloo-console" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7" +dependencies = [ + "gloo-events", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f" +dependencies = [ + "gloo-events", + "gloo-utils 0.1.7", + "serde", + "serde-wasm-bindgen 0.5.0", + "serde_urlencoded", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.1.7", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http 1.1.0", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a" +dependencies = [ + "anymap2", + "bincode", + "gloo-console", + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "hostname" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +dependencies = [ + "cfg-if", + "libc", + "windows", +] + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd69211b9b519e98303c015e21a007e293db403b6c85b9b124e133d25e242cdd" +dependencies = [ + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.8", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "inventory" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leptos" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cbb3237c274dadf00dcc27db96c52601b40375117178fb24a991cda073624f0" +dependencies = [ + "cfg-if", + "leptos_config", + "leptos_dom", + "leptos_macro", + "leptos_reactive", + "leptos_server", + "server_fn", + "tracing", + "typed-builder", + "typed-builder-macro", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_axum" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "910681b920c48a43508b2bd0261bdb67c4ef9456a0b3613f956a0d30e832e9de" +dependencies = [ + "axum", + "cfg-if", + "futures", + "http-body-util", + "leptos", + "leptos_integration_utils", + "leptos_macro", + "leptos_meta", + "leptos_router", + "once_cell", + "parking_lot", + "serde_json", + "server_fn", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "leptos_config" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ed778611380ddea47568ac6ad6ec5158d39b5bd59e6c4dcd24efc15dc3dc0d" +dependencies = [ + "config", + "regex", + "serde", + "thiserror", + "typed-builder", +] + +[[package]] +name = "leptos_dom" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8401c46c86c1f4c16dcb7881ed319fcdca9cda9b9e78a6088955cb423afcf119" +dependencies = [ + "async-recursion", + "cfg-if", + "drain_filter_polyfill", + "futures", + "getrandom", + "html-escape", + "indexmap", + "itertools", + "js-sys", + "leptos_reactive", + "once_cell", + "pad-adapter", + "paste", + "rustc-hash", + "serde", + "serde_json", + "server_fn", + "smallvec", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "leptos_hot_reload" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb53d4794240b684a2f4be224b84bee9e62d2abc498cf2bcd643cd565e01d96" +dependencies = [ + "anyhow", + "camino", + "indexmap", + "parking_lot", + "proc-macro2", + "quote", + "rstml", + "serde", + "syn 2.0.79", + "walkdir", +] + +[[package]] +name = "leptos_integration_utils" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a96976631c2225ec116a7bf9c0ed5bf6999a19fed33f5e3cbcf37af44c384dc" +dependencies = [ + "futures", + "leptos", + "leptos_config", + "leptos_hot_reload", + "leptos_meta", + "tracing", +] + +[[package]] +name = "leptos_macro" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b13bc3db70715cd8218c4535a5af3ae3c0e5fea6f018531fc339377b36bc0e0" +dependencies = [ + "attribute-derive", + "cfg-if", + "convert_case", + "html-escape", + "itertools", + "leptos_hot_reload", + "prettyplease", + "proc-macro-error2", + "proc-macro2", + "quote", + "rstml", + "server_fn_macro", + "syn 2.0.79", + "tracing", + "uuid", +] + +[[package]] +name = "leptos_meta" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25acc2f63cf91932013e400a95bf6e35e5d3dbb44a7b7e25a8e3057d12005b3b" +dependencies = [ + "cfg-if", + "indexmap", + "leptos", + "tracing", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_reactive" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4161acbf80f59219d8d14182371f57302bc7ff81ee41aba8ba1ff7295727f23" +dependencies = [ + "base64", + "cfg-if", + "futures", + "indexmap", + "js-sys", + "oco_ref", + "paste", + "pin-project", + "rustc-hash", + "self_cell", + "serde", + "serde-wasm-bindgen 0.6.5", + "serde_json", + "slotmap", + "thiserror", + "tokio", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "leptos_router" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d71dea7d42c0d29c40842750232d3425ed1cf10e313a1f898076d20871dad32" +dependencies = [ + "cached", + "cfg-if", + "gloo-net 0.6.0", + "itertools", + "js-sys", + "lazy_static", + "leptos", + "leptos_integration_utils", + "leptos_meta", + "linear-map", + "lru", + "once_cell", + "percent-encoding", + "regex", + "send_wrapper", + "serde", + "serde_json", + "serde_qs 0.13.0", + "thiserror", + "tracing", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "leptos_server" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a97eb90a13f71500b831c7119ddd3bdd0d7ae0a6b0487cade4fddeed3b8c03f" +dependencies = [ + "inventory", + "lazy_static", + "leptos_macro", + "leptos_reactive", + "serde", + "server_fn", + "thiserror", + "tracing", +] + +[[package]] +name = "lettre" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f204773bab09b150320ea1c83db41dc6ee606a4bc36dc1f43005fe7b58ce06" +dependencies = [ + "async-trait", + "base64", + "chumsky", + "email-encoding", + "email_address", + "fastrand", + "futures-io", + "futures-util", + "hostname", + "httpdate", + "idna 1.0.2", + "mime", + "nom", + "percent-encoding", + "quoted_printable", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "socket2", + "tokio", + "tokio-rustls", + "url", + "webpki-roots", +] + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linear-map" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" +dependencies = [ + "serde", + "serde_test", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "manyhow" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91ea592d76c0b6471965708ccff7e6a5d277f676b90ab31f4d3f3fc77fade64" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "manyhow-macros" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64621e2c08f2576e4194ea8be11daf24ac01249a4f53cd8befcbb7077120ead" +dependencies = [ + "proc-macro-utils 0.8.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.1.0", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonempty" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "oco_ref" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51ebcefb2f0b9a5e0bea115532c8ae4215d1b01eff176d0f4ba4192895c2708" +dependencies = [ + "serde", + "thiserror", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pad-adapter" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d80efc4b6721e8be2a10a5df21a30fa0b470f1539e53d8b4e6e75faf938b63" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "pest_meta" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "pin-project" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[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.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn 2.0.79", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-utils" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f59e109e2f795a5070e69578c4dc101068139f74616778025ae1011d4cd41a8" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", + "version_check", + "yansi", +] + +[[package]] +name = "psm" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205" +dependencies = [ + "cc", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quote-use" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" +dependencies = [ + "quote", + "quote-use-macros", +] + +[[package]] +name = "quote-use-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" +dependencies = [ + "proc-macro-utils 0.10.0", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "quoted_printable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rstml" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe542870b8f59dd45ad11d382e5339c9a1047cde059be136a7016095bbdefa77" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.79", + "syn_derive", + "thiserror", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + +[[package]] +name = "serde_qs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_test" +version = "1.0.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "server_fn" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fae7a3038a32e5a34ba32c6c45eb4852f8affaf8b794ebfcd4b1099e2d62ebe" +dependencies = [ + "axum", + "bytes", + "ciborium", + "const_format", + "dashmap 5.5.3", + "futures", + "gloo-net 0.6.0", + "http 1.1.0", + "http-body-util", + "hyper", + "inventory", + "js-sys", + "once_cell", + "send_wrapper", + "serde", + "serde_json", + "serde_qs 0.12.0", + "server_fn_macro_default", + "thiserror", + "tower 0.4.13", + "tower-layer", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faaaf648c6967aef78177c0610478abb5a3455811f401f3c62d10ae9bd3901a1" +dependencies = [ + "const_format", + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.79", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2aa8119b558a17992e0ac1fd07f080099564f24532858811ce04f742542440" +dependencies = [ + "server_fn_macro", + "syn 2.0.79", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "serde", + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +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 = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" +dependencies = [ + "atoi", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.14.5", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.79", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.79", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "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", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stacker" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "tera" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" +dependencies = [ + "globwalk", + "lazy_static", + "pest", + "pest_derive", + "regex", + "serde", + "serde_json", + "unic-segment", +] + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "hashbrown 0.14.5", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http 1.1.0", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-subscriber-wasm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79804e80980173c6c8e53d98508eb24a2dbc4ee17a3e8d2ca8e5bad6bf13a898" +dependencies = [ + "gloo", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "typed-builder" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77739c880e00693faef3d65ea3aad725f196da38b22fdc7ea6ded6e1ce4d3add" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna 0.5.0", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom", + "rand", + "serde", +] + +[[package]] +name = "validator" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db79c75af171630a3148bd3e6d7c4f42b6a9a014c2945bc5ed0020cbb8d9478e" +dependencies = [ + "idna 0.5.0", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[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.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.79", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "wasm-streams" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall", + "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" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[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" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "xxhash-rust" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..77ddef2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,128 @@ +[workspace] +members = [".", "avam-wasm"] +resolver = "2" + +[package] +name = "avam" +version = "0.1.0" +edition = "2021" + +[lib] +name = "avam" +path = "src/lib/lib.rs" + +[[bin]] +name = "avam" +path = "src/bin/server/main.rs" + +# Defines a size-optimized profile for the WASM bundle in release mode +[profile.wasm-release] +strip = "debuginfo" +inherits = "release" +opt-level = 'z' +lto = true +codegen-units = 1 +panic = "abort" + +[features] +hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] + +ssr = [ + "dep:argon2", + "dep:dotenvy", + "dep:rand", + "dep:tokio", + "dep:tracing-subscriber", + "dep:leptos_axum", + "dep:lettre", + "dep:tera", + "dep:sqlx", + "dep:axum", + "dep:axum-macros", + "dep:axum_session", + "dep:axum_session_sqlx", + "dep:tower", + "dep:tower-http", + "dep:tower-layer", + + "leptos/ssr", + "leptos_meta/ssr", + "leptos_router/ssr", +] + +[dependencies] +# Utilities +anyhow = { version = "1.0.89", optional = false } +argon2 = { version = "0.5.3", optional = true } +derive_more = { version = "1.0.0", features = ["full"], optional = false } +dotenvy = { version = "0.15.7", optional = true } +rand = { version = "0.8.5", optional = true } +serde = { version = "1.0.210", features = ["std", "derive"], optional = false } +thiserror = { version = "1.0.64", optional = false } +tokio = { version = "1.40.0", features = ["full"], optional = true } +tracing = { version = "0.1.40", optional = false } +tracing-subscriber = { version = "0.3.18", features = [ + "env-filter", +], optional = true } +uuid = { version = "1.10.0", features = [ + "v4", + "fast-rng", + "serde", +], optional = false } + +# Leptos +leptos = { version = "0.6", features = ["nightly"], optional = false } +leptos_axum = { version = "0.6", optional = true } +leptos_meta = { version = "0.6", features = ["nightly"], optional = false } +leptos_router = { version = "0.6", features = ["nightly"], optional = false } + +# Email +lettre = { version = "0.11.9", default-features = false, features = [ + "builder", + "hostname", + "pool", + "rustls-tls", + "smtp-transport", + "tokio1", + "tokio1-rustls-tls", +], optional = true } +tera = { version = "1.20.0", default-features = false, optional = true } + +# Database +sqlx = { version = "0.8.2", default-features = false, features = [ + "uuid", + "runtime-tokio-rustls", + "macros", + "postgres", +], optional = true } + +# Web +axum = { version = "0.7.7", optional = true } +axum-macros = { version = "0.4.2", optional = true } +axum_session = { version = "0.14.0", optional = true } +axum_session_sqlx = { version = "0.3.0", optional = true } +tower = { version = "0.4", optional = true, features = ["util"] } +tower-http = { version = "0.6.1", features = ["trace", "fs"], optional = true } +tower-layer = { version = "0.3.3", optional = true } +http = "1" +validator = "0.18.1" + +[[workspace.metadata.leptos]] +name = "avam" +site-root = "target/site" +site-pkg-dir = "pkg" +style-file = "style/main.scss" +assets-dir = "public" +site-addr = "0.0.0.0:3000" +reload-port = 3001 +browserquery = "defaults" +watch = false +env = "DEV" + +bin-package = "avam" +bin-default-features = false +bin-features = ["ssr"] + +lib-package = "avam-wasm" +lib-default-features = false +lib-features = [] diff --git a/avam-wasm/Cargo.toml b/avam-wasm/Cargo.toml new file mode 100644 index 0000000..05284a9 --- /dev/null +++ b/avam-wasm/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "avam-wasm" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +avam = { path = "../", features = ["hydrate"] } + +console_error_panic_hook = "0.1" +leptos = { version = "0.6", features = ["nightly", "hydrate"] } +wasm-bindgen = "=0.2.93" + +tracing-subscriber = "0.3.18" +tracing-subscriber-wasm = { version = "0.1.0" } diff --git a/avam-wasm/src/lib.rs b/avam-wasm/src/lib.rs new file mode 100644 index 0000000..bfa50a1 --- /dev/null +++ b/avam-wasm/src/lib.rs @@ -0,0 +1,6 @@ +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + use avam::domain::leptos::app::*; + console_error_panic_hook::set_once(); + leptos::mount_to_body(App); +} diff --git a/migrations/20241009121432_user.down.sql b/migrations/20241009121432_user.down.sql new file mode 100644 index 0000000..6c689bc --- /dev/null +++ b/migrations/20241009121432_user.down.sql @@ -0,0 +1,2 @@ +-- Add migration script here +DROP TABLE "users"; \ No newline at end of file diff --git a/migrations/20241009121432_user.up.sql b/migrations/20241009121432_user.up.sql new file mode 100644 index 0000000..86df1f0 --- /dev/null +++ b/migrations/20241009121432_user.up.sql @@ -0,0 +1,13 @@ +-- Add migration script here +CREATE TABLE "users" ( + "id" uuid NOT NULL, + "email" text NOT NULL, + "password" text NOT NULL, + "verified" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "users_email_key" UNIQUE ("email"), + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +) WITH (oids = false); + +CREATE INDEX "users_email_idx" ON "users" USING btree ("email"); \ No newline at end of file diff --git a/migrations/20241009170101_activation_token.down.sql b/migrations/20241009170101_activation_token.down.sql new file mode 100644 index 0000000..d29462b --- /dev/null +++ b/migrations/20241009170101_activation_token.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +DROP TABLE "activation_token"; diff --git a/migrations/20241009170101_activation_token.up.sql b/migrations/20241009170101_activation_token.up.sql new file mode 100644 index 0000000..88e78de --- /dev/null +++ b/migrations/20241009170101_activation_token.up.sql @@ -0,0 +1,8 @@ +-- Add up migration script here +CREATE TABLE "activation_token" ( + "user_id" uuid NOT NULL, + "token" character varying(255) NOT NULL, + "created_at" timestamp DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "activation_token_user_id_token" PRIMARY KEY ("user_id", "token") +) WITH (oids = false); diff --git a/migrations/20241011204946_forgot_password.down.sql b/migrations/20241011204946_forgot_password.down.sql new file mode 100644 index 0000000..5de4ce8 --- /dev/null +++ b/migrations/20241011204946_forgot_password.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +DROP TABLE "forgot_password"; \ No newline at end of file diff --git a/migrations/20241011204946_forgot_password.up.sql b/migrations/20241011204946_forgot_password.up.sql new file mode 100644 index 0000000..17e6ece --- /dev/null +++ b/migrations/20241011204946_forgot_password.up.sql @@ -0,0 +1,8 @@ +-- Add up migration script here +CREATE TABLE "forgot_password" ( + "user_id" uuid NOT NULL, + "token" character varying(255) NOT NULL, + "created_at" timestamp DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "forgot_password_user_id_token" PRIMARY KEY ("user_id", "token") +) WITH (oids = false); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..728d157 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1724 @@ +{ + "name": "avam", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@tailwindcss/typography": "^0.5.15", + "daisyui": "^4.12.13" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz", + "integrity": "sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-selector-tokenizer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/culori": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz", + "integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/daisyui": { + "version": "4.12.13", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.13.tgz", + "integrity": "sha512-BnXyQoOByUF/7wSdIKubyhXxbtL8gxwY3u2cNMkxGP39TSVJqMmlItqtpY903fQnLI/NokC+bc+ZV+PEPsppPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-selector-tokenizer": "^0.8", + "culori": "^3", + "picocolors": "^1", + "postcss-js": "^4" + }, + "engines": { + "node": ">=16.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/daisyui" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz", + "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7bb2c75 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "devDependencies": { + "@tailwindcss/typography": "^0.5.15", + "daisyui": "^4.12.13" + } +} diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png new file mode 100644 index 0000000..214c071 Binary files /dev/null and b/public/android-chrome-192x192.png differ diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png new file mode 100644 index 0000000..eff0664 Binary files /dev/null and b/public/android-chrome-512x512.png differ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..23c2f6e Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/background.jpg b/public/background.jpg new file mode 100644 index 0000000..7407c3e Binary files /dev/null and b/public/background.jpg differ diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png new file mode 100644 index 0000000..e5d2dc2 Binary files /dev/null and b/public/favicon-16x16.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 0000000..a191cac Binary files /dev/null and b/public/favicon-32x32.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..9ce992d Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/site.webmanifest b/public/site.webmanifest new file mode 100644 index 0000000..45dc8a2 --- /dev/null +++ b/public/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/src/bin/server/main.rs b/src/bin/server/main.rs new file mode 100644 index 0000000..85895da --- /dev/null +++ b/src/bin/server/main.rs @@ -0,0 +1,44 @@ +#[cfg(feature = "ssr")] +#[tokio::main] +async fn main() -> anyhow::Result<()> { + use avam::config::Config; + use avam::domain::api; + use avam::inbound::http::{state::AppState, HttpServer}; + use avam::outbound::dangerous_lettre::DangerousLettre; + use avam::outbound::postgres::Postgres; + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + + dotenvy::dotenv().ok(); + + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::try_from_default_env()?) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let config = Config::from_env()?; + + // outbound + let postgres = Postgres::new(&config.database_url).await?; + let dangerous_lettre = DangerousLettre::new( + &config.smtp_host, + config.smtp_port, + &config.smtp_username, + &config.smtp_password, + &config.smtp_sender, + )?; + + let api_service = api::Service::new(postgres.clone(), dangerous_lettre); + + let app_state = AppState::new(api_service).await; + + let http_server = HttpServer::new(app_state, postgres.pool()).await?; + http_server.run().await +} + +#[cfg(not(feature = "ssr"))] +pub fn main() { + println!("Do run this main?!"); + // no client-side main function + // unless we want this to work with e.g., Trunk for a purely client-side app + // see lib.rs for hydration function instead +} diff --git a/src/lib/config.rs b/src/lib/config.rs new file mode 100644 index 0000000..501bd82 --- /dev/null +++ b/src/lib/config.rs @@ -0,0 +1,45 @@ +use std::env; + +use anyhow::Context; + +const DATABASE_URL_KEY: &str = "DATABASE_URL"; +const SMTP_HOST: &str = "SMTP_HOST"; +const SMTP_PORT: &str = "SMTP_PORT"; +const SMTP_USERNAME: &str = "SMTP_USERNAME"; +const SMTP_PASSWORD: &str = "SMTP_PASSWORD"; +const SMTP_SENDER: &str = "SMTP_SENDER"; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Config { + pub database_url: String, + + pub smtp_host: String, + pub smtp_port: u16, + pub smtp_username: String, + pub smtp_password: String, + pub smtp_sender: String, +} + +impl Config { + pub fn from_env() -> anyhow::Result { + let database_url = load_env(DATABASE_URL_KEY)?; + let smtp_host = load_env(SMTP_HOST)?; + let smtp_port = load_env(SMTP_PORT)?.parse()?; + let smtp_username = load_env(SMTP_USERNAME)?; + let smtp_password = load_env(SMTP_PASSWORD)?; + let smtp_sender = load_env(SMTP_SENDER)?; + + Ok(Config { + database_url, + smtp_host, + smtp_port, + smtp_username, + smtp_password, + smtp_sender, + }) + } +} + +fn load_env(key: &str) -> anyhow::Result { + env::var(key).with_context(|| format!("failed to load environment variable {}", key)) +} diff --git a/src/lib/domain/api/mod.rs b/src/lib/domain/api/mod.rs new file mode 100644 index 0000000..5d9cd8c --- /dev/null +++ b/src/lib/domain/api/mod.rs @@ -0,0 +1,38 @@ +pub mod models; + +#[cfg(feature = "ssr")] +pub mod ports; + +#[cfg(feature = "ssr")] +mod service; + +#[cfg(feature = "ssr")] +pub use service::Service; + +#[cfg(feature = "ssr")] +pub mod prelude { + use super::super::super::outbound::dangerous_lettre::DangerousLettre; + use super::super::super::outbound::postgres::Postgres; + + // This is too damn tightly coupled to pgsql and lettre now + // But so far, this is the only thing that actually works + pub type AppService = std::sync::Arc>; + + pub use super::models::user::*; + pub use super::ports::*; + pub use super::service::*; + + pub use crate::domain::leptos::flashbag::Alert; + pub use crate::domain::leptos::flashbag::Flash; + pub use crate::domain::leptos::flashbag::FlashBag; + pub use crate::domain::leptos::flashbag::FlashMessage; + + pub use axum_session::SessionAnySession as Session; +} + +#[cfg(not(feature = "ssr"))] +pub mod prelude { + pub use super::models::user::*; + pub use crate::domain::leptos::flashbag::Alert; + pub use crate::domain::leptos::flashbag::Flash; +} diff --git a/src/lib/domain/api/models/mod.rs b/src/lib/domain/api/models/mod.rs new file mode 100644 index 0000000..22d12a3 --- /dev/null +++ b/src/lib/domain/api/models/mod.rs @@ -0,0 +1 @@ +pub mod user; diff --git a/src/lib/domain/api/models/user.rs b/src/lib/domain/api/models/user.rs new file mode 100644 index 0000000..5c4546d --- /dev/null +++ b/src/lib/domain/api/models/user.rs @@ -0,0 +1,453 @@ +#[cfg(feature = "ssr")] +use anyhow::anyhow; + +#[cfg(feature = "ssr")] +use argon2::{ + password_hash::{rand_core::OsRng, SaltString}, + Argon2, PasswordHash, PasswordHasher, PasswordVerifier, +}; + +use derive_more::{derive::Display, From}; + +#[cfg(feature = "ssr")] +use rand::{distributions::Alphanumeric, Rng}; +use serde::{ser::SerializeStruct, Deserialize, Serialize}; +use thiserror::Error; + +/// A uniquely identifiable blog of blog blog. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)] +pub struct User { + id: uuid::Uuid, + email: EmailAddress, + #[cfg(feature = "ssr")] + password: Password, + verified: Verified, +} + +impl User { + pub fn new( + id: uuid::Uuid, + email: EmailAddress, + #[cfg(feature = "ssr")] password: Password, + verified: Verified, + ) -> Self { + Self { + id, + email, + #[cfg(feature = "ssr")] + password, + verified, + } + } + + pub fn id(&self) -> &uuid::Uuid { + &self.id + } + + pub fn email(&self) -> &EmailAddress { + &self.email + } + + #[cfg(feature = "ssr")] + pub fn password(&self) -> &Password { + &self.password + } + + pub fn verified(&self) -> &Verified { + &self.verified + } +} + +impl Serialize for User { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut state = serializer.serialize_struct("User", 3)?; + state.serialize_field("id", &self.id())?; + state.serialize_field("email", &self.email())?; + state.serialize_field("verified", &self.verified())?; + state.end() + } +} + +#[cfg(feature = "ssr")] +#[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)] +#[serde(from = "String")] +pub struct ActivationToken(String); + +#[cfg(feature = "ssr")] +impl From for ActivationToken { + fn from(value: String) -> Self { + Self(value) + } +} + +#[cfg(feature = "ssr")] +impl From<&str> for ActivationToken { + fn from(value: &str) -> Self { + Self(value.to_string()) + } +} + +#[cfg(feature = "ssr")] +impl ActivationToken { + pub fn new() -> Self { + let token: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(11) + .map(char::from) + .collect(); + + Self(token) + } +} + +#[cfg(feature = "ssr")] +#[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)] +#[serde(from = "String")] +pub struct PasswordResetToken(String); + +#[cfg(feature = "ssr")] +impl From for PasswordResetToken { + fn from(value: String) -> Self { + Self(value) + } +} + +#[cfg(feature = "ssr")] +impl From<&str> for PasswordResetToken { + fn from(value: &str) -> Self { + Self(value.to_string()) + } +} + +#[cfg(feature = "ssr")] +impl PasswordResetToken { + pub fn new() -> Self { + let token: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(24) + .map(char::from) + .collect(); + + Self(token) + } +} + +#[cfg(feature = "ssr")] +#[derive(Debug, Error)] +pub enum ResetPasswordError { + #[error("The reset token is no longer valid.")] + NotFound, + #[error(transparent)] + Unknown(#[from] anyhow::Error), + // to be extended as new error scenarios are introduced +} + +#[derive( + Clone, Copy, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +#[serde(from = "bool")] +pub struct Verified(bool); + +impl From for Verified { + fn from(value: bool) -> Self { + Self::new(value) + } +} + +impl Verified { + pub fn new(value: bool) -> Self { + Self(value) + } + + pub fn is_verified(&self) -> bool { + self.0 + } +} + +impl From for bool { + fn from(value: Verified) -> Self { + value.0 + } +} + +#[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(try_from = "String")] +/// A valid email address. +pub struct EmailAddress(String); + +impl TryFrom for EmailAddress { + type Error = EmailAddressError; + fn try_from(value: String) -> Result { + Self::new(&value) + } +} + +impl TryFrom<&str> for EmailAddress { + type Error = EmailAddressError; + fn try_from(value: &str) -> Result { + Self::new(value) + } +} + +#[derive(Clone, Debug, Error)] +#[error("{invalid_email} is not a valid email address")] +pub struct EmailAddressError { + pub invalid_email: String, +} + +impl From for String { + fn from(value: EmailAddressError) -> Self { + format!("{}", value) + } +} + +impl EmailAddress { + pub fn new(raw: &str) -> Result { + let trimmed = raw.trim(); + Self::validate_email_address(trimmed)?; + Ok(Self(trimmed.to_string())) + } + + fn validate_email_address(email: &str) -> Result<(), EmailAddressError> { + if !email.contains('@') { + return Err(EmailAddressError { + invalid_email: email.to_string(), + }); + } + + if email.contains(' ') { + return Err(EmailAddressError { + invalid_email: email.to_string(), + }); + } + + if email.len() < 3 { + // a@b is technically valid, a@ or @b is not + return Err(EmailAddressError { + invalid_email: email.to_string(), + }); + } + + use validator::ValidateEmail; + if !email.validate_email() { + return Err(EmailAddressError { + invalid_email: email.to_string(), + }); + } + + Ok(()) + } +} + +#[cfg(feature = "ssr")] +#[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)] +#[serde(try_from = "String")] +/// A valid password. +pub struct Password { + hashed_password: String, +} + +#[cfg(feature = "ssr")] +impl TryFrom for Password { + type Error = PasswordError; + fn try_from(value: String) -> Result { + Self::new(&value) + } +} + +#[cfg(feature = "ssr")] +impl TryFrom<&str> for Password { + type Error = PasswordError; + fn try_from(value: &str) -> Result { + Self::new(value) + } +} + +#[cfg(feature = "ssr")] +impl Password { + pub fn new(password: &str) -> Result { + // password restrictions + if password.len() < 6 { + return Err(PasswordError::TooShort); + } + + // Do we need to protect people from being stupid? + + let salt = SaltString::generate(&mut OsRng); + let hashed_password = Argon2::default() + .hash_password(password.as_bytes(), &salt) + .map_err(|e| PasswordError::Unknown(anyhow!(e)))? + .to_string(); + + Ok(Self { hashed_password }) + } + + pub fn new_hashed(hashed_password: &str) -> Self { + Self { + hashed_password: hashed_password.to_string(), + } + } + + pub fn verify(&self, password: &str) -> Result<(), anyhow::Error> { + let hash = PasswordHash::new(&self.hashed_password).map_err(|e| anyhow!(e))?; + Argon2::default() + .verify_password(password.as_bytes(), &hash) + .map_err(|e| anyhow!(e)) + } +} + +#[cfg(feature = "ssr")] +#[derive(Debug, Error)] +pub enum PasswordError { + #[error("Password must be at least 6 characters long.")] + TooShort, + #[error(transparent)] + Unknown(#[from] anyhow::Error), + // to be extended as new error scenarios are introduced +} + +#[cfg(feature = "ssr")] +/// The fields required by the domain to create a [User]. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, From)] +pub struct CreateUserRequest { + email: EmailAddress, + password: Password, +} + +#[cfg(feature = "ssr")] +impl CreateUserRequest { + pub fn new(email: EmailAddress, password: Password) -> Self { + Self { email, password } + } + + pub fn email(&self) -> &EmailAddress { + &self.email + } + + pub fn password(&self) -> &Password { + &self.password + } +} + +#[cfg(feature = "ssr")] +#[derive(Debug, Error)] +pub enum CreateUserError { + #[error("User with email {email} already exists")] + Duplicate { email: EmailAddress }, + #[error(transparent)] + Unknown(#[from] anyhow::Error), + // to be extended as new error scenarios are introduced +} + +#[cfg(feature = "ssr")] +/// The fields required by the domain to create a [User]. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, From)] +pub struct UserLoginRequest { + email: EmailAddress, + password: String, +} + +#[cfg(feature = "ssr")] +impl UserLoginRequest { + pub fn new(email: EmailAddress, password: String) -> Self { + Self { email, password } + } + + pub fn email(&self) -> &EmailAddress { + &self.email + } + + pub fn password(&self) -> &str { + &self.password + } +} + +#[cfg(feature = "ssr")] +pub struct UpdateUserRequest { + email: Option, + password: Option, + verified: Option, +} + +#[cfg(feature = "ssr")] +impl UpdateUserRequest { + pub fn new() -> Self { + Self { + email: None, + password: None, + verified: None, + } + } + + pub fn email(&self) -> Option<&EmailAddress> { + self.email.as_ref() + } + + pub fn set_email(mut self, email: EmailAddress) -> Self { + self.email = Some(email); + self + } + + pub fn set_password(mut self, password: Password) -> Self { + self.password = Some(password); + self + } + + pub fn password(&self) -> Option<&Password> { + self.password.as_ref() + } + + pub fn set_verified(mut self, verified: Verified) -> Self { + self.verified = Some(verified); + self + } + + pub fn verified(&self) -> Option<&Verified> { + self.verified.as_ref() + } +} + +#[cfg(feature = "ssr")] +#[derive(Debug, Error)] +pub enum UpdateUserError { + #[error("User with email {email} already exists")] + Duplicate { email: EmailAddress }, + #[error(transparent)] + Unknown(#[from] anyhow::Error), + // to be extended as new error scenarios are introduced +} + +#[cfg(feature = "ssr")] +#[derive(Debug, Error)] +pub enum DeleteUserError { + #[error("User with id {id} not found")] + NotFound { id: uuid::Uuid }, + #[error(transparent)] + Unknown(#[from] anyhow::Error), + // to be extended as new error scenarios are introduced +} + +#[cfg(feature = "ssr")] +#[derive(Debug, Error)] +pub enum ActivateUserError { + #[error("Token {token} is invalid")] + NotFound { token: ActivationToken }, + #[error(transparent)] + Unknown(#[from] anyhow::Error), + // to be extended as new error scenarios are introduced +} + +#[cfg(feature = "ssr")] +#[derive(Debug, Error)] +pub enum UserLoginError { + #[error("User with email {email} not found")] + NotFound { email: EmailAddress }, + #[error("User with email {email} not verified")] + NotActive { email: EmailAddress }, + #[error(transparent)] + Unknown(#[from] anyhow::Error), + // to be extended as new error scenarios are introduced +} diff --git a/src/lib/domain/api/ports.rs b/src/lib/domain/api/ports.rs new file mode 100644 index 0000000..13b9ae3 --- /dev/null +++ b/src/lib/domain/api/ports.rs @@ -0,0 +1,133 @@ +/* + Module `ports` specifies the API by which external modules interact with the user domain. + + All traits are bounded by `Send + Sync + 'static`, since their implementations must be shareable + between request-handling threads. + + Trait methods are explicitly asynchronous, including `Send` bounds on response types, + since the application is expected to always run in a multithreaded environment. +*/ + +use std::future::Future; + +use super::models::user::*; + +/// `ApiService` is the public API for the user domain. +/// +/// External modules must conform to this contract – the domain is not concerned with the +/// implementation details or underlying technology of any external code. +pub trait ApiService: Clone + Send + Sync + 'static { + /// Asynchronously create a new [User]. + /// + /// # Errors + /// + /// - [CreateUserError::Duplicate] if an [User] with the same [EmailAddress] already exists. + fn create_user( + &self, + req: CreateUserRequest, + ) -> impl Future> + Send; + + // fn activate_user + + fn get_user_session( + &self, + session: &axum_session::SessionAnySession, // TODO: Get rid of this and make cleaner + ) -> impl Future> + Send; + + fn activate_user_account( + &self, + token: ActivationToken, + ) -> impl Future> + Send; + + fn user_login( + &self, + req: UserLoginRequest, + ) -> impl Future> + Send; + + fn forgot_password(&self, email: &EmailAddress) -> impl Future + Send; + + fn reset_password( + &self, + token: &PasswordResetToken, + password: &Password, + ) -> impl Future> + Send; + + fn find_user_by_password_reset_token( + &self, + token: &PasswordResetToken, + ) -> impl Future> + Send; + + // These shouldnt be here, _why_ are they here, and implement that here instead + // fn find_user_by_email(self, email: EmailAddress) -> impl Future> + Send; + // fn find_user_by_id(&self, user_id: uuid::Uuid) -> impl Future> + Send; +} + +pub trait UserRepository: Clone + Send + Sync + 'static { + // Create + fn create_user( + &self, + req: CreateUserRequest, + ) -> impl Future> + Send; + + fn create_activation_token( + &self, + ent: &User, + ) -> impl Future> + Send; + + fn create_password_reset_token( + &self, + ent: &User, + ) -> impl Future> + Send; + + // Read + fn all_users(&self) -> impl Future> + Send; + + fn find_user_by_id( + &self, + id: uuid::Uuid, + ) -> impl Future, anyhow::Error>> + Send; + + fn find_user_by_email( + &self, + email: &EmailAddress, + ) -> impl Future, anyhow::Error>> + Send; + + fn find_user_by_activation_token( + &self, + token: &ActivationToken, + ) -> impl Future, anyhow::Error>> + Send; + + fn find_user_by_password_reset_token( + &self, + token: &PasswordResetToken, + ) -> impl Future, anyhow::Error>> + Send; + + // // Update + fn update_user( + &self, + ent: &User, + req: UpdateUserRequest, + ) -> impl Future> + Send; + + // Delete + // fn delete_user(&self, ent: User) -> impl Future> + Send; + fn delete_activation_token_for_user( + &self, + ent: &User, + ) -> impl Future> + Send; + + fn delete_password_reset_tokens_for_user( + &self, + ent: &User, + ) -> impl Future> + Send; +} + +pub trait UserNotifier: Clone + Send + Sync + 'static { + fn user_created(&self, user: &User, token: &ActivationToken) + -> impl Future + Send; + fn forgot_password( + &self, + user: &User, + token: &PasswordResetToken, + ) -> impl Future + Send; +} diff --git a/src/lib/domain/api/service.rs b/src/lib/domain/api/service.rs new file mode 100644 index 0000000..10128f2 --- /dev/null +++ b/src/lib/domain/api/service.rs @@ -0,0 +1,193 @@ +/*! + Module `service` provides the canonical implementation of the [ApiService] port. All + user-domain logic is defined here. +*/ +use axum_session::SessionAnySession; + +use super::models::user::*; + +use super::ports::{ApiService, UserNotifier, UserRepository}; + +pub trait Repository = UserRepository; +pub trait Email = UserNotifier; + +#[derive(Debug, Clone)] +pub struct Service +where + R: Repository, + N: Email, +{ + repo: R, + notifier: N, +} + +impl Service +where + R: Repository, + N: Email, +{ + pub fn new(repo: R, notifier: N) -> Self { + Self { repo, notifier } + } +} + +impl ApiService for Service +where + R: UserRepository, + N: Email, +{ + async fn create_user(&self, req: CreateUserRequest) -> Result { + let result = self.repo.create_user(req).await; + + if result.is_err() { + // something went wrong, log the error + // but keep passing on the result to the requester (http server) + return result; + } + + let user = result.as_ref().unwrap(); + + let token = self.repo.create_activation_token(user).await?; + + // generate a activation token and send an email + self.notifier.user_created(user, &token).await; + + result + } + + async fn get_user_session(&self, session: &SessionAnySession) -> Option { + let Some(user_id) = session.get("user") else { + return None; + }; + + self.repo.find_user_by_id(user_id).await.unwrap_or(None) + } + + async fn activate_user_account( + &self, + token: ActivationToken, + ) -> Result { + let user = match self.repo.find_user_by_activation_token(&token).await { + Ok(u) => u, + Err(e) => return Err(ActivateUserError::Unknown(e.into())), + }; + + let Some(user) = user else { + return Err(ActivateUserError::NotFound { token }); + }; + + let req = UpdateUserRequest::new().set_verified(Verified::new(true)); + + let update = self.repo.update_user(&user, req).await; + + if let Err(e) = update { + return Err(ActivateUserError::Unknown(e.into())); + } + + self.repo.delete_activation_token_for_user(&user).await?; + + let (_, user) = update.unwrap(); + + Ok(user) + } + + async fn user_login(&self, req: UserLoginRequest) -> Result { + let email = req.email(); + + let user = match self.repo.find_user_by_email(email).await { + Ok(u) => u, + Err(e) => { + tracing::error!("{:#?}", e); + return Err(UserLoginError::Unknown(e.into())); + } + }; + + let Some(user) = user else { + tracing::warn!("User not found"); + return Err(UserLoginError::NotFound { + email: email.clone(), + }); + }; + + if !user.verified().is_verified() { + return Err(UserLoginError::NotActive { + email: email.clone(), + }); + } + + if let Err(e) = user.password().verify(req.password()) { + tracing::warn!("{:#?}", e); + return Err(UserLoginError::NotFound { + email: email.clone(), + }); + } + + // Invalidate any reset tokens that might exist + let _ = self.repo.delete_password_reset_tokens_for_user(&user).await; + + Ok(user) + } + + async fn forgot_password(&self, email: &EmailAddress) { + let user = match self.repo.find_user_by_email(email).await { + Ok(u) => u, + Err(e) => { + tracing::error!("{:#?}", e); + return; + } + }; + + let Some(user) = user else { + tracing::warn!("User not found"); + return; + }; + + let token = match self.repo.create_password_reset_token(&user).await { + Ok(t) => t, + Err(e) => { + tracing::error!("{:#?}", e); + return; + } + }; + + self.notifier.forgot_password(&user, &token).await; + } + + async fn reset_password( + &self, + token: &PasswordResetToken, + password: &Password, + ) -> Result { + let user = match self.repo.find_user_by_password_reset_token(token).await { + Ok(u) => u, + Err(e) => { + tracing::error!("{:#?}", e); + return Err(ResetPasswordError::Unknown(e.into())); + } + }; + + let Some(user) = user else { + tracing::warn!("User not found"); + return Err(ResetPasswordError::NotFound); + }; + + let req = UpdateUserRequest::new().set_password(password.clone()); + + self.repo + .update_user(&user, req) + .await + .map_err(|e| ResetPasswordError::Unknown(e.into()))?; + + // Invalidate any reset tokens that might exist + let _ = self.repo.delete_password_reset_tokens_for_user(&user).await; + + Ok(user) + } + + async fn find_user_by_password_reset_token(&self, token: &PasswordResetToken) -> Option { + self.repo + .find_user_by_password_reset_token(token) + .await + .unwrap_or(None) + } +} diff --git a/src/lib/domain/leptos/app.rs b/src/lib/domain/leptos/app.rs new file mode 100644 index 0000000..30c349c --- /dev/null +++ b/src/lib/domain/leptos/app.rs @@ -0,0 +1,89 @@ +mod components; +mod pages; + +use leptos::*; +use leptos_meta::*; +use leptos_router::*; +use pages::{ + auth::{ + forgot::ForgotPage, login::LoginPage, logout::LogoutPage, register::RegisterPage, + reset::ResetPage, + }, + dashboard::DashboardPage, + error::{AppError, ErrorTemplate}, +}; + +#[component] +pub fn App() -> impl IntoView { + provide_meta_context(); + + let trigger_user = create_rw_signal(true); + + let user = create_resource(trigger_user, move |_| async move { + super::check_user().await.unwrap() + }); + + view! { + // https://fontawesome.com/v5/search + + + + + + + + + + + + + <Router fallback=|| { + let mut outside_errors = Errors::default(); + outside_errors.insert_with_default_key(AppError::NotFound); + view! { + <ErrorTemplate outside_errors/> + } + .into_view() + }> + <main class="h-screen overflow-auto dark:base-100 dark:text-white"> + <Routes> + <Route path="/auth" view=move || { + view! { + <Suspense> + <Show when=move || user().is_some_and(|u| u.is_some())> + <Redirect path="/" /> + </Show> + </Suspense> + <Outlet /> + } + }> + <Route path="login" view=move || view! { <LoginPage user_signal=trigger_user /> } /> + <Route path="register" view=RegisterPage /> + <Route path="forgot" view=ForgotPage /> + <Route path="reset/:token" view=ResetPage /> + </Route> // auth + + <Route path="" view=move || { + view! { + <Suspense> + <Show when=move || user().is_some_and(|u| u.is_none())> + <Redirect path="/auth/login" /> + </Show> + </Suspense> + <Outlet /> + } + }> + <Route path="" view=move || view! { + <Suspense> + <Show when=move || user().is_some_and(|u| u.is_some())> + <DashboardPage user={ user().unwrap().unwrap() } /> + </Show> + </Suspense> + } /> + </Route> // dashboard + <Route path="/auth/logout" view=move || view! { <LogoutPage user_signal=trigger_user /> } /> + </Routes> + </main> + </Router> + } +} diff --git a/src/lib/domain/leptos/app/components/alert.rs b/src/lib/domain/leptos/app/components/alert.rs new file mode 100644 index 0000000..a16cdd8 --- /dev/null +++ b/src/lib/domain/leptos/app/components/alert.rs @@ -0,0 +1,42 @@ +use crate::domain::api::prelude::Alert; +use leptos::*; + +#[component] +pub fn Alert(alert: Alert, children: Children) -> impl IntoView { + match alert { + Alert::None => view! { + <div role="alert" class="alert my-2"> + <span>{ children() }</span> + </div> + } + .into_view(), + Alert::Info => view! { + <div role="alert" class="alert alert-info my-2"> + <i class="fas fa-info-circle"></i> + <span>{ children() }</span> + </div> + } + .into_view(), + Alert::Success => view! { + <div role="alert" class="alert alert-success my-2"> + <i class="fas fa-check-circle"></i> + <span>{ children() }</span> + </div> + } + .into_view(), + Alert::Warning => view! { + <div role="alert" class="alert alert-warning my-2"> + <i class="fas fa-exclamation-triangle"></i> + <span>{ children() }</span> + </div> + } + .into_view(), + Alert::Error => view! { + <div role="alert" class="alert alert-error my-2"> + <i class="fas fa-exclamation-circle"></i> + <span>{ children() }</span> + </div> + } + .into_view(), + } +} diff --git a/src/lib/domain/leptos/app/components/mod.rs b/src/lib/domain/leptos/app/components/mod.rs new file mode 100644 index 0000000..811b2ab --- /dev/null +++ b/src/lib/domain/leptos/app/components/mod.rs @@ -0,0 +1 @@ +pub mod alert; diff --git a/src/lib/domain/leptos/app/pages/auth/auth_base.rs b/src/lib/domain/leptos/app/pages/auth/auth_base.rs new file mode 100644 index 0000000..eb41c88 --- /dev/null +++ b/src/lib/domain/leptos/app/pages/auth/auth_base.rs @@ -0,0 +1,36 @@ +use leptos::*; + +/// Renders the home page of your application. +#[component] +pub fn AuthBase(children: Children) -> impl IntoView { + view! { + <div class="h-full flex flex-row"> + + <div class="w-full md:basis-1/3 2xl:basis-1/4 flex h-full flex-col px-4 py-16"> + <div class="flex h-full grow-0 flex-col px-4 py-16"> + + <div class="mt-auto"> + + <img class="mx-auto" src="/android-chrome-192x192.png" alt={ crate::PROJECT_NAME }/> + + { children() } + </div> + + <div class="mt-auto text-center text-sm text-gray-500"> + <p>{ crate::COPYRIGHT }</p> + <p class=""> + <span class="px-1"><a href="https://git.avii.nl/AVAM/avam" class="link" target="_BLANK"><i class="fab fa-git-alt"></i></a></span> + <span class="px-1"><a href="#" class="link" target="_BLANK"><i class="fab fa-discord"></i></a></span> + </p> + </div> + + </div> + </div> + + <div class="md:basis-2/3 2xl:basis-3/4 bg-cover bg-center bg-auth"></div> + + </div> + + } + .into_view() +} diff --git a/src/lib/domain/leptos/app/pages/auth/forgot.rs b/src/lib/domain/leptos/app/pages/auth/forgot.rs new file mode 100644 index 0000000..02c0c60 --- /dev/null +++ b/src/lib/domain/leptos/app/pages/auth/forgot.rs @@ -0,0 +1,81 @@ +use leptos::*; +use leptos_router::*; + +use super::auth_base::AuthBase; + +#[server] +async fn forgot_action(email: String) -> Result<(), ServerFnError<String>> { + use crate::domain::api::prelude::*; + + let email = EmailAddress::new(&email).map_err(|_| format!("\"{}\" is not a email address", email))?; + + let app = use_context::<AppService>().unwrap(); + let flashbag = use_context::<FlashBag>().unwrap(); + + // If the email address is known, we'll set a recovery link, if it's not, we wont, but the flash remains the same. + app.forgot_password(&email).await; + + let flash = FlashMessage::new("login", + format!( + "An e-mail has been sent to {} with a link to reset your password.", + email + )).with_alert(Alert::Success); + + flashbag.set(flash); + + leptos_axum::redirect("/auth/login"); + + Ok(()) +} + +/// Renders the home page of your application. +#[component] +pub fn ForgotPage() -> impl IntoView { + let submit = Action::<ForgotAction, _>::server(); + let response = submit.value().read_only(); + + view! { + <AuthBase> + + <Suspense> + <Show when=move || response.get().is_some_and(|e| e.is_err())> + <div role="alert" class="alert alert-error my-2"> + <i class="fas fa-exclamation-circle"></i> + <span> + { move || if let Some(Err(e)) = response.get() { + {format!("{}", e)}.into_view() + } else { + ().into_view() + }} + </span> + </div> + </Show> + </Suspense> + + <ActionForm action=submit class="w-full"> + <div class="flex flex-col gap-2"> + <label class="input input-bordered flex items-center gap-2"> + <i class="fas fa-envelope"></i> + <input type="text" placeholder="E-mail" name="email" class="grow" /> + </label> + + <div> + <input type="submit" value="Request Password Reset" class="btn btn-primary btn-block" /> + </div> + + <div class="text-center text-sm"> + "New Account? Sign up "<a href="/auth/register" class="link">"here"</a>"!" + </div> + + <div class="text-center text-sm"> + "Remembered your password? Login "<a href="/auth/login" class="link">"here"</a>"!" + </div> + </div> + + </ActionForm> + + </AuthBase> + + } + .into_view() +} diff --git a/src/lib/domain/leptos/app/pages/auth/login.rs b/src/lib/domain/leptos/app/pages/auth/login.rs new file mode 100644 index 0000000..9e4876a --- /dev/null +++ b/src/lib/domain/leptos/app/pages/auth/login.rs @@ -0,0 +1,106 @@ +use leptos::*; +use leptos_router::*; + +use crate::domain::leptos::app::components::alert::Alert; + +use super::auth_base::AuthBase; + +#[server] +async fn login_action(email: String, password: String) -> Result<(), ServerFnError<String>> { + use crate::domain::api::prelude::*; + + let app = use_context::<AppService>().unwrap(); + let session = use_context::<Session>().unwrap(); + + let email = email.try_into().map_err(|e| format!("{}", e))?; + + let user = match app.user_login(UserLoginRequest::new(email, password)).await { + Ok(u) => u, + Err(_) => return Err(ServerFnError::WrappedServerError("Username or password incorrect".into())), + }; + + session.set("user", user.id()); + + Ok(()) +} + +/// Renders the home page of your application. +#[component] +pub fn LoginPage(user_signal: RwSignal<bool>) -> impl IntoView { + let submit = Action::<LoginAction, _>::server(); + let response = submit.value().read_only(); + + create_effect(move |_| { + if response.get().is_some() { + user_signal.set(!user_signal.get_untracked()); + } + }); + + let flash = create_resource( + || (), + move |_| async move { + use crate::domain::leptos::get_flash; + + get_flash("login".to_string()).await.unwrap() + }, + ); + + view! { + <AuthBase> + + <Suspense> + <Show when=move || response.get().is_some_and(|e| e.is_err())> + <div role="alert" class="alert alert-error my-2"> + <i class="fas fa-exclamation-circle"></i> + <span> + { move || if let Some(Err(e)) = response.get() { + {format!("{}", e)}.into_view() + } else { + ().into_view() + }} + </span> + </div> + </Show> + </Suspense> + + <Suspense> + <Show when=move || flash().is_some_and(|u| u.is_some())> + + <Alert alert={ flash().unwrap().unwrap().alert() }> + { flash().unwrap().unwrap().message() } + </Alert> + + </Show> + </Suspense> + + <ActionForm action=submit class="w-full"> + <div class="flex flex-col gap-2"> + <label class="input input-bordered flex items-center gap-2"> + <i class="fas fa-envelope"></i> + <input type="text" placeholder="E-mail" name="email" class="grow" /> + </label> + + <label class="input input-bordered flex items-center gap-2"> + <i class="fas fa-lock"></i> + <input type="password" placeholder="●●●●●●●●" name="password" class="grow" on:input=move |_| submit.value().set(None) /> + </label> + + <div> + <input type="submit" value="Login" class="btn btn-primary btn-block" /> + </div> + + <div class="text-center text-sm"> + "New Account? Sign up "<a href="/auth/register" class="link">"here"</a>"!" + </div> + <div class="text-center text-sm"> + <a href="/auth/forgot" class="link">"Forgot password"</a> + </div> + </div> + + </ActionForm> + + </AuthBase> + + } + .into_view() +} diff --git a/src/lib/domain/leptos/app/pages/auth/logout.rs b/src/lib/domain/leptos/app/pages/auth/logout.rs new file mode 100644 index 0000000..f261ac3 --- /dev/null +++ b/src/lib/domain/leptos/app/pages/auth/logout.rs @@ -0,0 +1,34 @@ +use leptos::*; +use leptos_router::Redirect; + +#[server] +async fn logout_action() -> Result<(), ServerFnError<String>> { + use crate::domain::api::prelude::*; + + let session = use_context::<Session>().unwrap(); + session.clear(); + + Ok(()) +} + +#[component] +pub fn LogoutPage(user_signal: RwSignal<bool>) -> impl IntoView { + let submit = Action::<LogoutAction, _>::server(); + let response = submit.value().read_only(); + + create_effect(move |_| { + if response.get().is_some() { + user_signal.set(!user_signal.get_untracked()); + } + }); + + submit.dispatch(LogoutAction {}); + + view! { + <Suspense> + <Show when=move || response().is_some()> + <Redirect path="/" /> + </Show> + </Suspense> + } +} diff --git a/src/lib/domain/leptos/app/pages/auth/mod.rs b/src/lib/domain/leptos/app/pages/auth/mod.rs new file mode 100644 index 0000000..f6b8abe --- /dev/null +++ b/src/lib/domain/leptos/app/pages/auth/mod.rs @@ -0,0 +1,6 @@ +pub mod auth_base; +pub mod forgot; +pub mod login; +pub mod logout; +pub mod register; +pub mod reset; diff --git a/src/lib/domain/leptos/app/pages/auth/register.rs b/src/lib/domain/leptos/app/pages/auth/register.rs new file mode 100644 index 0000000..1c3079b --- /dev/null +++ b/src/lib/domain/leptos/app/pages/auth/register.rs @@ -0,0 +1,98 @@ +use leptos::*; +use leptos_router::*; + + +use super::auth_base::AuthBase; + + + +#[server] +async fn register_action( + email: String, + password: String, + confirm_password: String, +) -> Result<(), ServerFnError<String>> { + use crate::domain::api::prelude::*; + + let app = use_context::<AppService>().unwrap(); + let flashbag = use_context::<FlashBag>().unwrap(); + + if password != confirm_password { + return Err("Passwords don't match".to_string().into()); + } + + let email = EmailAddress::new(&email).map_err(|_| format!("\"{}\" is not a email address", email))?; + let password = Password::new(&password).map_err(|e| format!("{}", e))?; + + let user = app.create_user(CreateUserRequest::new(email, password)) + .await + .map_err(|e| format!("{}", e))?; + + let flash = FlashMessage::new("login", + format!( + "An e-mail has been sent to {} with a link to activate your account.", + user.email() + )).with_alert(Alert::Success); + + flashbag.set(flash); + + leptos_axum::redirect("/auth/login"); + + Ok(()) +} + +/// Renders the home page of your application. +#[component] +pub fn RegisterPage() -> impl IntoView { + let submit = Action::<RegisterAction, _>::server(); + + let response = submit.value().read_only(); + + view! { + <AuthBase> + + <Suspense> + <Show when=move || response.get().is_some_and(|e| e.is_err())> + <div role="alert" class="alert alert-error my-2"> + <i class="fas fa-exclamation-circle"></i> + <span> + { move || if let Some(Err(e)) = response.get() { + {format!("{}", e)}.into_view() + } else { + ().into_view() + }} + </span> + </div> + </Show> + </Suspense> + + <ActionForm action=submit class="w-full"> + <div class="flex flex-col gap-2"> + <label class="input input-bordered flex items-center gap-2"> + <i class="fas fa-envelope"></i> + <input type="text" placeholder="E-mail" name="email" class="grow" /> + </label> + + <label class="input input-bordered flex items-center gap-2"> + <i class="fas fa-lock"></i> + <input type="password" placeholder="Password" name="password" class="grow" /> + </label> + + <label class="input input-bordered flex items-center gap-2"> + <i class="fas fa-lock"></i> + <input type="password" placeholder="Confirm Password" name="confirm_password" class="grow" /> + </label> + + <div> + <input type="submit" value="Sign Up!" class="btn btn-primary btn-block" /> + </div> + + <div class="text-center text-sm"> + "Already have an Account? Login "<a href="/auth/login" class="link">"here"</a>"!" + </div> + </div> + + </ActionForm> + </AuthBase> + }.into_view() +} diff --git a/src/lib/domain/leptos/app/pages/auth/reset.rs b/src/lib/domain/leptos/app/pages/auth/reset.rs new file mode 100644 index 0000000..c997d1e --- /dev/null +++ b/src/lib/domain/leptos/app/pages/auth/reset.rs @@ -0,0 +1,108 @@ +use leptos::*; +use leptos_router::*; + +use super::auth_base::AuthBase; + +#[server] +async fn reset_action(token: String, password: String, confirm_password: String) -> Result<(), ServerFnError<String>> { + use crate::domain::api::prelude::*; + + let app = use_context::<AppService>().unwrap(); + let flashbag = use_context::<FlashBag>().unwrap(); + + if password != confirm_password { + return Err("Passwords don't match".to_string().into()); + } + + let password = Password::new(&password).map_err(|e| format!("{}", e))?; + + app.reset_password(&token.into(), &password).await.map_err(|e| format!("{}", e))?; + + let flash = FlashMessage::new("login", + format!( + "Your password has been reset." + )).with_alert(Alert::Success); + + flashbag.set(flash); + + leptos_axum::redirect("/auth/login"); + + Ok(()) +} + +/// Renders the home page of your application. +#[component] +pub fn ResetPage() -> impl IntoView { + let submit = Action::<ResetAction, _>::server(); + let response = submit.value().read_only(); + + let params = use_params_map(); + + let token = move || { + params.with(|params| params.get("token").cloned()) + }; + + let user = create_resource(token, move |_| async move { + crate::domain::leptos::check_forgot_password_token(token()).await.unwrap() + }); + + view! { + <AuthBase> + + <Suspense> + <Show when=move || response.get().is_some_and(|e| e.is_err())> + <div role="alert" class="alert alert-error my-2"> + <i class="fas fa-exclamation-circle"></i> + <span> + { move || if let Some(Err(e)) = response.get() { + {format!("{}", e)}.into_view() + } else { + ().into_view() + }} + </span> + </div> + </Show> + </Suspense> + + <ActionForm action=submit class="w-full"> + <div class="flex flex-col gap-2"> + + <Suspense> + <Show when=move || user().is_some_and(|e| e.is_none())> + <Redirect path="/auth/login"/> + </Show> + <Show when=move || user().is_some_and(|e| e.is_some())> + <label class="input input-bordered flex items-center gap-2"> + <i class="fas fa-envelope"></i> + <input type="text" class="grow" value={user().unwrap().unwrap().email().to_string()} disabled /> + </label> + </Show> + </Suspense> + + <label class="input input-bordered flex items-center gap-2"> + <i class="fas fa-lock"></i> + <input type="password" placeholder="New Password" name="password" class="grow" /> + </label> + + <label class="input input-bordered flex items-center gap-2"> + <i class="fas fa-lock"></i> + <input type="password" placeholder="Confirm Password" name="confirm_password" class="grow" /> + </label> + + <div> + <input type="hidden" name="token" value={token()} /> + <input type="submit" value="Reset!" class="btn btn-primary btn-block" /> + </div> + + <div class="text-center text-sm"> + "Remembered your password? Login "<a href="/auth/login" class="link">"here"</a>"!" + </div> + </div> + + </ActionForm> + + </AuthBase> + + } + .into_view() +} diff --git a/src/lib/domain/leptos/app/pages/dashboard.rs b/src/lib/domain/leptos/app/pages/dashboard.rs new file mode 100644 index 0000000..e091cd1 --- /dev/null +++ b/src/lib/domain/leptos/app/pages/dashboard.rs @@ -0,0 +1,46 @@ +use leptos::*; + +use crate::domain::api::prelude::User; + +/// Renders the home page of your application. +#[component] +pub fn DashboardPage(user: User) -> impl IntoView { + view! { + <section class="login is-fullheight"> + <div class="columns is-fullheight"> + <div class="column is-one-third-fullhd is-half-widescreen"> + <div class="login_container"> + <div style="margin: auto 0"> + <div class="has-text-centered"> + <img src="/android-chrome-192x192.png" alt={ crate::PROJECT_NAME }/> + </div> + + <pre>Hello, { user.email().to_string() }!</pre> + <div class="content has-text-centered"> + <a href="/auth/logout">Logout</a> + </div> + + </div> + + <footer> + <div class="content has-text-centered"> + <p> + { crate::PROJECT_NAME } + </p> + <p> + <span class="icon"><a href="https://git.avii.nl/AVAM/avam" class="is-link" target="_BLANK"><i class="fab fa-git-alt"></i></a></span> + <span class="icon"><a href="#" class="is-link" target="_BLANK"><i class="fab fa-discord"></i></a></span> + </p> + </div> + </footer> + + </div> + + </div> + + <div class="column is-fullheight background is-hidden-mobile has-background-primary has-text-primary-invert"> + </div> + </div> + </section> + }.into_view() +} diff --git a/src/lib/domain/leptos/app/pages/error.rs b/src/lib/domain/leptos/app/pages/error.rs new file mode 100644 index 0000000..afab043 --- /dev/null +++ b/src/lib/domain/leptos/app/pages/error.rs @@ -0,0 +1,73 @@ +use http::status::StatusCode; +use leptos::*; +use thiserror::Error; + +#[derive(Clone, Debug, Error)] +pub enum AppError { + #[error("Not Found")] + NotFound, +} + +impl AppError { + pub fn status_code(&self) -> StatusCode { + match self { + AppError::NotFound => StatusCode::NOT_FOUND, + } + } +} + +// A basic function to display errors served by the error boundaries. +// Feel free to do more complicated things here than just displaying the error. +#[component] +pub fn ErrorTemplate( + #[prop(optional)] outside_errors: Option<Errors>, + #[prop(optional)] errors: Option<RwSignal<Errors>>, +) -> impl IntoView { + let errors = match outside_errors { + Some(e) => create_rw_signal(e), + None => match errors { + Some(e) => e, + None => panic!("No Errors found and we expected errors!"), + }, + }; + // Get Errors from Signal + let errors = errors.get_untracked(); + + // Downcast lets us take a type that implements `std::error::Error` + let errors: Vec<AppError> = errors + .into_iter() + .filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned()) + .collect(); + println!("Errors: {errors:#?}"); + + // Only the response code for the first error is actually sent from the server + // this may be customized by the specific application + #[cfg(feature = "ssr")] + { + use leptos_axum::ResponseOptions; + let response = use_context::<ResponseOptions>(); + if let Some(response) = response { + response.set_status(errors[0].status_code()); + } + } + + view! { + <For + // a function that returns the items we're iterating over; a signal is fine + each= move || {errors.clone().into_iter().enumerate()} + // a unique key for each item as a reference + key=|(index, _error)| *index + // renders each item to a view + children=move |error| { + let error_string = error.1.to_string(); + let error_code= error.1.status_code(); + view! { + <div class="container"> + <p class="title">{error_code.to_string()}</p> + <p>"Error: " {error_string}</p> + </div> + } + } + /> + } +} diff --git a/src/lib/domain/leptos/app/pages/mod.rs b/src/lib/domain/leptos/app/pages/mod.rs new file mode 100644 index 0000000..d7c0147 --- /dev/null +++ b/src/lib/domain/leptos/app/pages/mod.rs @@ -0,0 +1,3 @@ +pub mod auth; +pub mod dashboard; +pub mod error; diff --git a/src/lib/domain/leptos/flashbag.rs b/src/lib/domain/leptos/flashbag.rs new file mode 100644 index 0000000..0d074e8 --- /dev/null +++ b/src/lib/domain/leptos/flashbag.rs @@ -0,0 +1,107 @@ +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "ssr")] +use axum_session::SessionAnySession; + +#[derive(Clone, Serialize, Deserialize)] +pub enum Alert { + None, + Info, + Success, + Warning, + Error, +} + +#[derive(Clone, Serialize)] +pub struct FlashMessage<S> +where + S: Serialize, +{ + name: String, + message: S, + alert: Alert, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct Flash { + name: String, + message: String, + alert: Alert, +} + +impl Flash { + pub fn message(&self) -> String { + self.message.clone() + } + + pub fn alert(&self) -> Alert { + self.alert.clone() + } +} + +impl<S> FlashMessage<S> +where + S: Serialize, +{ + pub fn new(name: &str, message: S) -> Self { + Self { + name: name.to_string(), + message: message.into(), + alert: Alert::None, + } + } + + pub fn with_alert(mut self, alert: Alert) -> Self { + self.alert = alert; + self + } +} + +#[cfg(feature = "ssr")] +#[derive(Clone)] +pub struct FlashBag { + session: SessionAnySession, +} + +#[cfg(feature = "ssr")] +impl FlashBag { + pub fn new(session: SessionAnySession) -> Self { + Self { session } + } + + pub fn set<T: Serialize>(&self, message: FlashMessage<T>) { + let flash_name = &message.name; + + let name = format!("__flash_alert:{}__", flash_name); + self.session.set(&name, message.alert); + + let name = format!("__flash:{}__", flash_name); + self.session.set(&name, message.message); + } + + pub fn get(&self, flash_name: &str) -> Option<Flash> { + let name = format!("__flash:{}__", flash_name); + + let Some(message) = self.session.get(&name) else { + return None; + }; + + let alert_name = format!("__flash_alert:{}__", flash_name); + let alert = self.session.get(&alert_name).unwrap_or(Alert::None); + + self.clear(&flash_name); + + Some(Flash { + name, + message, + alert, + }) + } + + pub fn clear(&self, name: &str) { + let name = format!("__flash:{}__", name); + let alert_name = format!("__flash_alert:{}__", name); + self.session.remove(&name); + self.session.remove(&alert_name); + } +} diff --git a/src/lib/domain/leptos/mod.rs b/src/lib/domain/leptos/mod.rs new file mode 100644 index 0000000..1cc02cf --- /dev/null +++ b/src/lib/domain/leptos/mod.rs @@ -0,0 +1,41 @@ +use super::api::prelude::Flash; +use leptos::*; + +pub mod app; +pub mod flashbag; + +#[server] +pub async fn get_flash(name: String) -> Result<Option<Flash>, ServerFnError<String>> { + use super::api::prelude::*; + + // use crate::flashbag::FlashBag; + let Some(session) = use_context::<FlashBag>() else { + return Ok(None); + }; + + Ok(session.get(&name)) +} + +#[server] +pub async fn check_user() -> Result<Option<crate::domain::api::prelude::User>, ServerFnError<String>> +{ + use crate::domain::api::prelude::*; + + let app = use_context::<AppService>().unwrap(); + let session = use_context::<Session>().unwrap(); + + Ok(app.get_user_session(&session).await) +} + +#[server] +pub async fn check_forgot_password_token( + token: Option<String>, +) -> Result<Option<crate::domain::api::prelude::User>, ServerFnError<String>> { + use crate::domain::api::prelude::*; + + let app = use_context::<AppService>().unwrap(); + + let Some(token) = token else { return Ok(None) }; + + Ok(app.find_user_by_password_reset_token(&(token.into())).await) +} diff --git a/src/lib/domain/mod.rs b/src/lib/domain/mod.rs new file mode 100644 index 0000000..a99edea --- /dev/null +++ b/src/lib/domain/mod.rs @@ -0,0 +1,2 @@ +pub mod api; +pub mod leptos; diff --git a/src/lib/inbound/http.rs b/src/lib/inbound/http.rs new file mode 100644 index 0000000..bff9943 --- /dev/null +++ b/src/lib/inbound/http.rs @@ -0,0 +1,90 @@ +pub mod handlers; +pub mod state; + +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + +use anyhow::Context; +use axum::{ + extract::ConnectInfo, + routing::{get, post}, + Extension, +}; +use axum_session::{SessionAnyPool, SessionConfig, SessionLayer, SessionStore}; +use axum_session_sqlx::SessionPgPool; +use handlers::{ + fileserv::file_and_error_handler, + leptos::{leptos_routes_handler, server_fn_handler}, + user::activate_account, +}; +use leptos_axum::{generate_route_list, LeptosRoutes}; +use state::AppState; +use tokio::net; + +use crate::domain::{api::ports::ApiService, leptos::app::App}; + +/// The application's HTTP server. The underlying HTTP package is opaque to module consumers. +pub struct HttpServer { + router: axum::Router, + listener: net::TcpListener, +} + +impl HttpServer { + /// Returns a new HTTP server bound to the port specified in `config`. + pub async fn new<S: ApiService>(app_state: AppState<S>, pool: sqlx::Pool<sqlx::Postgres>) -> anyhow::Result<Self> { + let leptos_options = &app_state.leptos_options; + let addr = leptos_options.site_addr; + + let session_config = SessionConfig::default(); + + let session_pool = SessionAnyPool::new(SessionPgPool::from(pool)); + + let session_store = SessionStore::<SessionAnyPool>::new(Some(session_pool), session_config) + .await + .unwrap(); + + let trace_layer = tower_http::trace::TraceLayer::new_for_http().make_span_with( + |request: &axum::extract::Request<_>| { + let uri = request.uri().to_string(); + + let remote_addr = match request.headers().get("x-forwarded-for") { + Some(a) => a.to_str().unwrap_or("127.0.0.1").parse().unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)), + None => { + let socket_addr = request.extensions().get::<ConnectInfo<SocketAddr>>(); + socket_addr.unwrap().ip() + } + }; + + tracing::info_span!("http_request", remote_addr = ?remote_addr, method = ?request.method(), uri) + }, + ); + + let router = axum::Router::new() + .route("/auth/activate/:token", get(activate_account)) + .route("/api/*fn_name", post(server_fn_handler)) + .leptos_routes_with_handler(generate_route_list(App), get(leptos_routes_handler)) + .fallback(file_and_error_handler) + .layer(Extension(app_state.api_service())) + .layer(SessionLayer::new(session_store)) + .layer(trace_layer) + .with_state(app_state); + + let listener = net::TcpListener::bind(&addr) + .await + .with_context(|| format!("failed to listen on {}", &addr))?; + + Ok(Self { router, listener }) + } + + /// Runs the HTTP server. + pub async fn run(self) -> anyhow::Result<()> { + tracing::debug!("listening on {}", self.listener.local_addr().unwrap()); + axum::serve( + self.listener, + self.router + .into_make_service_with_connect_info::<SocketAddr>(), + ) + .await + .context("received error from running server")?; + Ok(()) + } +} diff --git a/src/lib/inbound/http/handlers/fileserv.rs b/src/lib/inbound/http/handlers/fileserv.rs new file mode 100644 index 0000000..40b9c64 --- /dev/null +++ b/src/lib/inbound/http/handlers/fileserv.rs @@ -0,0 +1,74 @@ +use axum::response::Response as AxumResponse; +use axum::{ + body::Body, + extract::State, + http::{Request, Response, StatusCode}, + response::IntoResponse, +}; +use leptos::*; +use tower::ServiceExt; +use tower_http::services::ServeDir; + +use crate::domain::api::ports::ApiService; +use crate::domain::api::prelude::FlashBag; +use crate::domain::leptos::app::App; +use crate::inbound::http::state::AppState; + +pub async fn file_and_error_handler<S: ApiService>( + State(options): State<LeptosOptions>, + State(app_state): State<AppState<S>>, + session: axum_session::SessionAnySession, + req: Request<Body>, +) -> AxumResponse { + let root = options.site_root.clone(); + let (parts, body) = req.into_parts(); + + let mut static_parts = parts.clone(); + static_parts.headers.clear(); + if let Some(encodings) = parts.headers.get("accept-encoding") { + static_parts + .headers + .insert("accept-encoding", encodings.clone()); + } + + let res = get_static_file(Request::from_parts(static_parts, Body::empty()), &root) + .await + .unwrap(); + + if res.status() == StatusCode::OK { + res.into_response() + } else { + let handler = leptos_axum::render_app_to_stream_with_context( + options.to_owned(), + move || { + provide_context(app_state.api_service().clone()); + provide_context(session.clone()); + provide_context(FlashBag::new(session.clone())); + }, + App, + ); + handler(Request::from_parts(parts, body)) + .await + .into_response() + } +} + +async fn get_static_file( + request: Request<Body>, + root: &str, +) -> Result<Response<Body>, (StatusCode, String)> { + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` + // This path is relative to the cargo root + match ServeDir::new(root) + .precompressed_gzip() + .precompressed_br() + .oneshot(request) + .await + { + Ok(res) => Ok(res.into_response()), + Err(err) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Error serving files: {err}"), + )), + } +} diff --git a/src/lib/inbound/http/handlers/leptos.rs b/src/lib/inbound/http/handlers/leptos.rs new file mode 100644 index 0000000..d2c3cfc --- /dev/null +++ b/src/lib/inbound/http/handlers/leptos.rs @@ -0,0 +1,48 @@ +use axum::{ + body::Body, + extract::{Request, State}, + response::{IntoResponse, Response}, +}; +use leptos::*; +use leptos_axum::handle_server_fns_with_context; + +use crate::{ + domain::{ + api::{ports::ApiService, prelude::FlashBag}, + leptos::app::App, + }, + inbound::http::state::AppState, +}; + +pub(crate) async fn server_fn_handler<S: ApiService>( + State(app_state): State<AppState<S>>, + session: axum_session::SessionAnySession, + request: Request<Body>, +) -> impl axum::response::IntoResponse { + handle_server_fns_with_context( + move || { + provide_context(app_state.api_service().clone()); + provide_context(session.clone()); + provide_context(FlashBag::new(session.clone())); + }, + request, + ) + .await +} + +pub(crate) async fn leptos_routes_handler<S: ApiService>( + State(app_state): State<AppState<S>>, + session: axum_session::SessionAnySession, + req: Request<Body>, +) -> Response { + let handler = leptos_axum::render_app_to_stream_with_context( + app_state.leptos_options.clone(), + move || { + provide_context(app_state.api_service().clone()); + provide_context(session.clone()); + provide_context(FlashBag::new(session.clone())); + }, + || view! { <App/> }, + ); + handler(req).await.into_response() +} diff --git a/src/lib/inbound/http/handlers/mod.rs b/src/lib/inbound/http/handlers/mod.rs new file mode 100644 index 0000000..02f7ea5 --- /dev/null +++ b/src/lib/inbound/http/handlers/mod.rs @@ -0,0 +1,3 @@ +pub mod fileserv; +pub mod leptos; +pub mod user; diff --git a/src/lib/inbound/http/handlers/user.rs b/src/lib/inbound/http/handlers/user.rs new file mode 100644 index 0000000..5f7cc0b --- /dev/null +++ b/src/lib/inbound/http/handlers/user.rs @@ -0,0 +1,40 @@ +use crate::{ + domain::api::{ + models::user::ActivationToken, + ports::ApiService, + prelude::{Alert, FlashBag, FlashMessage}, + }, + inbound::http::state::AppState, +}; + +use axum::{ + extract::{Path, State}, + response::{IntoResponse, Redirect, Response}, +}; +use tracing::warn; + +pub async fn activate_account<S: ApiService>( + State(app_state): State<AppState<S>>, + session: axum_session::SessionAnySession, + Path(token): Path<String>, +) -> Response { + let app = app_state.api_service(); + + let token = ActivationToken::from(token); + + if let Err(e) = app.activate_user_account(token).await { + warn!("{:#?}", e); + let flash = + FlashMessage::new("login", "Invalid activation token").with_alert(Alert::Warning); + + FlashBag::new(session).set(flash); + + return Redirect::to("/auth/login").into_response(); + } + + let flash = FlashMessage::new("login", "Activation successful, you can now login") + .with_alert(Alert::Success); + + FlashBag::new(session).set(flash); + Redirect::to("/auth/login").into_response() +} diff --git a/src/lib/inbound/http/state.rs b/src/lib/inbound/http/state.rs new file mode 100644 index 0000000..0c830f5 --- /dev/null +++ b/src/lib/inbound/http/state.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; + +use axum::extract::FromRef; +use leptos::get_configuration; + +use crate::domain::api::ports::ApiService; + +#[derive(Debug, Clone)] +/// The global application state shared between all request handlers. +pub struct AppState<S> +where + S: ApiService, +{ + pub leptos_options: leptos::LeptosOptions, + api_service: Arc<S>, +} + +impl<S> AppState<S> +where + S: ApiService, +{ + pub async fn new(api_service: S) -> Self { + Self { + leptos_options: get_configuration(None).await.unwrap().leptos_options, + api_service: Arc::new(api_service), + } + } + + pub fn api_service(&self) -> Arc<S> { + self.api_service.clone() + } +} + +impl<S> FromRef<AppState<S>> for leptos::LeptosOptions +where + S: ApiService, +{ + fn from_ref(input: &AppState<S>) -> Self { + input.leptos_options.clone() + } +} diff --git a/src/lib/inbound/mod.rs b/src/lib/inbound/mod.rs new file mode 100644 index 0000000..3883215 --- /dev/null +++ b/src/lib/inbound/mod.rs @@ -0,0 +1 @@ +pub mod http; diff --git a/src/lib/lib.rs b/src/lib/lib.rs new file mode 100644 index 0000000..2e94558 --- /dev/null +++ b/src/lib/lib.rs @@ -0,0 +1,12 @@ +#![feature(trait_alias)] + +pub static PROJECT_NAME: &str = "Avii's Virtual Airline Manager"; +pub static COPYRIGHT: &str = "Avii's Virtual Airline Manager © 2024"; +pub mod domain; + +#[cfg(feature = "ssr")] +pub mod config; +#[cfg(feature = "ssr")] +pub mod inbound; +#[cfg(feature = "ssr")] +pub mod outbound; diff --git a/src/lib/outbound/dangerous_lettre.rs b/src/lib/outbound/dangerous_lettre.rs new file mode 100644 index 0000000..0467c80 --- /dev/null +++ b/src/lib/outbound/dangerous_lettre.rs @@ -0,0 +1,84 @@ +pub mod user_notifier; + +use lettre::message::{header, Mailbox, MultiPart, SinglePart}; + +use lettre::transport::smtp::authentication::Credentials; +use lettre::transport::smtp::client::{Tls, TlsParameters}; +use lettre::AsyncSmtpTransport; +use lettre::Message; +use lettre::Tokio1Executor; + +use tera::{Context, Tera}; + +#[derive(Debug, Clone)] +pub struct DangerousLettre { + mailer: AsyncSmtpTransport<Tokio1Executor>, + from: Mailbox, + tera: Tera, +} + +impl DangerousLettre { + pub fn new( + host: &str, + port: u16, + username: &str, + password: &str, + from: &str, + ) -> Result<Self, anyhow::Error> { + let creds = Credentials::new(username.to_string(), password.to_string()); + + let tls = TlsParameters::builder(host.to_owned()) + .dangerous_accept_invalid_certs(true) + .dangerous_accept_invalid_hostnames(true) + .build()?; + + let mailer: AsyncSmtpTransport<Tokio1Executor> = + AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(host) + .port(port) + .tls(Tls::Opportunistic(tls)) + .credentials(creds) + .build(); + + let tera = Tera::new("templates/**/*.email.*")?; + + let from = from.parse::<Mailbox>()?; + + Ok(Self { mailer, from, tera }) + } + + pub fn template( + &self, + template: &str, + subject: &str, + context: &Context, + to: Mailbox, + ) -> Result<Message, anyhow::Error> { + let plain_text = self + .tera + .render(&format!("{}.email.txt", template), context)?; + + let html = self + .tera + .render(&format!("{}.email.html", template), context)?; + + let message = Message::builder() + .subject(subject) + .from(self.from.clone()) + .to(to) + .multipart( + MultiPart::alternative() // This is composed of two parts. + .singlepart( + SinglePart::builder() + .header(header::ContentType::TEXT_PLAIN) + .body(plain_text), // Every message should have a plain text fallback. + ) + .singlepart( + SinglePart::builder() + .header(header::ContentType::TEXT_HTML) + .body(html), + ), + )?; + + Ok(message) + } +} diff --git a/src/lib/outbound/dangerous_lettre/user_notifier.rs b/src/lib/outbound/dangerous_lettre/user_notifier.rs new file mode 100644 index 0000000..1094f23 --- /dev/null +++ b/src/lib/outbound/dangerous_lettre/user_notifier.rs @@ -0,0 +1,50 @@ +use lettre::AsyncTransport; + +use crate::domain::api::ports::UserNotifier; + +use crate::domain::api::models::user::*; + +use super::DangerousLettre; + +impl UserNotifier for DangerousLettre { + async fn user_created(&self, user: &User, token: &ActivationToken) { + let mut context = tera::Context::new(); + + let url = format!("http://127.0.0.1:3000/auth/activate/{}", token); // Move base url to env + + context.insert("activate_url", &url); + + let to = format!("{}", user.email()).parse().unwrap(); + + let message = self + .template("user/created", "Welcome to AVAM!", &context, to) + .unwrap(); + + if let Err(e) = self.mailer.send(message).await { + eprintln!("{:#?}", e); + }; + } + + async fn forgot_password(&self, user: &User, token: &PasswordResetToken) { + let mut context = tera::Context::new(); + + let url = format!("http://127.0.0.1:3000/auth/reset/{}", token); // Move base url to env + + context.insert("reset_url", &url); + + let to = format!("{}", user.email()).parse().unwrap(); + + let message = self + .template( + "user/password_reset", + "Password reset request", + &context, + to, + ) + .unwrap(); + + if let Err(e) = self.mailer.send(message).await { + eprintln!("{:#?}", e); + }; + } +} diff --git a/src/lib/outbound/mod.rs b/src/lib/outbound/mod.rs new file mode 100644 index 0000000..f37d6a9 --- /dev/null +++ b/src/lib/outbound/mod.rs @@ -0,0 +1,2 @@ +pub mod dangerous_lettre; +pub mod postgres; diff --git a/src/lib/outbound/postgres.rs b/src/lib/outbound/postgres.rs new file mode 100644 index 0000000..fc5b690 --- /dev/null +++ b/src/lib/outbound/postgres.rs @@ -0,0 +1,28 @@ +pub mod user_repository; + +use std::str::FromStr; + +use anyhow::Context; +use sqlx::{postgres::PgConnectOptions, PgPool, Pool}; + +#[derive(Debug, Clone)] +pub struct Postgres { + pool: Pool<sqlx::Postgres>, +} + +impl Postgres { + pub async fn new(url: &str) -> Result<Self, anyhow::Error> { + let pool = PgPool::connect_with( + PgConnectOptions::from_str(url) + .with_context(|| format!("Invalid database url: {}", url))?, + ) + .await + .with_context(|| format!("Failed to open database at {}", url))?; + + Ok(Self { pool }) + } + + pub fn pool(&self) -> Pool<sqlx::Postgres> { + self.pool.clone() + } +} diff --git a/src/lib/outbound/postgres/user_repository.rs b/src/lib/outbound/postgres/user_repository.rs new file mode 100644 index 0000000..86f9c32 --- /dev/null +++ b/src/lib/outbound/postgres/user_repository.rs @@ -0,0 +1,343 @@ +use anyhow::Context; +use sqlx::postgres::PgDatabaseError; +use sqlx::Executor; +use sqlx::QueryBuilder; +use sqlx::Row; + +use crate::domain::api::models::user::*; +use crate::domain::api::ports::UserRepository; + +use super::Postgres; + +impl UserRepository for Postgres { + async fn create_user(&self, req: CreateUserRequest) -> Result<User, CreateUserError> { + let mut tx = self + .pool + .begin() + .await + .context("Failed to start sql transaction")?; + + let id = uuid::Uuid::new_v4(); + let email = req.email(); + let password = req.password(); + + let query = sqlx::query( + r#" + INSERT INTO users (id, email, password) + VALUES ($1, $2, $3) + RETURNING + id, + email, + password, + verified + "#, + ) + .bind(id) + .bind(email.to_string()) + .bind(password.to_string()); + + let execute = tx.execute(query).await; + + if let Err(ref e) = execute { + if let Some(e) = e.as_database_error() { + let e = e.downcast_ref::<PgDatabaseError>(); + if e.code() == "23505" { + return Err(CreateUserError::Duplicate { + email: email.clone(), + }); + } + } + } + + // fallthrough and handle other errors with `?` + execute.map_err(|e| CreateUserError::Unknown(e.into()))?; + + tx.commit() + .await + .context("failed to commit SQL transaction")?; + + Ok(User::new(id, email.clone(), password.clone(), false.into())) + } + + async fn create_activation_token(&self, ent: &User) -> Result<ActivationToken, anyhow::Error> { + let mut tx = self + .pool + .begin() + .await + .context("Failed to start sql transaction")?; + + let user_id = ent.id(); + + let token = ActivationToken::new(); + + let query = sqlx::query( + r#" + INSERT INTO activation_token (user_id, token) + VALUES ($1, $2)"#, + ) + .bind(user_id) + .bind(token.to_string()); + + tx.execute(query) + .await + .context("failed to execute SQL transaction")?; + tx.commit() + .await + .context("failed to commit SQL transaction")?; + + Ok(token) + } + + async fn create_password_reset_token( + &self, + ent: &User, + ) -> Result<PasswordResetToken, anyhow::Error> { + let _ = self.delete_password_reset_tokens_for_user(ent).await; + + let mut tx = self + .pool + .begin() + .await + .context("Failed to start sql transaction")?; + + let user_id = ent.id(); + + let token = PasswordResetToken::new(); + + let query = sqlx::query( + r#" + INSERT INTO forgot_password (user_id, token) + VALUES ($1, $2)"#, + ) + .bind(user_id) + .bind(token.to_string()); + + tx.execute(query) + .await + .context("failed to execute SQL transaction")?; + tx.commit() + .await + .context("failed to commit SQL transaction")?; + + Ok(token) + } + + async fn all_users(&self) -> Vec<User> { + todo!() + } + + async fn find_user_by_id(&self, id: uuid::Uuid) -> Result<Option<User>, anyhow::Error> { + let query = sqlx::query( + r#" + SELECT + id, + email, + password, + verified + FROM users WHERE id = $1 + "#, + ) + .bind(id); + + let row = self + .pool + .fetch_optional(query) + .await + .context("failed to execute SQL transaction")?; + + let Some(row) = row else { + return Ok(None); + }; + + let id = row.get("id"); + let email = EmailAddress::new(row.get("email"))?; + let password = Password::new_hashed(row.get("password")); + let verified = Verified::new(row.get("verified")); + + Ok(Some(User::new(id, email, password, verified))) + } + + async fn find_user_by_email( + &self, + email: &EmailAddress, + ) -> Result<Option<User>, anyhow::Error> { + let query = sqlx::query( + r#" + SELECT + id, + email, + password, + verified + FROM users WHERE email = $1 + "#, + ) + .bind(email.to_string()); + + let row = self + .pool + .fetch_optional(query) + .await + .context("failed to execute SQL transaction")?; + + let Some(row) = row else { + return Ok(None); + }; + + let id = row.get("id"); + let email = EmailAddress::new(row.get("email"))?; + let password = Password::new_hashed(row.get("password")); + let verified = Verified::new(row.get("verified")); + + Ok(Some(User::new(id, email, password, verified))) + } + + async fn find_user_by_activation_token( + &self, + token: &ActivationToken, + ) -> Result<Option<User>, anyhow::Error> { + let query = sqlx::query( + r#" + SELECT + user_id, + token + FROM activation_token WHERE token = $1 + "#, + ) + .bind(token.to_string()); + + let Some(row) = self + .pool + .fetch_optional(query) + .await + .context("failed to execute SQL transaction")? + else { + return Ok(None); + }; + + let id = row.get("user_id"); + + Ok(self.find_user_by_id(id).await?) + } + + async fn find_user_by_password_reset_token( + &self, + token: &PasswordResetToken, + ) -> Result<Option<User>, anyhow::Error> { + let query = sqlx::query( + r#" + SELECT + user_id, + token + FROM forgot_password WHERE token = $1 + "#, + ) + .bind(token.to_string()); + + let Some(row) = self + .pool + .fetch_optional(query) + .await + .context("failed to execute SQL transaction")? + else { + return Ok(None); + }; + + let id = row.get("user_id"); + + Ok(self.find_user_by_id(id).await?) + } + + async fn update_user( + &self, + ent: &User, + req: UpdateUserRequest, + ) -> Result<(User, User), UpdateUserError> { + let mut tx = self + .pool + .begin() + .await + .context("Failed to start sql transaction")?; + + let mut query = QueryBuilder::new("UPDATE users SET "); + + let mut new_email = ent.email(); + let mut new_password = ent.password(); + let mut new_verified = ent.verified(); + + if let Some(email) = req.email() { + new_email = email; + query.push(" email = "); + query.push_bind(email.to_string()); + }; + + if let Some(password) = req.password() { + new_password = password; + query.push(" password = "); + query.push_bind(password.to_string()); + }; + + if let Some(verified) = req.verified() { + new_verified = verified; + query.push(" verified = "); + query.push_bind::<bool>((*verified).into()); + }; + + query.push(" WHERE id = "); + query.push_bind(ent.id()); + + tx.execute(query.build()) + .await + .context("failed to execute SQL transaction")?; + tx.commit() + .await + .context("failed to commit SQL transaction")?; + + Ok(( + ent.clone(), + User::new( + ent.id().clone(), + new_email.clone(), + new_password.clone(), + new_verified.clone(), + ), + )) + } + + async fn delete_activation_token_for_user(&self, ent: &User) -> Result<(), anyhow::Error> { + let mut tx = self + .pool + .begin() + .await + .context("Failed to start sql transaction")?; + + let query = sqlx::query("DELETE FROM activation_token WHERE user_id = $1").bind(ent.id()); + + tx.execute(query) + .await + .context("failed to execute SQL transaction")?; + tx.commit() + .await + .context("failed to commit SQL transaction")?; + + Ok(()) + } + + async fn delete_password_reset_tokens_for_user(&self, ent: &User) -> Result<(), anyhow::Error> { + let mut tx = self + .pool + .begin() + .await + .context("Failed to start sql transaction")?; + + let query = sqlx::query("DELETE FROM forgot_password WHERE user_id = $1").bind(ent.id()); + + tx.execute(query) + .await + .context("failed to execute SQL transaction")?; + tx.commit() + .await + .context("failed to commit SQL transaction")?; + + Ok(()) + } +} diff --git a/style/input.scss b/style/input.scss new file mode 100644 index 0000000..217edd9 --- /dev/null +++ b/style/input.scss @@ -0,0 +1,22 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.bg-auth { + background-image: url("/background.jpg") +} + +.input ::placeholder { + opacity: 0.35; +} + +input:is(:-webkit-autofill, :autofill) { + border: 0px; + -webkit-text-fill-color: var(--fallback-a,oklch(var(--a))); + -webkit-box-shadow: 0 0 0px 1000px var(--fallback-b1,oklch(var(--b1))) inset; +} + +label:has(input:is(:-webkit-autofill, :autofill)) { + border-color: var(--fallback-a,oklch(var(--a)/.2)) !important; + color: var(--fallback-a,oklch(var(--a))); +} diff --git a/style/main.scss b/style/main.scss new file mode 100644 index 0000000..45912ee --- /dev/null +++ b/style/main.scss @@ -0,0 +1,1589 @@ +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +/* +! tailwindcss v3.4.13 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + letter-spacing: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +input:where([type='button']), +input:where([type='reset']), +input:where([type='submit']) { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +:root, +[data-theme] { + background-color: var(--fallback-b1,oklch(var(--b1)/1)); + color: var(--fallback-bc,oklch(var(--bc)/1)); +} + +@supports not (color: oklch(0% 0 0)) { + :root { + color-scheme: light; + --fallback-p: #491eff; + --fallback-pc: #d4dbff; + --fallback-s: #ff41c7; + --fallback-sc: #fff9fc; + --fallback-a: #00cfbd; + --fallback-ac: #00100d; + --fallback-n: #2b3440; + --fallback-nc: #d7dde4; + --fallback-b1: #ffffff; + --fallback-b2: #e5e6e6; + --fallback-b3: #e5e6e6; + --fallback-bc: #1f2937; + --fallback-in: #00b3f0; + --fallback-inc: #000000; + --fallback-su: #00ca92; + --fallback-suc: #000000; + --fallback-wa: #ffc22d; + --fallback-wac: #000000; + --fallback-er: #ff6f70; + --fallback-erc: #000000; + } + + @media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --fallback-p: #7582ff; + --fallback-pc: #050617; + --fallback-s: #ff71cf; + --fallback-sc: #190211; + --fallback-a: #00c7b5; + --fallback-ac: #000e0c; + --fallback-n: #2a323c; + --fallback-nc: #a6adbb; + --fallback-b1: #1d232a; + --fallback-b2: #191e24; + --fallback-b3: #15191e; + --fallback-bc: #a6adbb; + --fallback-in: #00b3f0; + --fallback-inc: #000000; + --fallback-su: #00ca92; + --fallback-suc: #000000; + --fallback-wa: #ffc22d; + --fallback-wac: #000000; + --fallback-er: #ff6f70; + --fallback-erc: #000000; + } + } +} + +html { + -webkit-tap-highlight-color: transparent; +} + +* { + scrollbar-color: color-mix(in oklch, currentColor 35%, transparent) transparent; +} + +*:hover { + scrollbar-color: color-mix(in oklch, currentColor 60%, transparent) transparent; +} + +:root { + color-scheme: light; + --in: 72.06% 0.191 231.6; + --su: 64.8% 0.150 160; + --wa: 84.71% 0.199 83.87; + --er: 71.76% 0.221 22.18; + --pc: 89.824% 0.06192 275.75; + --ac: 15.352% 0.0368 183.61; + --inc: 0% 0 0; + --suc: 0% 0 0; + --wac: 0% 0 0; + --erc: 0% 0 0; + --rounded-box: 1rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: .2s; + --btn-focus-scale: 0.95; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --p: 49.12% 0.3096 275.75; + --s: 69.71% 0.329 342.55; + --sc: 98.71% 0.0106 342.55; + --a: 76.76% 0.184 183.61; + --n: 32.1785% 0.02476 255.701624; + --nc: 89.4994% 0.011585 252.096176; + --b1: 100% 0 0; + --b2: 96.1151% 0 0; + --b3: 92.4169% 0.00108 197.137559; + --bc: 27.8078% 0.029596 256.847952; +} + +@media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --in: 72.06% 0.191 231.6; + --su: 64.8% 0.150 160; + --wa: 84.71% 0.199 83.87; + --er: 71.76% 0.221 22.18; + --pc: 13.138% 0.0392 275.75; + --sc: 14.96% 0.052 342.55; + --ac: 14.902% 0.0334 183.61; + --inc: 0% 0 0; + --suc: 0% 0 0; + --wac: 0% 0 0; + --erc: 0% 0 0; + --rounded-box: 1rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: .2s; + --btn-focus-scale: 0.95; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --p: 65.69% 0.196 275.75; + --s: 74.8% 0.26 342.55; + --a: 74.51% 0.167 183.61; + --n: 31.3815% 0.021108 254.139175; + --nc: 74.6477% 0.0216 264.435964; + --b1: 25.3267% 0.015896 252.417568; + --b2: 23.2607% 0.013807 253.100675; + --b3: 21.1484% 0.01165 254.087939; + --bc: 74.6477% 0.0216 264.435964; + } +} + +[data-theme=light] { + color-scheme: light; + --in: 72.06% 0.191 231.6; + --su: 64.8% 0.150 160; + --wa: 84.71% 0.199 83.87; + --er: 71.76% 0.221 22.18; + --pc: 89.824% 0.06192 275.75; + --ac: 15.352% 0.0368 183.61; + --inc: 0% 0 0; + --suc: 0% 0 0; + --wac: 0% 0 0; + --erc: 0% 0 0; + --rounded-box: 1rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: .2s; + --btn-focus-scale: 0.95; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --p: 49.12% 0.3096 275.75; + --s: 69.71% 0.329 342.55; + --sc: 98.71% 0.0106 342.55; + --a: 76.76% 0.184 183.61; + --n: 32.1785% 0.02476 255.701624; + --nc: 89.4994% 0.011585 252.096176; + --b1: 100% 0 0; + --b2: 96.1151% 0 0; + --b3: 92.4169% 0.00108 197.137559; + --bc: 27.8078% 0.029596 256.847952; +} + +[data-theme=dark] { + color-scheme: dark; + --in: 72.06% 0.191 231.6; + --su: 64.8% 0.150 160; + --wa: 84.71% 0.199 83.87; + --er: 71.76% 0.221 22.18; + --pc: 13.138% 0.0392 275.75; + --sc: 14.96% 0.052 342.55; + --ac: 14.902% 0.0334 183.61; + --inc: 0% 0 0; + --suc: 0% 0 0; + --wac: 0% 0 0; + --erc: 0% 0 0; + --rounded-box: 1rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: .2s; + --btn-focus-scale: 0.95; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --p: 65.69% 0.196 275.75; + --s: 74.8% 0.26 342.55; + --a: 74.51% 0.167 183.61; + --n: 31.3815% 0.021108 254.139175; + --nc: 74.6477% 0.0216 264.435964; + --b1: 25.3267% 0.015896 252.417568; + --b2: 23.2607% 0.013807 253.100675; + --b3: 21.1484% 0.01165 254.087939; + --bc: 74.6477% 0.0216 264.435964; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.alert { + display: grid; + width: 100%; + grid-auto-flow: row; + align-content: flex-start; + align-items: center; + justify-items: center; + gap: 1rem; + text-align: center; + border-radius: var(--rounded-box, 1rem); + border-width: 1px; + --tw-border-opacity: 1; + border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); + padding: 1rem; + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --alert-bg: var(--fallback-b2,oklch(var(--b2)/1)); + --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1)); + background-color: var(--alert-bg); +} + +@media (min-width: 640px) { + .alert { + grid-auto-flow: column; + grid-template-columns: auto minmax(auto,1fr); + justify-items: start; + text-align: start; + } +} + +.avatar.placeholder > div { + display: flex; + align-items: center; + justify-content: center; +} + +@media (hover:hover) { + .label a:hover { + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + } +} + +.btn { + display: inline-flex; + height: 3rem; + min-height: 3rem; + flex-shrink: 0; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + flex-wrap: wrap; + align-items: center; + justify-content: center; + border-radius: var(--rounded-btn, 0.5rem); + border-color: transparent; + border-color: oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity)); + padding-left: 1rem; + padding-right: 1rem; + text-align: center; + font-size: 0.875rem; + line-height: 1em; + gap: 0.5rem; + font-weight: 600; + text-decoration-line: none; + transition-duration: 200ms; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + border-width: var(--border-btn, 1px); + transition-property: color, background-color, border-color, opacity, box-shadow, transform; + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + outline-color: var(--fallback-bc,oklch(var(--bc)/1)); + background-color: oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity)); + --tw-bg-opacity: 1; + --tw-border-opacity: 1; +} + +.btn-disabled, + .btn[disabled], + .btn:disabled { + pointer-events: none; +} + +:where(.btn:is(input[type="checkbox"])), +:where(.btn:is(input[type="radio"])) { + width: auto; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.btn:is(input[type="checkbox"]):after, +.btn:is(input[type="radio"]):after { + --tw-content: attr(aria-label); + content: var(--tw-content); +} + +@media (hover: hover) { + .btm-nav > *.disabled:hover, + .btm-nav > *[disabled]:hover { + pointer-events: none; + --tw-border-opacity: 0; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-bg-opacity: 0.1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-text-opacity: 0.2; + } + + .btn:hover { + --tw-border-opacity: 1; + border-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn:hover { + background-color: color-mix( + in oklab, + oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity, 1)) 90%, + black + ); + border-color: color-mix( + in oklab, + oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity, 1)) 90%, + black + ); + } + } + + @supports not (color: oklch(0% 0 0)) { + .btn:hover { + background-color: var(--btn-color, var(--fallback-b2)); + border-color: var(--btn-color, var(--fallback-b2)); + } + } + + .btn.glass:hover { + --glass-opacity: 25%; + --glass-border-opacity: 15%; + } + + .btn-outline.btn-primary:hover { + --tw-text-opacity: 1; + color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-primary:hover { + background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); + } + } + + .btn-disabled:hover, + .btn[disabled]:hover, + .btn:disabled:hover { + --tw-border-opacity: 0; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-bg-opacity: 0.2; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-text-opacity: 0.2; + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn:is(input[type="checkbox"]:checked):hover, .btn:is(input[type="radio"]:checked):hover { + background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); + } + } +} + +.footer { + display: grid; + width: 100%; + grid-auto-flow: row; + place-items: start; + -moz-column-gap: 1rem; + column-gap: 1rem; + row-gap: 2.5rem; + font-size: 0.875rem; + line-height: 1.25rem; +} + +.footer > * { + display: grid; + place-items: start; + gap: 0.5rem; +} + +@media (min-width: 48rem) { + .footer { + grid-auto-flow: column; + } + + .footer-center { + grid-auto-flow: row dense; + } +} + +.label { + display: flex; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + align-items: center; + justify-content: space-between; + padding-left: 0.25rem; + padding-right: 0.25rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.input { + flex-shrink: 1; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + height: 3rem; + padding-left: 1rem; + padding-right: 1rem; + font-size: 1rem; + line-height: 2; + line-height: 1.5rem; + border-radius: var(--rounded-btn, 0.5rem); + border-width: 1px; + border-color: transparent; + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); +} + +.input[type="number"]::-webkit-inner-spin-button, +.input-md[type="number"]::-webkit-inner-spin-button { + margin-top: -1rem; + margin-bottom: -1rem; + margin-inline-end: -1rem; +} + +.link { + cursor: pointer; + text-decoration-line: underline; +} + +.menu li.disabled { + cursor: not-allowed; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + color: var(--fallback-bc,oklch(var(--bc)/0.3)); +} + +.alert-info { + border-color: var(--fallback-in,oklch(var(--in)/0.2)); + --tw-text-opacity: 1; + color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity))); + --alert-bg: var(--fallback-in,oklch(var(--in)/1)); + --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1)); +} + +.alert-success { + border-color: var(--fallback-su,oklch(var(--su)/0.2)); + --tw-text-opacity: 1; + color: var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity))); + --alert-bg: var(--fallback-su,oklch(var(--su)/1)); + --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1)); +} + +.alert-warning { + border-color: var(--fallback-wa,oklch(var(--wa)/0.2)); + --tw-text-opacity: 1; + color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity))); + --alert-bg: var(--fallback-wa,oklch(var(--wa)/1)); + --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1)); +} + +.alert-error { + border-color: var(--fallback-er,oklch(var(--er)/0.2)); + --tw-text-opacity: 1; + color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); + --alert-bg: var(--fallback-er,oklch(var(--er)/1)); + --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1)); +} + +.btm-nav > *.disabled, + .btm-nav > *[disabled] { + pointer-events: none; + --tw-border-opacity: 0; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-bg-opacity: 0.1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-text-opacity: 0.2; +} + +.btm-nav > * .label { + font-size: 1rem; + line-height: 1.5rem; +} + +@media (prefers-reduced-motion: no-preference) { + .btn { + animation: button-pop var(--animation-btn, 0.25s) ease-out; + } +} + +.btn:active:hover, + .btn:active:focus { + animation: button-pop 0s ease-out; + transform: scale(var(--btn-focus-scale, 0.97)); +} + +@supports not (color: oklch(0% 0 0)) { + .btn { + background-color: var(--btn-color, var(--fallback-b2)); + border-color: var(--btn-color, var(--fallback-b2)); + } + + .btn-primary { + --btn-color: var(--fallback-p); + } +} + +@supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-primary.btn-active { + background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); + } +} + +.btn:focus-visible { + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; +} + +.btn-primary { + --tw-text-opacity: 1; + color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); + outline-color: var(--fallback-p,oklch(var(--p)/1)); +} + +@supports (color: oklch(0% 0 0)) { + .btn-primary { + --btn-color: var(--p); + } +} + +.btn.glass { + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + outline-color: currentColor; +} + +.btn.glass.btn-active { + --glass-opacity: 25%; + --glass-border-opacity: 15%; +} + +.btn-outline.btn-primary { + --tw-text-opacity: 1; + color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity))); +} + +.btn-outline.btn-primary.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); +} + +.btn.btn-disabled, + .btn[disabled], + .btn:disabled { + --tw-border-opacity: 0; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-bg-opacity: 0.2; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-text-opacity: 0.2; +} + +.btn:is(input[type="checkbox"]:checked), +.btn:is(input[type="radio"]:checked) { + --tw-border-opacity: 1; + border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); + --tw-text-opacity: 1; + color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); +} + +.btn:is(input[type="checkbox"]:checked):focus-visible, .btn:is(input[type="radio"]:checked):focus-visible { + outline-color: var(--fallback-p,oklch(var(--p)/1)); +} + +@keyframes button-pop { + 0% { + transform: scale(var(--btn-focus-scale, 0.98)); + } + + 40% { + transform: scale(1.02); + } + + 100% { + transform: scale(1); + } +} + +@keyframes checkmark { + 0% { + background-position-y: 5px; + } + + 50% { + background-position-y: -2px; + } + + 100% { + background-position-y: 0; + } +} + +.input input { + --tw-bg-opacity: 1; + background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); + background-color: transparent; +} + +.input input:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.input[list]::-webkit-calendar-picker-indicator { + line-height: 1em; +} + +.input-bordered { + border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); +} + +.input:focus, + .input:focus-within { + box-shadow: none; + border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); +} + +.input:has(> input[disabled]), + .input-disabled, + .input:disabled, + .input[disabled] { + cursor: not-allowed; + --tw-border-opacity: 1; + border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); + color: var(--fallback-bc,oklch(var(--bc)/0.4)); +} + +.input:has(> input[disabled])::-moz-placeholder, .input-disabled::-moz-placeholder, .input:disabled::-moz-placeholder, .input[disabled]::-moz-placeholder { + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); + --tw-placeholder-opacity: 0.2; +} + +.input:has(> input[disabled])::placeholder, + .input-disabled::placeholder, + .input:disabled::placeholder, + .input[disabled]::placeholder { + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); + --tw-placeholder-opacity: 0.2; +} + +.input:has(> input[disabled]) > input[disabled] { + cursor: not-allowed; +} + +.input::-webkit-date-and-time-value { + text-align: inherit; +} + +.join > :where(*:not(:first-child)):is(.btn) { + margin-inline-start: calc(var(--border-btn) * -1); +} + +.link:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.link:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; +} + +.mockup-phone .display { + overflow: hidden; + border-radius: 40px; + margin-top: -25px; +} + +.mockup-browser .mockup-browser-toolbar .input { + position: relative; + margin-left: auto; + margin-right: auto; + display: block; + height: 1.75rem; + width: 24rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); + padding-left: 2rem; + direction: ltr; +} + +.mockup-browser .mockup-browser-toolbar .input:before { + content: ""; + position: absolute; + left: 0.5rem; + top: 50%; + aspect-ratio: 1 / 1; + height: 0.75rem; + --tw-translate-y: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + border-radius: 9999px; + border-width: 2px; + border-color: currentColor; + opacity: 0.6; +} + +.mockup-browser .mockup-browser-toolbar .input:after { + content: ""; + position: absolute; + left: 1.25rem; + top: 50%; + height: 0.5rem; + --tw-translate-y: 25%; + --tw-rotate: -45deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + border-radius: 9999px; + border-width: 1px; + border-color: currentColor; + opacity: 0.6; +} + +@keyframes modal-pop { + 0% { + opacity: 0; + } +} + +@keyframes progress-loading { + 50% { + background-position-x: -115%; + } +} + +@keyframes radiomark { + 0% { + box-shadow: 0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset, + 0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset; + } + + 50% { + box-shadow: 0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset, + 0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset; + } + + 100% { + box-shadow: 0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset, + 0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset; + } +} + +@keyframes rating-pop { + 0% { + transform: translateY(-0.125em); + } + + 40% { + transform: translateY(-0.125em); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes skeleton { + from { + background-position: 150%; + } + + to { + background-position: -50%; + } +} + +@keyframes toast-pop { + 0% { + transform: scale(0.9); + opacity: 0; + } + + 100% { + transform: scale(1); + opacity: 1; + } +} + +.btn-block { + width: 100%; +} + +.join.join-vertical > :where(*:not(:first-child)):is(.btn) { + margin-top: calc(var(--border-btn) * -1); +} + +.join.join-horizontal > :where(*:not(:first-child)):is(.btn) { + margin-inline-start: calc(var(--border-btn) * -1); + margin-top: 0px; +} + +.static { + position: static; +} + +.relative { + position: relative; +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.my-2 { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.mt-auto { + margin-top: auto; +} + +.flex { + display: flex; +} + +.hidden { + display: none; +} + +.h-full { + height: 100%; +} + +.h-screen { + height: 100vh; +} + +.w-full { + width: 100%; +} + +.grow { + flex-grow: 1; +} + +.grow-0 { + flex-grow: 0; +} + +.flex-row { + flex-direction: row; +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.gap-2 { + gap: 0.5rem; +} + +.overflow-auto { + overflow: auto; +} + +.bg-cover { + background-size: cover; +} + +.bg-center { + background-position: center; +} + +.px-1 { + padding-left: 0.25rem; + padding-right: 0.25rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.py-16 { + padding-top: 4rem; + padding-bottom: 4rem; +} + +.text-center { + text-align: center; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-gray-500 { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} + +.bg-auth { + background-image: url("/background.jpg") +} + +.input ::-moz-placeholder { + opacity: 0.35; +} + +.input ::placeholder { + opacity: 0.35; +} + +input:is(:-webkit-autofill, :-webkit-autofill) { + border: 0px; + -webkit-text-fill-color: var(--fallback-a,oklch(var(--a))); + -webkit-box-shadow: 0 0 0px 1000px var(--fallback-b1,oklch(var(--b1))) inset; +} + +input:is(:-webkit-autofill, :autofill) { + border: 0px; + -webkit-text-fill-color: var(--fallback-a,oklch(var(--a))); + -webkit-box-shadow: 0 0 0px 1000px var(--fallback-b1,oklch(var(--b1))) inset; +} + +label:has(input:is(:-webkit-autofill, :-webkit-autofill)) { + border-color: var(--fallback-a,oklch(var(--a)/.2)) !important; + color: var(--fallback-a,oklch(var(--a))); +} + +label:has(input:is(:-webkit-autofill, :autofill)) { + border-color: var(--fallback-a,oklch(var(--a)/.2)) !important; + color: var(--fallback-a,oklch(var(--a))); +} + +.dark\:text-white:is([data-theme="dark"] *) { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +@media (min-width: 768px) { + .md\:basis-1\/3 { + flex-basis: 33.333333%; + } + + .md\:basis-2\/3 { + flex-basis: 66.666667%; + } +} + +@media (min-width: 1536px) { + .\32xl\:basis-1\/4 { + flex-basis: 25%; + } + + .\32xl\:basis-3\/4 { + flex-basis: 75%; + } +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..aced825 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,18 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: { + files: ["*.scss", "*.html", "./src/**/*.rs"], + }, + darkMode: ["class", '[data-theme="dark"]'], + theme: { + extend: {}, + }, + daisyui: { + themes: ["default", "light", "dark"], + base: true, // applies background color and foreground color for root element by default + styled: true, // include daisyUI colors and design decisions for all components + utils: true, // adds responsive and modifier utility classes + + }, + plugins: [require("@tailwindcss/typography"), require("daisyui")], +} \ No newline at end of file diff --git a/templates/user/created.email.html b/templates/user/created.email.html new file mode 100644 index 0000000..f936f24 --- /dev/null +++ b/templates/user/created.email.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>AVAM - Activate your Account + + + +

Thank you for joining Avii's Virtual Airline Manager.

+ +

Please click here or go to {{ activate_url }} to activate your + account.

+ + + \ No newline at end of file diff --git a/templates/user/created.email.txt b/templates/user/created.email.txt new file mode 100644 index 0000000..4c4b3be --- /dev/null +++ b/templates/user/created.email.txt @@ -0,0 +1,3 @@ +Thank you for joining Avii's Virtual Airline Manager. + +Please go to {{ activate_url }} to activate your account. \ No newline at end of file diff --git a/templates/user/password_reset.email.html b/templates/user/password_reset.email.html new file mode 100644 index 0000000..ee4f927 --- /dev/null +++ b/templates/user/password_reset.email.html @@ -0,0 +1,17 @@ + + + + + + + AVAM - Reset Password Request + + + +

Reset Password

+ +

Please click here or go to {{ reset_url }} to reset your password.

+ + + \ No newline at end of file diff --git a/templates/user/password_reset.email.txt b/templates/user/password_reset.email.txt new file mode 100644 index 0000000..05ce15d --- /dev/null +++ b/templates/user/password_reset.email.txt @@ -0,0 +1,3 @@ +Password Reset + +Please go to {{ reset_url }} to reset your password. \ No newline at end of file diff --git a/templates/user/user_notifier.rs b/templates/user/user_notifier.rs new file mode 100644 index 0000000..61e2eff --- /dev/null +++ b/templates/user/user_notifier.rs @@ -0,0 +1,50 @@ +use lettre::AsyncTransport; + +use crate::domain::api::ports::UserNotifier; + +use crate::domain::api::models::user::*; + +use super::DangerousLettre; + +impl UserNotifier for DangerousLettre { + async fn user_created(&self, user: &User, token: &ActivationToken) { + let mut context = tera::Context::new(); + + let url = format!("http://127.0.0.1:3000/auth/activate/{}", token); // Move base url to env + + context.insert("activate_url", &url); + + let to = format!("{}", user.email()).parse().unwrap(); + + let message = self + .template("user/created", "Welcome to AVAM!", &context, to) + .unwrap(); + + if let Err(e) = self.mailer.send(message).await { + eprintln!("{:#?}", e); + }; + } + + async fn forgot_password(&self, user: &User, token: &ForgotPasswordToken) { + let mut context = tera::Context::new(); + + let url = format!("http://127.0.0.1:3000/auth/reset/{}", token); // Move base url to env + + context.insert("reset_url", &url); + + let to = format!("{}", user.email()).parse().unwrap(); + + let message = self + .template( + "user/password_reset", + "Password reset request", + &context, + to, + ) + .unwrap(); + + if let Err(e) = self.mailer.send(message).await { + eprintln!("{:#?}", e); + }; + } +}