diff --git a/backend/.env.development b/backend/.env.development new file mode 100644 index 0000000..e8e5e8a --- /dev/null +++ b/backend/.env.development @@ -0,0 +1,20 @@ +# --- PostgreSQL (usado pelo Docker Compose e pela connection string abaixo) --- +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USER=local_user +POSTGRES_DB=local_db +POSTGRES_PASSWORD=local_password + +# --- App (lido por src/config.rs) --- +DATABASE_URL=postgres://local_user:local_password@localhost:5432/local_db + +JWT_SECRET=dev-secret-change-in-production +JWT_EXPIRY_SECS=3600 +JWT_REFRESH_EXPIRY_SECS=2592000 + +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +HOST=0.0.0.0 +PORT=8080 +APP_ENV=development diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..75abcac --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,18 @@ +# Banco de dados +DATABASE_URL=postgresql://meowspool:meowspool@localhost:5432/meowspool + +# JWT +JWT_SECRET=troque_por_um_secret_forte_em_producao_minimo_32_chars +JWT_EXPIRY_SECS=3600 +JWT_REFRESH_EXPIRY_SECS=2592000 + +# OAuth Google +GOOGLE_CLIENT_ID=seu_google_client_id.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=seu_google_client_secret + +# Servidor +HOST=0.0.0.0 +PORT=8080 + +# Ambiente (development | production) +APP_ENV=development diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/backend/Cargo.lock b/backend/Cargo.lock new file mode 100644 index 0000000..63f33dc --- /dev/null +++ b/backend/Cargo.lock @@ -0,0 +1,4308 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[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.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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 = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "auto-future" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tower 0.5.3", + "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.4.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.2", + "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.117", +] + +[[package]] +name = "axum-test" +version = "15.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac63648e380fd001402a02ec804e7686f9c4751f8cad85b7de0b53dae483a128" +dependencies = [ + "anyhow", + "auto-future", + "axum", + "bytes", + "cookie", + "http 1.4.0", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tokio", + "tower 0.5.3", + "url", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + +[[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 = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[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 = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +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 = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[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.117", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[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.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +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 = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper 1.8.1", + "hyper-util", + "rustls 0.23.37", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", + "webpki-roots 1.0.6", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.8.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.3", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "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 = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "bitflags 2.11.0", + "libc", + "plain", + "redox_syscall 0.7.3", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "meowspool-backend" +version = "0.1.0" +dependencies = [ + "anyhow", + "argon2", + "async-trait", + "axum", + "axum-test", + "base64 0.22.1", + "dotenvy", + "image", + "jsonwebtoken", + "oauth2", + "qrcode", + "reqwest 0.12.28", + "serde", + "serde_json", + "sqlx", + "thiserror 1.0.69", + "time", + "tokio", + "tower 0.4.13", + "tower-http 0.5.2", + "tracing", + "tracing-subscriber", + "uuid", + "validator", +] + +[[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 = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "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 = "oauth2" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" +dependencies = [ + "base64 0.13.1", + "chrono", + "getrandom 0.2.17", + "http 0.2.12", + "rand 0.8.5", + "reqwest 0.11.27", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[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", + "syn 1.0.109", + "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-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "qrcode" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" +dependencies = [ + "image", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.37", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls 0.23.37", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.3", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[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 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.2", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-rustls 0.24.2", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.25.4", + "winreg", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-rustls 0.27.7", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.37", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-rustls 0.26.4", + "tower 0.5.3", + "tower-http 0.6.8", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.6", +] + +[[package]] +name = "reserve-port" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94070964579245eb2f76e62a7668fe87bd9969ed6c41256f3bf614e3323dd3cc" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-multipart-rfc7578_2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b748410c0afdef2ebbe3685a6a862e2ee937127cdaae623336a459451c8d57" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http 0.2.12", + "mime", + "mime_guess", + "rand 0.8.5", + "thiserror 1.0.69", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.9", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[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 = "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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[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 = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls 0.23.37", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.18", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.117", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +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.117", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.11.0", + "byteorder", + "bytes", + "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 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.11.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.18", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[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.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[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", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +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.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.37", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + +[[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.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna 1.1.0", + "percent-encoding", + "serde", + "serde_derive", +] + +[[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.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[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", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0bcf92720c40105ac4b2dda2a4ea3aa717d4d6a862cc217da653a4bd5c6b10" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +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 = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "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-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[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_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[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_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[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 = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" +dependencies = [ + "zune-core", +] diff --git a/backend/Cargo.toml b/backend/Cargo.toml new file mode 100644 index 0000000..ea01224 --- /dev/null +++ b/backend/Cargo.toml @@ -0,0 +1,70 @@ +[package] +name = "meowspool-backend" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "meowspool" +path = "src/main.rs" + +[dependencies] +# HTTP Framework +axum = { version = "0.7", features = ["macros"] } +tower = "0.4" +tower-http = { version = "0.5", features = ["cors", "trace"] } + +# Async Runtime +tokio = { version = "1", features = ["full"] } + +# Database +sqlx = { version = "0.8", features = [ + "runtime-tokio-rustls", + "postgres", + "uuid", + "time", + "macros", +] } + +# Auth +jsonwebtoken = "9" +argon2 = "0.5" +oauth2 = "4" + +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# UUID +uuid = { version = "1", features = ["v4", "serde"] } + +# Time +time = { version = "0.3", features = ["serde", "serde-well-known"] } + +# Errors +thiserror = "1" +anyhow = "1" + +# Validation +validator = { version = "0.18", features = ["derive"] } + +# Env vars +dotenvy = "0.15" + +# Logging / Tracing +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# HTTP Client (OAuth, Google APIs) +reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } + +# QR Code generation +qrcode = "0.14" +image = "0.25" +base64 = "0.22" + +# Async trait +async-trait = "0.1" + +[dev-dependencies] +tokio = { version = "1", features = ["full"] } +axum-test = "15" diff --git a/backend/agent.md b/backend/agent.md new file mode 100644 index 0000000..26eca40 --- /dev/null +++ b/backend/agent.md @@ -0,0 +1,383 @@ +# MeowSpool Backend — Agent Guide + +## Visão Geral + +Backend da aplicação MeowSpool escrito em **Rust**, utilizando **Axum** como framework HTTP e **SQLx** com **PostgreSQL** como banco de dados. A arquitetura segue o padrão **Hexagonal (Ports & Adapters)**, garantindo que o núcleo do domínio seja completamente isolado de detalhes de infraestrutura. + +--- + +## Stack + +| Componente | Biblioteca | Versão mínima | +|-------------------|-----------------------------------------------|---------------| +| HTTP Framework | `axum` | 0.7 | +| Async Runtime | `tokio` (full) | 1.x | +| ORM / Query | `sqlx` (postgres, uuid, time, macros) | 0.8 | +| Autenticação JWT | `jsonwebtoken` | 9.x | +| Hash de senha | `argon2` | 0.5 | +| OAuth Google | `oauth2` | 4.x | +| UUID | `uuid` (v4, serde) | 1.x | +| Serialização | `serde`, `serde_json` | 1.x | +| Erros | `thiserror` | 1.x | +| Env vars | `dotenvy` | 0.15 | +| Logging | `tracing`, `tracing-subscriber` | 0.1 | +| Validação | `validator` | 0.18 | +| HTTP Client | `reqwest` (json, rustls-tls) | 0.12 | +| Geração QR Code | `qrcode` | 0.14 | + +--- + +## Estrutura de Pastas + +``` +backend/ +├── Cargo.toml +├── Cargo.lock +├── .env.example +├── agent.md <- este arquivo +├── migrations/ <- SQL puro, gerenciado pelo SQLx CLI +│ ├── 20240101000001_create_users.sql +│ ├── 20240101000002_create_spool_presets.sql +│ └── 20240101000003_create_filaments.sql +└── src/ + ├── main.rs <- entry point: inicializa config, DB, router e servidor + ├── config.rs <- struct Config lida de variáveis de ambiente + ├── error.rs <- AppError unificado com IntoResponse + ├── router.rs <- composição de todas as rotas Axum + │ + ├── domain/ <- NÚCLEO: entidades puras, sem dependências externas + │ ├── mod.rs + │ ├── user.rs <- struct User, enum AuthProvider + │ ├── filament.rs <- struct Filament, enum Material + │ └── spool_preset.rs <- struct SpoolPreset + │ + ├── ports/ <- INTERFACES: traits que o domínio exige + │ ├── mod.rs + │ ├── user_repository.rs <- trait UserRepository + │ ├── filament_repository.rs <- trait FilamentRepository + │ └── spool_preset_repository.rs <- trait SpoolPresetRepository + │ + ├── application/ <- CASOS DE USO: orquestram domínio + ports + │ ├── mod.rs + │ ├── auth_service.rs <- login, register, OAuth, refresh, logout + │ ├── filament_service.rs <- CRUD, cálculo de peso líquido, QR, SVG + │ └── spool_preset_service.rs <- CRUD presets (system read-only, user CRUD) + │ + └── adapters/ + ├── inbound/ <- HTTP: recebe requisições, delega ao application + │ ├── mod.rs + │ ├── auth_handler.rs + │ ├── filament_handler.rs + │ ├── spool_preset_handler.rs + │ ├── user_handler.rs + │ └── middleware/ + │ └── auth.rs <- extrator JWT que popula CurrentUser no estado + └── outbound/ <- INFRAESTRUTURA: implementa as traits de ports + ├── mod.rs + ├── postgres_user_repo.rs + ├── postgres_filament_repo.rs + └── postgres_spool_preset_repo.rs +``` + +--- + +## Arquitetura Hexagonal — Regras de Dependência + +``` +adapters/inbound (HTTP) + | + v + application (services) + | + v + domain (entidades) + | + ^ + ports (traits) + | + ^ +adapters/outbound (PostgreSQL) +``` + +**Regra fundamental:** O `domain` e os `ports` **nunca** importam nada de `adapters` ou `application`. Dependências permitidas no domínio: `uuid`, `serde`, `time`. Qualquer violação é um bug arquitetural. + +--- + +## Rotas da API + +Todas as rotas são prefixadas com `/api/v1`. + +### Auth — `/api/v1/auth` + +| Método | Rota | Handler | Acesso | +|--------|-----------------------|---------------------------|--------------------------------| +| POST | `/register` | `register_handler` | Público | +| POST | `/login` | `login_handler` | Público | +| POST | `/oauth/google` | `google_oauth_handler` | Público | +| POST | `/refresh` | `refresh_token_handler` | Público (requer refresh token) | +| POST | `/logout` | `logout_handler` | Autenticado | +| POST | `/forgot-password` | `forgot_password_handler` | Público | +| POST | `/verify-email` | `verify_email_handler` | Público | +| POST | `/reset-password` | `reset_password_handler` | Público (requer token de reset)| + +### Users — `/api/v1/users` + +| Método | Rota | Handler | Acesso | +|--------|-------|---------------------|-------------| +| GET | `/me` | `get_me_handler` | Autenticado | +| PUT | `/me` | `update_me_handler` | Autenticado | + +### Filaments — `/api/v1/filaments` + +| Método | Rota | Handler | Acesso | +|--------|-------------------|----------------------------|-------------| +| GET | `/` | `list_filaments_handler` | Autenticado | +| POST | `/` | `create_filament_handler` | Autenticado | +| GET | `/:id` | `get_filament_handler` | Autenticado | +| PUT | `/:id` | `update_filament_handler` | Autenticado | +| DELETE | `/:id` | `delete_filament_handler` | Autenticado | +| GET | `/:id/qrcode` | `get_qrcode_handler` | Autenticado | +| GET | `/:id/label.svg` | `export_label_handler` | Autenticado | + +**Query params de listagem (`GET /filaments`):** +- `material` — filtra por tipo (PLA, ABS, PETG, TPU, ASA, PA, PC...) +- `brand` — filtra por marca +- `search` — busca em marca, modelo e notas +- `stock_level` — `low` (<=15%), `medium` (<=35%), `ok` (>35%) +- `sort` — `net_weight_asc`, `net_weight_desc`, `created_at_desc` (padrão) +- `page` / `per_page` — paginação (padrão: página 1, 20 itens) + +### Spool Presets — `/api/v1/spool-presets` + +| Método | Rota | Handler | Acesso | +|--------|----------|--------------------------|-------------------------------------| +| GET | `/` | `list_presets_handler` | Autenticado | +| POST | `/` | `create_preset_handler` | Autenticado | +| PUT | `/:id` | `update_preset_handler` | Autenticado (apenas presets do user)| +| DELETE | `/:id` | `delete_preset_handler` | Autenticado (apenas presets do user)| + +### Dashboard — `/api/v1/dashboard` + +| Método | Rota | Handler | Acesso | +|--------|------|----------------------|-------------| +| GET | `/` | `dashboard_handler` | Autenticado | + +**Resposta do dashboard:** +```json +{ + "total_stock_kg": 14.2, + "low_stock_count": 3, + "by_material": [ + { "material": "PLA", "count": 4, "total_kg": 5.8 } + ], + "low_stock_filaments": [ + { "id": "...", "model": "PETG", "net_weight_g": 120, "percentage": 12 } + ], + "recent_filaments": [...] +} +``` + +--- + +## Padrão de Erros + +Use `thiserror` em todos os módulos e um `AppError` central em `src/error.rs`: + +```rust +#[derive(Debug, thiserror::Error)] +pub enum AppError { + #[error("not found")] + NotFound, + + #[error("unauthorized")] + Unauthorized, + + #[error("forbidden")] + Forbidden, + + #[error("validation error: {0}")] + Validation(String), + + #[error("conflict: {0}")] + Conflict(String), + + #[error("internal error")] + Internal(#[from] anyhow::Error), +} +``` + +`AppError` implementa `IntoResponse` do Axum, mapeando cada variante para o status HTTP correto e body JSON consistente: + +```json +{ "error": "not found", "code": "NOT_FOUND" } +``` + +--- + +## Convenções de Código + +### Nomenclatura + +- **Structs de domínio:** `PascalCase` — `User`, `Filament`, `SpoolPreset` +- **Traits (ports):** `PascalCase` com sufixo `Repository` — `UserRepository` +- **Implementações concretas:** prefixo do banco — `PostgresUserRepository` +- **Serviços:** sufixo `Service` — `AuthService`, `FilamentService` +- **Handlers:** sufixo `_handler` — `login_handler`, `create_filament_handler` +- **DTOs de entrada:** sufixo `Request` — `CreateFilamentRequest` +- **DTOs de saída:** sufixo `Response` — `FilamentResponse` + +### Estrutura de um Handler + +Todo handler deve ser uma função async pequena. A lógica de negócio **nunca** fica no handler — ela fica no `application/*_service.rs`. + +```rust +// adapters/inbound/filament_handler.rs +pub async fn create_filament_handler( + State(state): State, + Extension(current_user): Extension, + Json(req): Json, +) -> Result { + req.validate()?; + let filament = state.filament_service.create(current_user.id, req).await?; + Ok((StatusCode::CREATED, Json(FilamentResponse::from(filament)))) +} +``` + +### Estrutura de um Service + +O service recebe repositórios via injeção de dependência (trait objects em Arc): + +```rust +// application/filament_service.rs +pub struct FilamentService { + repo: Arc, + preset_repo: Arc, +} + +impl FilamentService { + pub async fn create(&self, user_id: Uuid, req: CreateFilamentRequest) -> Result { + // 1. buscar preset para calcular net_weight + // 2. calcular net_weight_g = total_weight_g - spool_weight_g + // 3. construir entidade Filament + // 4. persistir via repo + // 5. retornar entidade + } +} +``` + +### Estrutura de um Repository (Port) + +```rust +// ports/filament_repository.rs +#[async_trait::async_trait] +pub trait FilamentRepository: Send + Sync { + async fn find_by_id(&self, id: Uuid, user_id: Uuid) -> Result, AppError>; + async fn list(&self, user_id: Uuid, filter: FilamentFilter) -> Result, AppError>; + async fn create(&self, filament: &Filament) -> Result; + async fn update(&self, filament: &Filament) -> Result; + async fn delete(&self, id: Uuid, user_id: Uuid) -> Result<(), AppError>; +} +``` + +--- + +## Migrations + +Use o **SQLx CLI** para gerenciar migrations: + +```bash +# instalar +cargo install sqlx-cli --no-default-features --features postgres + +# criar nova migration +sqlx migrate add + +# aplicar +sqlx migrate run + +# reverter +sqlx migrate revert +``` + +Arquivos ficam em `backend/migrations/`. Nomeie com timestamp e descrição clara. + +--- + +## Variáveis de Ambiente + +Copie `.env.example` para `.env` antes de rodar. Veja o arquivo `.env.example` para a lista completa. + +--- + +## Como Adicionar um Novo Endpoint (Passo a Passo) + +1. **Domain** — adicione ou modifique a entidade em `src/domain/`. +2. **Port** — adicione o método necessário no trait em `src/ports/`. +3. **Outbound Adapter** — implemente o método no repositório PostgreSQL em `src/adapters/outbound/`. +4. **Application Service** — adicione o caso de uso em `src/application/`, chamando o port. +5. **Request/Response DTOs** — defina structs com `serde` e `validator` no handler. +6. **Inbound Handler** — crie o handler em `src/adapters/inbound/`, delegando para o service. +7. **Router** — registre a rota em `src/router.rs`. +8. **Migration** — se necessário, crie um arquivo SQL em `migrations/`. + +--- + +## Como Rodar Localmente + +```bash +# na raiz do backend/ +cp .env.example .env +# edite .env com suas credenciais locais + +# subir PostgreSQL via Docker +docker run -d \ + --name meowspool-db \ + -e POSTGRES_USER=meowspool \ + -e POSTGRES_PASSWORD=meowspool \ + -e POSTGRES_DB=meowspool \ + -p 5432:5432 \ + postgres:16-alpine + +# rodar migrations +sqlx migrate run + +# rodar em modo watch (requer cargo-watch) +cargo watch -x run +``` + +--- + +## Sincronização Offline (Estratégia) + +O backend implementa **last-write-wins com timestamp**: + +- Toda entidade possui `updated_at` (timestamp UTC). +- O mobile envia o `updated_at` local no body do PUT. +- Se o `updated_at` do servidor for mais recente, retorna `409 Conflict` com a versão do servidor. +- Se o `updated_at` do cliente for mais recente (ou igual), o servidor aceita a atualização. + +--- + +## Geração de QR Code e Etiqueta SVG + +### QR Code (`GET /filaments/:id/qrcode`) +- Gera QR Code com deep link: `meowspool://filaments/:id` +- Retorna PNG (`image/png`) por padrão, ou SVG com `?format=svg` +- Biblioteca: crate `qrcode` + +### Etiqueta SVG (`GET /filaments/:id/label.svg`) +- Query params: `width_mm` (padrão: 50), `height_mm` (padrão: 30) +- Retorna SVG com: cor visual, modelo, material, marca, peso líquido e QR Code embutido +- `Content-Type: image/svg+xml` +- `Content-Disposition: attachment; filename="meowspool-label-{id}.svg"` + +--- + +## Segurança + +- Senhas hasheadas com **Argon2id** (crate `argon2`). +- JWT assinado com **HS256** — nunca exponha o `JWT_SECRET`. +- Toda rota autenticada passa pelo middleware `auth.rs` que valida o token e popula `Extension`. +- Queries usam **bind parameters** do SQLx — nunca interpolação de string em SQL. +- `user_id` é sempre extraído do token JWT, nunca aceito como parâmetro de URL ou body. +- Presets do sistema (`is_system = true`) são protegidos no nível de serviço: edição ou deleção retorna `403 Forbidden`. diff --git a/backend/bruno-routes/MewoSpool/.gitignore b/backend/bruno-routes/MewoSpool/.gitignore new file mode 100644 index 0000000..e19311f --- /dev/null +++ b/backend/bruno-routes/MewoSpool/.gitignore @@ -0,0 +1,9 @@ +# Secrets +.env* + +# Dependencies +node_modules + +# OS files +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/backend/bruno-routes/MewoSpool/Auth/Forgot Password.yml b/backend/bruno-routes/MewoSpool/Auth/Forgot Password.yml new file mode 100644 index 0000000..7d9a32a --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Auth/Forgot Password.yml @@ -0,0 +1,20 @@ +info: + name: Forgot Password + type: http + seq: 5 + +http: + method: POST + url: http://0.0.0.0:8080/api/v1/auth/forgot-password + body: + type: json + data: |- + { + "email": "felipe@felipecncloud.com" + } + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Auth/Login.yml b/backend/bruno-routes/MewoSpool/Auth/Login.yml new file mode 100644 index 0000000..f38d739 --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Auth/Login.yml @@ -0,0 +1,21 @@ +info: + name: Login + type: http + seq: 2 + +http: + method: POST + url: http://0.0.0.0:8080/api/v1/auth/login + body: + type: json + data: |- + { + "email": "felipe@felipecncloud.com", + "password": "12345678" + } + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Auth/Logout.yml b/backend/bruno-routes/MewoSpool/Auth/Logout.yml new file mode 100644 index 0000000..8dcd385 --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Auth/Logout.yml @@ -0,0 +1,24 @@ +info: + name: Logout + type: http + seq: 2 + +http: + method: POST + url: http://0.0.0.0:8080/api/v1/auth/logout + body: + type: json + data: |- + { + "email": "felipe@felipecncloud.com", + "password": "l11f06c10" + } + auth: + type: bearer + token: "{{access_token}}" + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Auth/OAuth Google.yml b/backend/bruno-routes/MewoSpool/Auth/OAuth Google.yml new file mode 100644 index 0000000..7db25d7 --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Auth/OAuth Google.yml @@ -0,0 +1,20 @@ +info: + name: OAuth Google + type: http + seq: 8 + +http: + method: POST + url: http://0.0.0.0:8080/api/v1/auth/oauth/google + body: + type: json + data: |- + { + "id_token": "{{google_id_token}}" + } + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Auth/Refresh Token.yml b/backend/bruno-routes/MewoSpool/Auth/Refresh Token.yml new file mode 100644 index 0000000..a6bd4c3 --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Auth/Refresh Token.yml @@ -0,0 +1,20 @@ +info: + name: Refresh Token + type: http + seq: 4 + +http: + method: POST + url: http://0.0.0.0:8080/api/v1/auth/refresh + body: + type: json + data: |- + { + "refresh_token": "{{refresh_token}}" + } + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Auth/Register.yml b/backend/bruno-routes/MewoSpool/Auth/Register.yml new file mode 100644 index 0000000..c80ad4e --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Auth/Register.yml @@ -0,0 +1,21 @@ +info: + name: Register + type: http + seq: 1 + +http: + method: POST + url: http://0.0.0.0:8080/api/v1/auth/register + body: + type: json + data: |- + { + "email": "felipe@felipecncloud.com", + "password": "12345678" + } + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Auth/Reset Password.yml b/backend/bruno-routes/MewoSpool/Auth/Reset Password.yml new file mode 100644 index 0000000..cbf7d27 --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Auth/Reset Password.yml @@ -0,0 +1,21 @@ +info: + name: Reset Password + type: http + seq: 7 + +http: + method: POST + url: http://0.0.0.0:8080/api/v1/auth/reset-password + body: + type: json + data: |- + { + "token": "{{reset_token}}", + "new_password": "novaSenha123" + } + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Auth/Verify Email.yml b/backend/bruno-routes/MewoSpool/Auth/Verify Email.yml new file mode 100644 index 0000000..fe965d3 --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Auth/Verify Email.yml @@ -0,0 +1,20 @@ +info: + name: Verify Email + type: http + seq: 6 + +http: + method: POST + url: http://0.0.0.0:8080/api/v1/auth/verify-email + body: + type: json + data: |- + { + "token": "{{email_verify_token}}" + } + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Auth/folder.yml b/backend/bruno-routes/MewoSpool/Auth/folder.yml new file mode 100644 index 0000000..d572964 --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Auth/folder.yml @@ -0,0 +1,6 @@ +info: + name: Auth + type: folder + seq: 2 + +request: {} diff --git a/backend/bruno-routes/MewoSpool/Dashboard.yml b/backend/bruno-routes/MewoSpool/Dashboard.yml new file mode 100644 index 0000000..718bd9f --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Dashboard.yml @@ -0,0 +1,17 @@ +info: + name: Dashboard + type: http + seq: 1 + +http: + method: GET + url: http://0.0.0.0:8080/api/v1/dashboard + auth: + type: bearer + token: "{{access_token}}" + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Filaments/Create Filament.yml b/backend/bruno-routes/MewoSpool/Filaments/Create Filament.yml new file mode 100644 index 0000000..35fa274 --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Filaments/Create Filament.yml @@ -0,0 +1,32 @@ +info: + name: Create Filament + type: http + seq: 2 + +http: + method: POST + url: http://0.0.0.0:8080/api/v1/filaments + body: + type: json + data: |- + { + "material": "PLA", + "brand": "Bambu", + "model": "PLA Basic", + "color_hex": "#FF5733", + "spool_preset_id": "{{preset_id}}", + "total_weight_g": 1250, + "temp_hotend_c": 220, + "temp_bed_c": 60, + "flow_factor_pct": 1.0, + "notes": "Filamento de teste" + } + auth: + type: bearer + token: "{{access_token}}" + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Filaments/Delete Filament.yml b/backend/bruno-routes/MewoSpool/Filaments/Delete Filament.yml new file mode 100644 index 0000000..b9ebe6e --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Filaments/Delete Filament.yml @@ -0,0 +1,17 @@ +info: + name: Delete Filament + type: http + seq: 5 + +http: + method: DELETE + url: http://0.0.0.0:8080/api/v1/filaments/{{filament_id}} + auth: + type: bearer + token: "{{access_token}}" + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Filaments/Export Label SVG.yml b/backend/bruno-routes/MewoSpool/Filaments/Export Label SVG.yml new file mode 100644 index 0000000..72855ec --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Filaments/Export Label SVG.yml @@ -0,0 +1,24 @@ +info: + name: Export Label SVG + type: http + seq: 7 + +http: + method: GET + url: http://0.0.0.0:8080/api/v1/filaments/{{filament_id}}/label.svg + params: + - name: width_mm + value: "80" + enabled: true + - name: height_mm + value: "30" + enabled: true + auth: + type: bearer + token: "{{access_token}}" + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Filaments/Get Filament.yml b/backend/bruno-routes/MewoSpool/Filaments/Get Filament.yml new file mode 100644 index 0000000..295d9dd --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Filaments/Get Filament.yml @@ -0,0 +1,17 @@ +info: + name: Get Filament + type: http + seq: 3 + +http: + method: GET + url: http://0.0.0.0:8080/api/v1/filaments/{{filament_id}} + auth: + type: bearer + token: "{{access_token}}" + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Filaments/Get QR Code.yml b/backend/bruno-routes/MewoSpool/Filaments/Get QR Code.yml new file mode 100644 index 0000000..e4ae7a4 --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Filaments/Get QR Code.yml @@ -0,0 +1,22 @@ +info: + name: Get QR Code + type: http + seq: 6 + +http: + method: GET + url: http://0.0.0.0:8080/api/v1/filaments/{{filament_id}}/qrcode + auth: + type: bearer + token: "{{access_token}}" + +runtime: + variables: + - name: filament_id + value: e1edc611-f020-4234-94e5-db5e1393dbf2 + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Filaments/List Filaments.yml b/backend/bruno-routes/MewoSpool/Filaments/List Filaments.yml new file mode 100644 index 0000000..2f0f7c5 --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Filaments/List Filaments.yml @@ -0,0 +1,36 @@ +info: + name: List Filaments + type: http + seq: 1 + +http: + method: GET + url: http://0.0.0.0:8080/api/v1/filaments + params: + - name: page + value: "1" + enabled: true + - name: per_page + value: "20" + enabled: true + - name: material + value: PLA + enabled: false + - name: brand + value: Bambu + enabled: false + - name: search + value: "" + enabled: false + - name: stock_level + value: low + enabled: false + auth: + type: bearer + token: "{{access_token}}" + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Filaments/Update Filament.yml b/backend/bruno-routes/MewoSpool/Filaments/Update Filament.yml new file mode 100644 index 0000000..98ddc93 --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Filaments/Update Filament.yml @@ -0,0 +1,32 @@ +info: + name: Update Filament + type: http + seq: 4 + +http: + method: PUT + url: http://0.0.0.0:8080/api/v1/filaments/{{filament_id}} + body: + type: json + data: |- + { + "material": "PETG", + "brand": "Bambu", + "model": "PETG Basic", + "color_hex": "#33A1FF", + "spool_preset_id": "{{preset_id}}", + "total_weight_g": 1000, + "temp_hotend_c": 240, + "temp_bed_c": 70, + "flow_factor_pct": 1.0, + "notes": "Atualizado" + } + auth: + type: bearer + token: "{{access_token}}" + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Filaments/folder.yml b/backend/bruno-routes/MewoSpool/Filaments/folder.yml new file mode 100644 index 0000000..28381ce --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Filaments/folder.yml @@ -0,0 +1,6 @@ +info: + name: Filaments + type: folder + seq: 3 + +request: {} diff --git a/backend/bruno-routes/MewoSpool/SpoolPresets/Create Spool Preset.yml b/backend/bruno-routes/MewoSpool/SpoolPresets/Create Spool Preset.yml new file mode 100644 index 0000000..6e90d3f --- /dev/null +++ b/backend/bruno-routes/MewoSpool/SpoolPresets/Create Spool Preset.yml @@ -0,0 +1,24 @@ +info: + name: Create Spool Preset + type: http + seq: 2 + +http: + method: POST + url: http://0.0.0.0:8080/api/v1/spool-presets + body: + type: json + data: |- + { + "name": "Carretel Customizado 300g", + "spool_weight_g": 300 + } + auth: + type: bearer + token: "{{access_token}}" + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/SpoolPresets/Delete Spool Preset.yml b/backend/bruno-routes/MewoSpool/SpoolPresets/Delete Spool Preset.yml new file mode 100644 index 0000000..0d930aa --- /dev/null +++ b/backend/bruno-routes/MewoSpool/SpoolPresets/Delete Spool Preset.yml @@ -0,0 +1,17 @@ +info: + name: Delete Spool Preset + type: http + seq: 4 + +http: + method: DELETE + url: http://0.0.0.0:8080/api/v1/spool-presets/{{preset_id}} + auth: + type: bearer + token: "{{access_token}}" + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/SpoolPresets/List Spool Presets.yml b/backend/bruno-routes/MewoSpool/SpoolPresets/List Spool Presets.yml new file mode 100644 index 0000000..c2aa136 --- /dev/null +++ b/backend/bruno-routes/MewoSpool/SpoolPresets/List Spool Presets.yml @@ -0,0 +1,17 @@ +info: + name: List Spool Presets + type: http + seq: 1 + +http: + method: GET + url: http://0.0.0.0:8080/api/v1/spool-presets + auth: + type: bearer + token: "{{access_token}}" + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/SpoolPresets/Update Spool Preset.yml b/backend/bruno-routes/MewoSpool/SpoolPresets/Update Spool Preset.yml new file mode 100644 index 0000000..1eea351 --- /dev/null +++ b/backend/bruno-routes/MewoSpool/SpoolPresets/Update Spool Preset.yml @@ -0,0 +1,24 @@ +info: + name: Update Spool Preset + type: http + seq: 3 + +http: + method: PUT + url: http://0.0.0.0:8080/api/v1/spool-presets/{{preset_id}} + body: + type: json + data: |- + { + "name": "Carretel Customizado 350g", + "spool_weight_g": 350 + } + auth: + type: bearer + token: "{{access_token}}" + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/SpoolPresets/folder.yml b/backend/bruno-routes/MewoSpool/SpoolPresets/folder.yml new file mode 100644 index 0000000..1b2044f --- /dev/null +++ b/backend/bruno-routes/MewoSpool/SpoolPresets/folder.yml @@ -0,0 +1,6 @@ +info: + name: SpoolPresets + type: folder + seq: 4 + +request: {} diff --git a/backend/bruno-routes/MewoSpool/Users/Get Me.yml b/backend/bruno-routes/MewoSpool/Users/Get Me.yml new file mode 100644 index 0000000..4cba86f --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Users/Get Me.yml @@ -0,0 +1,17 @@ +info: + name: Get Me + type: http + seq: 1 + +http: + method: GET + url: http://0.0.0.0:8080/api/v1/users/me + auth: + type: bearer + token: "{{access_token}}" + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Users/Update Me.yml b/backend/bruno-routes/MewoSpool/Users/Update Me.yml new file mode 100644 index 0000000..56a85be --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Users/Update Me.yml @@ -0,0 +1,23 @@ +info: + name: Update Me + type: http + seq: 2 + +http: + method: PUT + url: http://0.0.0.0:8080/api/v1/users/me + body: + type: json + data: |- + { + "email": "felipe@felipecncloud.com" + } + auth: + type: bearer + token: "{{access_token}}" + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/backend/bruno-routes/MewoSpool/Users/folder.yml b/backend/bruno-routes/MewoSpool/Users/folder.yml new file mode 100644 index 0000000..f996cdd --- /dev/null +++ b/backend/bruno-routes/MewoSpool/Users/folder.yml @@ -0,0 +1,6 @@ +info: + name: Users + type: folder + seq: 5 + +request: {} diff --git a/backend/bruno-routes/MewoSpool/environments/route.yml b/backend/bruno-routes/MewoSpool/environments/route.yml new file mode 100644 index 0000000..249b1ac --- /dev/null +++ b/backend/bruno-routes/MewoSpool/environments/route.yml @@ -0,0 +1,10 @@ +name: route +variables: + - secret: true + name: access_token + - secret: true + name: refresh_token + - name: preset_id + value: 0ffa808d-af09-4bc6-9a40-43df3a4c6dd0 + - name: filament_id + value: e1edc611-f020-4234-94e5-db5e1393dbf2 diff --git a/backend/bruno-routes/MewoSpool/opencollection.yml b/backend/bruno-routes/MewoSpool/opencollection.yml new file mode 100644 index 0000000..ae53510 --- /dev/null +++ b/backend/bruno-routes/MewoSpool/opencollection.yml @@ -0,0 +1,10 @@ +opencollection: 1.0.0 + +info: + name: MewoSpool +bundled: false +extensions: + bruno: + ignore: + - node_modules + - .git diff --git a/backend/infra/compose.yaml b/backend/infra/compose.yaml new file mode 100644 index 0000000..270b605 --- /dev/null +++ b/backend/infra/compose.yaml @@ -0,0 +1,17 @@ +services: + database: + image: "postgres:16.0-alpine3.18" + env_file: + - ../.env.development + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 10 + +volumes: + postgres_data: diff --git a/backend/migrations/20240101000001_create_users.sql b/backend/migrations/20240101000001_create_users.sql new file mode 100644 index 0000000..eb2fa24 --- /dev/null +++ b/backend/migrations/20240101000001_create_users.sql @@ -0,0 +1,16 @@ +-- Migration: 001 - Criar tabela de usuários + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + email TEXT NOT NULL UNIQUE, + password_hash TEXT, + google_id TEXT UNIQUE, + email_verified BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_users_email ON users (email); +CREATE INDEX idx_users_google_id ON users (google_id) WHERE google_id IS NOT NULL; diff --git a/backend/migrations/20240101000002_create_spool_presets.sql b/backend/migrations/20240101000002_create_spool_presets.sql new file mode 100644 index 0000000..78bd763 --- /dev/null +++ b/backend/migrations/20240101000002_create_spool_presets.sql @@ -0,0 +1,30 @@ +-- Migration: 002 - Criar tabela de presets de carretel e inserir presets do sistema + +CREATE TABLE spool_presets ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL, + spool_weight_g INTEGER NOT NULL CHECK (spool_weight_g > 0), + is_system BOOLEAN NOT NULL DEFAULT FALSE, + user_id UUID REFERENCES users (id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Presets do sistema não têm user_id; presets customizados têm + CONSTRAINT chk_preset_ownership CHECK ( + (is_system = TRUE AND user_id IS NULL) OR + (is_system = FALSE AND user_id IS NOT NULL) + ) +); + +CREATE INDEX idx_spool_presets_user_id ON spool_presets (user_id) WHERE user_id IS NOT NULL; +CREATE INDEX idx_spool_presets_is_system ON spool_presets (is_system); + +-- Presets built-in do sistema (somente leitura para usuários) +INSERT INTO spool_presets (name, spool_weight_g, is_system) VALUES + ('Bambu Lab (Plástico)', 250, TRUE), + ('Elegoo (Papelão)', 200, TRUE), + ('Creality (Plástico)', 230, TRUE), + ('Prusament (Plástico)', 201, TRUE), + ('Sunlu (Papelão)', 200, TRUE), + ('Polymaker (Plástico)', 220, TRUE), + ('Genérico Papelão 1kg', 200, TRUE), + ('Genérico Plástico 1kg', 250, TRUE); diff --git a/backend/migrations/20240101000003_create_filaments.sql b/backend/migrations/20240101000003_create_filaments.sql new file mode 100644 index 0000000..4b30773 --- /dev/null +++ b/backend/migrations/20240101000003_create_filaments.sql @@ -0,0 +1,26 @@ +-- Migration: 003 - Criar tabela de filamentos + +CREATE TABLE filaments ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE, + material TEXT NOT NULL, + brand TEXT NOT NULL, + model TEXT, + color_hex TEXT NOT NULL, + spool_preset_id UUID NOT NULL REFERENCES spool_presets (id), + total_weight_g INTEGER NOT NULL CHECK (total_weight_g > 0), + net_weight_g INTEGER NOT NULL CHECK (net_weight_g >= 0), + temp_hotend_c INTEGER CHECK (temp_hotend_c > 0), + temp_bed_c INTEGER CHECK (temp_bed_c >= 0), + flow_factor_pct DOUBLE PRECISION CHECK (flow_factor_pct > 0), + notes TEXT, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_filaments_user_id ON filaments (user_id); +CREATE INDEX idx_filaments_material ON filaments (user_id, material); +CREATE INDEX idx_filaments_brand ON filaments (user_id, brand); +CREATE INDEX idx_filaments_updated_at ON filaments (user_id, updated_at DESC); +-- Índice para suporte a alertas de estoque baixo +CREATE INDEX idx_filaments_net_weight ON filaments (user_id, net_weight_g); diff --git a/backend/src/adapters/inbound/auth_handler.rs b/backend/src/adapters/inbound/auth_handler.rs new file mode 100644 index 0000000..2e08ce1 --- /dev/null +++ b/backend/src/adapters/inbound/auth_handler.rs @@ -0,0 +1,190 @@ +use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; +use serde::{Deserialize, Serialize}; +use validator::Validate; + +use crate::{error::AppError, router::AppState}; + +// ------------------------------------------------------------------------- +// Request / Response DTOs +// ------------------------------------------------------------------------- + +#[derive(Debug, Deserialize, Validate)] +pub struct RegisterRequest { + #[validate(email(message = "invalid email format"))] + pub email: String, + #[validate(length(min = 8, message = "password must be at least 8 characters"))] + pub password: String, +} + +#[derive(Debug, Deserialize, Validate)] +pub struct LoginRequest { + #[validate(email)] + pub email: String, + pub password: String, +} + +#[derive(Debug, Deserialize)] +pub struct GoogleOAuthRequest { + /// ID token retornado pelo SDK do Google no cliente + pub id_token: String, +} + +#[derive(Debug, Deserialize)] +pub struct RefreshTokenRequest { + pub refresh_token: String, +} + +#[derive(Debug, Deserialize, Validate)] +pub struct ForgotPasswordRequest { + #[validate(email)] + pub email: String, +} + +#[derive(Debug, Deserialize)] +pub struct VerifyEmailRequest { + pub token: String, +} + +#[derive(Debug, Deserialize, Validate)] +pub struct ResetPasswordRequest { + pub token: String, + #[validate(length(min = 8))] + pub new_password: String, +} + +#[derive(Debug, Serialize)] +pub struct AuthResponse { + pub access_token: String, + pub refresh_token: String, + pub token_type: &'static str, +} + +// ------------------------------------------------------------------------- +// Handlers +// ------------------------------------------------------------------------- + +pub async fn register_handler( + State(state): State, + Json(req): Json, +) -> Result { + req.validate()?; + let tokens = state.auth_service.register(req.email, req.password).await?; + Ok(( + StatusCode::CREATED, + Json(AuthResponse { + access_token: tokens.access_token, + refresh_token: tokens.refresh_token, + token_type: "Bearer", + }), + )) +} + +pub async fn login_handler( + State(state): State, + Json(req): Json, +) -> Result { + req.validate()?; + let tokens = state.auth_service.login(req.email, req.password).await?; + Ok(Json(AuthResponse { + access_token: tokens.access_token, + refresh_token: tokens.refresh_token, + token_type: "Bearer", + })) +} + +pub async fn google_oauth_handler( + State(state): State, + Json(req): Json, +) -> Result { + // Verificar id_token com a API do Google e extrair sub + email + let (google_id, email) = verify_google_id_token(&req.id_token, &state.config.google_client_id).await?; + let tokens = state.auth_service.google_oauth(google_id, email).await?; + Ok(Json(AuthResponse { + access_token: tokens.access_token, + refresh_token: tokens.refresh_token, + token_type: "Bearer", + })) +} + +pub async fn refresh_token_handler( + State(state): State, + Json(req): Json, +) -> Result { + let tokens = state.auth_service.refresh_token(&req.refresh_token).await?; + Ok(Json(AuthResponse { + access_token: tokens.access_token, + refresh_token: tokens.refresh_token, + token_type: "Bearer", + })) +} + +pub async fn logout_handler() -> impl IntoResponse { + // JWT é stateless; o cliente descarta os tokens. + // Implementação futura: blocklist de refresh tokens via Redis. + StatusCode::NO_CONTENT +} + +pub async fn forgot_password_handler( + Json(req): Json, +) -> Result { + req.validate()?; + // TODO: gerar token de reset e enviar email + // Retornamos sempre 200 para não vazar informação sobre emails cadastrados + Ok(StatusCode::OK) +} + +pub async fn verify_email_handler( + Json(_req): Json, +) -> Result { + // TODO: validar token e marcar email_verified = true + Ok(StatusCode::OK) +} + +pub async fn reset_password_handler( + Json(req): Json, +) -> Result { + req.validate()?; + // TODO: validar token de reset e atualizar senha + Ok(StatusCode::OK) +} + +// ------------------------------------------------------------------------- +// Helper: verificação de Google ID Token +// ------------------------------------------------------------------------- + +/// Verifica o id_token do Google chamando o endpoint tokeninfo e retorna (sub, email). +async fn verify_google_id_token( + id_token: &str, + _client_id: &str, +) -> Result<(String, String), AppError> { + let url = format!( + "https://oauth2.googleapis.com/tokeninfo?id_token={id_token}" + ); + + let resp = reqwest::Client::new() + .get(&url) + .send() + .await + .map_err(|e| AppError::Internal(e.into()))?; + + if !resp.status().is_success() { + return Err(AppError::Unauthorized); + } + + let payload: serde_json::Value = resp + .json() + .await + .map_err(|e| AppError::Internal(e.into()))?; + + let sub = payload["sub"] + .as_str() + .ok_or(AppError::Unauthorized)? + .to_string(); + + let email = payload["email"] + .as_str() + .ok_or(AppError::Unauthorized)? + .to_string(); + + Ok((sub, email)) +} diff --git a/backend/src/adapters/inbound/filament_handler.rs b/backend/src/adapters/inbound/filament_handler.rs new file mode 100644 index 0000000..feab234 --- /dev/null +++ b/backend/src/adapters/inbound/filament_handler.rs @@ -0,0 +1,311 @@ +use axum::{ + extract::{Path, Query, State}, + http::{header, StatusCode}, + response::IntoResponse, + Extension, Json, +}; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use uuid::Uuid; +use validator::Validate; + +use crate::{ + adapters::inbound::middleware::auth::CurrentUser, + application::filament_service::{ + CreateFilamentInput, DashboardStats, MaterialSummary, UpdateFilamentInput, + }, + domain::filament::Filament, + error::AppError, + ports::FilamentFilter, + router::AppState, +}; + +// ------------------------------------------------------------------------- +// Query params +// ------------------------------------------------------------------------- + +#[derive(Debug, Deserialize, Default)] +pub struct ListFilamentsQuery { + pub material: Option, + pub brand: Option, + pub search: Option, + pub stock_level: Option, + pub sort: Option, + #[serde(default = "default_page")] + pub page: u32, + #[serde(default = "default_per_page")] + pub per_page: u32, +} + +fn default_page() -> u32 { 1 } +fn default_per_page() -> u32 { 20 } + +#[derive(Debug, Deserialize)] +pub struct LabelQuery { + #[serde(default = "default_width_mm")] + pub width_mm: u32, + #[serde(default = "default_height_mm")] + pub height_mm: u32, +} + +fn default_width_mm() -> u32 { 50 } +fn default_height_mm() -> u32 { 30 } + +// ------------------------------------------------------------------------- +// Request / Response DTOs +// ------------------------------------------------------------------------- + +#[derive(Debug, Deserialize, Validate)] +pub struct CreateFilamentRequest { + #[validate(length(min = 1, max = 20))] + pub material: String, + #[validate(length(min = 1, max = 100))] + pub brand: String, + #[validate(length(max = 100))] + pub model: Option, + #[validate(length(min = 4, max = 7))] + pub color_hex: String, + pub spool_preset_id: Uuid, + #[validate(range(min = 1))] + pub total_weight_g: i32, + pub temp_hotend_c: Option, + pub temp_bed_c: Option, + pub flow_factor_pct: Option, + #[validate(length(max = 1000))] + pub notes: Option, +} + +#[derive(Debug, Deserialize, Validate)] +pub struct UpdateFilamentRequest { + #[validate(length(min = 1, max = 20))] + pub material: String, + #[validate(length(min = 1, max = 100))] + pub brand: String, + #[validate(length(max = 100))] + pub model: Option, + #[validate(length(min = 4, max = 7))] + pub color_hex: String, + pub spool_preset_id: Uuid, + #[validate(range(min = 1))] + pub total_weight_g: i32, + pub temp_hotend_c: Option, + pub temp_bed_c: Option, + pub flow_factor_pct: Option, + #[validate(length(max = 1000))] + pub notes: Option, + pub client_updated_at: Option, +} + +#[derive(Debug, Serialize)] +pub struct FilamentResponse { + pub id: Uuid, + pub user_id: Uuid, + pub material: String, + pub brand: String, + pub model: Option, + pub color_hex: String, + pub spool_preset_id: Uuid, + pub total_weight_g: i32, + pub net_weight_g: i32, + pub stock_percentage: u8, + pub temp_hotend_c: Option, + pub temp_bed_c: Option, + pub flow_factor_pct: Option, + pub notes: Option, + pub updated_at: OffsetDateTime, + pub created_at: OffsetDateTime, +} + +impl From for FilamentResponse { + fn from(f: Filament) -> Self { + let stock_percentage = f.stock_percentage(); + Self { + id: f.id, + user_id: f.user_id, + material: f.material.to_string(), + brand: f.brand, + model: f.model, + color_hex: f.color_hex, + spool_preset_id: f.spool_preset_id, + total_weight_g: f.total_weight_g, + net_weight_g: f.net_weight_g, + stock_percentage, + temp_hotend_c: f.temp_hotend_c, + temp_bed_c: f.temp_bed_c, + flow_factor_pct: f.flow_factor_pct, + notes: f.notes, + updated_at: f.updated_at, + created_at: f.created_at, + } + } +} + +#[derive(Debug, Serialize)] +pub struct DashboardResponse { + pub total_stock_kg: f64, + pub low_stock_count: i64, + pub by_material: Vec, + pub low_stock_filaments: Vec, + pub recent_filaments: Vec, +} + +#[derive(Debug, Serialize)] +pub struct MaterialSummaryResponse { + pub material: String, + pub count: i64, + pub total_kg: f64, +} + +impl From for MaterialSummaryResponse { + fn from(m: MaterialSummary) -> Self { + Self { + material: m.material, + count: m.count, + total_kg: m.total_kg, + } + } +} + +impl From for DashboardResponse { + fn from(s: DashboardStats) -> Self { + Self { + total_stock_kg: s.total_stock_kg, + low_stock_count: s.low_stock_count, + by_material: s.by_material.into_iter().map(Into::into).collect(), + low_stock_filaments: s.low_stock_filaments.into_iter().map(Into::into).collect(), + recent_filaments: s.recent_filaments.into_iter().map(Into::into).collect(), + } + } +} + +// ------------------------------------------------------------------------- +// Handlers +// ------------------------------------------------------------------------- + +pub async fn dashboard_handler( + State(state): State, + Extension(user): Extension, +) -> Result { + let stats = state.filament_service.dashboard(user.id).await?; + Ok(Json(DashboardResponse::from(stats))) +} + +pub async fn list_filaments_handler( + State(state): State, + Extension(user): Extension, + Query(query): Query, +) -> Result { + let filter = FilamentFilter { + material: query.material, + brand: query.brand, + search: query.search, + stock_level: query.stock_level, + sort: None, // TODO: parse sort string para SortOrder + page: query.page, + per_page: query.per_page, + }; + + let filaments = state.filament_service.list(user.id, filter).await?; + let response: Vec = filaments.into_iter().map(Into::into).collect(); + Ok(Json(response)) +} + +pub async fn create_filament_handler( + State(state): State, + Extension(user): Extension, + Json(req): Json, +) -> Result { + req.validate()?; + let input = CreateFilamentInput { + material: req.material, + brand: req.brand, + model: req.model, + color_hex: req.color_hex, + spool_preset_id: req.spool_preset_id, + total_weight_g: req.total_weight_g, + temp_hotend_c: req.temp_hotend_c, + temp_bed_c: req.temp_bed_c, + flow_factor_pct: req.flow_factor_pct, + notes: req.notes, + }; + let filament = state.filament_service.create(user.id, input).await?; + Ok((StatusCode::CREATED, Json(FilamentResponse::from(filament)))) +} + +pub async fn get_filament_handler( + State(state): State, + Extension(user): Extension, + Path(id): Path, +) -> Result { + let filament = state.filament_service.get(id, user.id).await?; + Ok(Json(FilamentResponse::from(filament))) +} + +pub async fn update_filament_handler( + State(state): State, + Extension(user): Extension, + Path(id): Path, + Json(req): Json, +) -> Result { + req.validate()?; + let input = UpdateFilamentInput { + material: req.material, + brand: req.brand, + model: req.model, + color_hex: req.color_hex, + spool_preset_id: req.spool_preset_id, + total_weight_g: req.total_weight_g, + temp_hotend_c: req.temp_hotend_c, + temp_bed_c: req.temp_bed_c, + flow_factor_pct: req.flow_factor_pct, + notes: req.notes, + client_updated_at: req.client_updated_at, + }; + let filament = state.filament_service.update(id, user.id, input).await?; + Ok(Json(FilamentResponse::from(filament))) +} + +pub async fn delete_filament_handler( + State(state): State, + Extension(user): Extension, + Path(id): Path, +) -> Result { + state.filament_service.delete(id, user.id).await?; + Ok(StatusCode::NO_CONTENT) +} + +pub async fn get_qrcode_handler( + State(state): State, + Extension(user): Extension, + Path(id): Path, +) -> Result { + let png = state.filament_service.generate_qrcode_png(id, user.id).await?; + Ok(( + [(header::CONTENT_TYPE, "image/png")], + png, + )) +} + +pub async fn export_label_handler( + State(state): State, + Extension(user): Extension, + Path(id): Path, + Query(query): Query, +) -> Result { + let svg = state + .filament_service + .generate_label_svg(id, user.id, query.width_mm, query.height_mm) + .await?; + + let filename = format!("meowspool-label-{id}.svg"); + Ok(( + [ + (header::CONTENT_TYPE, "image/svg+xml".to_string()), + ( + header::CONTENT_DISPOSITION, + format!("attachment; filename=\"{filename}\""), + ), + ], + svg, + )) +} diff --git a/backend/src/adapters/inbound/middleware/auth.rs b/backend/src/adapters/inbound/middleware/auth.rs new file mode 100644 index 0000000..e2a39ee --- /dev/null +++ b/backend/src/adapters/inbound/middleware/auth.rs @@ -0,0 +1,47 @@ +use axum::{ + extract::{Request, State}, + middleware::Next, + response::Response, +}; +use uuid::Uuid; + +use crate::{error::AppError, router::AppState}; + +/// Informações do usuário autenticado, injetadas como Extension nos handlers. +#[derive(Debug, Clone)] +pub struct CurrentUser { + pub id: Uuid, + pub email: String, +} + +/// Middleware de autenticação JWT. +/// Extrai o Bearer token do header Authorization, valida via AuthService +/// e injeta `CurrentUser` como Extension para uso nos handlers. +pub async fn auth_middleware( + State(state): State, + mut req: Request, + next: Next, +) -> Result { + let token = extract_bearer_token(&req)?; + let claims = state.auth_service.validate_access_token(token)?; + + let current_user = CurrentUser { + id: claims.sub, + email: claims.email, + }; + + req.extensions_mut().insert(current_user); + Ok(next.run(req).await) +} + +fn extract_bearer_token(req: &Request) -> Result<&str, AppError> { + let header = req + .headers() + .get(axum::http::header::AUTHORIZATION) + .and_then(|v| v.to_str().ok()) + .ok_or(AppError::Unauthorized)?; + + header + .strip_prefix("Bearer ") + .ok_or(AppError::Unauthorized) +} diff --git a/backend/src/adapters/inbound/middleware/mod.rs b/backend/src/adapters/inbound/middleware/mod.rs new file mode 100644 index 0000000..0e4a05d --- /dev/null +++ b/backend/src/adapters/inbound/middleware/mod.rs @@ -0,0 +1 @@ +pub mod auth; diff --git a/backend/src/adapters/inbound/mod.rs b/backend/src/adapters/inbound/mod.rs new file mode 100644 index 0000000..165a7da --- /dev/null +++ b/backend/src/adapters/inbound/mod.rs @@ -0,0 +1,5 @@ +pub mod auth_handler; +pub mod filament_handler; +pub mod middleware; +pub mod spool_preset_handler; +pub mod user_handler; diff --git a/backend/src/adapters/inbound/spool_preset_handler.rs b/backend/src/adapters/inbound/spool_preset_handler.rs new file mode 100644 index 0000000..0d94670 --- /dev/null +++ b/backend/src/adapters/inbound/spool_preset_handler.rs @@ -0,0 +1,106 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + Extension, Json, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use validator::Validate; + +use crate::{ + adapters::inbound::middleware::auth::CurrentUser, + domain::spool_preset::SpoolPreset, + error::AppError, + router::AppState, +}; + +// ------------------------------------------------------------------------- +// Request / Response DTOs +// ------------------------------------------------------------------------- + +#[derive(Debug, Deserialize, Validate)] +pub struct CreatePresetRequest { + #[validate(length(min = 1, max = 100))] + pub name: String, + #[validate(range(min = 1, max = 2000))] + pub spool_weight_g: i32, +} + +#[derive(Debug, Deserialize, Validate)] +pub struct UpdatePresetRequest { + #[validate(length(min = 1, max = 100))] + pub name: String, + #[validate(range(min = 1, max = 2000))] + pub spool_weight_g: i32, +} + +#[derive(Debug, Serialize)] +pub struct SpoolPresetResponse { + pub id: Uuid, + pub name: String, + pub spool_weight_g: i32, + pub is_system: bool, + pub user_id: Option, +} + +impl From for SpoolPresetResponse { + fn from(p: SpoolPreset) -> Self { + Self { + id: p.id, + name: p.name, + spool_weight_g: p.spool_weight_g, + is_system: p.is_system, + user_id: p.user_id, + } + } +} + +// ------------------------------------------------------------------------- +// Handlers +// ------------------------------------------------------------------------- + +pub async fn list_presets_handler( + State(state): State, + Extension(user): Extension, +) -> Result { + let presets = state.spool_preset_service.list_for_user(user.id).await?; + let response: Vec = presets.into_iter().map(Into::into).collect(); + Ok(Json(response)) +} + +pub async fn create_preset_handler( + State(state): State, + Extension(user): Extension, + Json(req): Json, +) -> Result { + req.validate()?; + let preset = state + .spool_preset_service + .create(user.id, req.name, req.spool_weight_g) + .await?; + Ok((StatusCode::CREATED, Json(SpoolPresetResponse::from(preset)))) +} + +pub async fn update_preset_handler( + State(state): State, + Extension(user): Extension, + Path(id): Path, + Json(req): Json, +) -> Result { + req.validate()?; + let preset = state + .spool_preset_service + .update(id, user.id, req.name, req.spool_weight_g) + .await?; + Ok(Json(SpoolPresetResponse::from(preset))) +} + +pub async fn delete_preset_handler( + State(state): State, + Extension(user): Extension, + Path(id): Path, +) -> Result { + state.spool_preset_service.delete(id, user.id).await?; + Ok(StatusCode::NO_CONTENT) +} diff --git a/backend/src/adapters/inbound/user_handler.rs b/backend/src/adapters/inbound/user_handler.rs new file mode 100644 index 0000000..d6af0da --- /dev/null +++ b/backend/src/adapters/inbound/user_handler.rs @@ -0,0 +1,72 @@ +use axum::{extract::State, response::IntoResponse, Extension, Json}; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use uuid::Uuid; +use validator::Validate; + +use crate::{ + adapters::inbound::middleware::auth::CurrentUser, + domain::user::User, + error::AppError, + router::AppState, +}; + +// ------------------------------------------------------------------------- +// Request / Response DTOs +// ------------------------------------------------------------------------- + +#[derive(Debug, Deserialize, Validate)] +pub struct UpdateMeRequest { + #[validate(email)] + pub email: Option, +} + +#[derive(Debug, Serialize)] +pub struct UserResponse { + pub id: Uuid, + pub email: String, + pub email_verified: bool, + pub auth_provider: String, + pub created_at: OffsetDateTime, +} + +impl From for UserResponse { + fn from(u: User) -> Self { + let auth_provider = format!("{:?}", u.auth_provider()).to_lowercase(); + Self { + id: u.id, + email: u.email, + email_verified: u.email_verified, + auth_provider, + created_at: u.created_at, + } + } +} + +// ------------------------------------------------------------------------- +// Handlers +// ------------------------------------------------------------------------- + +pub async fn get_me_handler( + State(state): State, + Extension(current_user): Extension, +) -> Result { + let user = state + .auth_service + .user_repo_ref() + .find_by_id(current_user.id) + .await? + .ok_or(AppError::NotFound)?; + + Ok(Json(UserResponse::from(user))) +} + +pub async fn update_me_handler( + State(_state): State, + Extension(_current_user): Extension, + Json(req): Json, +) -> Result { + req.validate()?; + // TODO: implementar atualização de perfil + Ok(axum::http::StatusCode::OK) +} diff --git a/backend/src/adapters/mod.rs b/backend/src/adapters/mod.rs new file mode 100644 index 0000000..5756441 --- /dev/null +++ b/backend/src/adapters/mod.rs @@ -0,0 +1,2 @@ +pub mod inbound; +pub mod outbound; diff --git a/backend/src/adapters/outbound/mod.rs b/backend/src/adapters/outbound/mod.rs new file mode 100644 index 0000000..8907963 --- /dev/null +++ b/backend/src/adapters/outbound/mod.rs @@ -0,0 +1,7 @@ +pub mod postgres_filament_repo; +pub mod postgres_spool_preset_repo; +pub mod postgres_user_repo; + +pub use postgres_filament_repo::PostgresFilamentRepository; +pub use postgres_spool_preset_repo::PostgresSpoolPresetRepository; +pub use postgres_user_repo::PostgresUserRepository; diff --git a/backend/src/adapters/outbound/postgres_filament_repo.rs b/backend/src/adapters/outbound/postgres_filament_repo.rs new file mode 100644 index 0000000..3a919e0 --- /dev/null +++ b/backend/src/adapters/outbound/postgres_filament_repo.rs @@ -0,0 +1,215 @@ +use async_trait::async_trait; +use sqlx::PgPool; +use std::sync::Arc; +use uuid::Uuid; + +use crate::{ + domain::filament::{Filament, Material}, + error::AppError, + ports::{FilamentFilter, FilamentRepository}, +}; + +pub struct PostgresFilamentRepository { + db: Arc, +} + +impl PostgresFilamentRepository { + pub fn new(db: Arc) -> Self { + Self { db } + } +} + +#[async_trait] +impl FilamentRepository for PostgresFilamentRepository { + async fn find_by_id(&self, id: Uuid, user_id: Uuid) -> Result, AppError> { + let row = sqlx::query_as::<_, FilamentRow>( + r#"SELECT id, user_id, material, brand, model, color_hex, spool_preset_id, + total_weight_g, net_weight_g, temp_hotend_c, temp_bed_c, + flow_factor_pct, notes, updated_at, created_at + FROM filaments + WHERE id = $1 AND user_id = $2"#, + ) + .bind(id) + .bind(user_id) + .fetch_optional(self.db.as_ref()) + .await?; + + Ok(row.map(Filament::from)) + } + + async fn list(&self, user_id: Uuid, filter: FilamentFilter) -> Result, AppError> { + let rows = sqlx::query_as::<_, FilamentRow>( + r#"SELECT id, user_id, material, brand, model, color_hex, spool_preset_id, + total_weight_g, net_weight_g, temp_hotend_c, temp_bed_c, + flow_factor_pct, notes, updated_at, created_at + FROM filaments + WHERE user_id = $1 + AND ($2::text IS NULL OR material ILIKE $2) + AND ($3::text IS NULL OR brand ILIKE '%' || $3 || '%') + AND ($4::text IS NULL OR ( + brand ILIKE '%' || $4 || '%' + OR model ILIKE '%' || $4 || '%' + OR notes ILIKE '%' || $4 || '%' + )) + AND ($5::text IS NULL OR ( + ($5 = 'low' AND net_weight_g::float / 1000.0 * 100 <= 15) OR + ($5 = 'medium' AND net_weight_g::float / 1000.0 * 100 <= 35) OR + ($5 = 'ok' AND net_weight_g::float / 1000.0 * 100 > 35) + )) + ORDER BY created_at DESC + LIMIT $6 OFFSET $7"#, + ) + .bind(user_id) + .bind(&filter.material) + .bind(&filter.brand) + .bind(&filter.search) + .bind(&filter.stock_level) + .bind(filter.per_page as i64) + .bind(((filter.page.saturating_sub(1)) * filter.per_page) as i64) + .fetch_all(self.db.as_ref()) + .await?; + + Ok(rows.into_iter().map(Filament::from).collect()) + } + + async fn count(&self, user_id: Uuid, filter: &FilamentFilter) -> Result { + let row: (i64,) = sqlx::query_as( + r#"SELECT COUNT(*) FROM filaments + WHERE user_id = $1 + AND ($2::text IS NULL OR material ILIKE $2) + AND ($3::text IS NULL OR brand ILIKE '%' || $3 || '%') + AND ($4::text IS NULL OR ( + brand ILIKE '%' || $4 || '%' + OR model ILIKE '%' || $4 || '%' + OR notes ILIKE '%' || $4 || '%' + ))"#, + ) + .bind(user_id) + .bind(&filter.material) + .bind(&filter.brand) + .bind(&filter.search) + .fetch_one(self.db.as_ref()) + .await?; + + Ok(row.0) + } + + async fn create(&self, filament: &Filament) -> Result { + let row = sqlx::query_as::<_, FilamentRow>( + r#"INSERT INTO filaments ( + id, user_id, material, brand, model, color_hex, spool_preset_id, + total_weight_g, net_weight_g, temp_hotend_c, temp_bed_c, + flow_factor_pct, notes, updated_at, created_at + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) + RETURNING id, user_id, material, brand, model, color_hex, spool_preset_id, + total_weight_g, net_weight_g, temp_hotend_c, temp_bed_c, + flow_factor_pct, notes, updated_at, created_at"#, + ) + .bind(filament.id) + .bind(filament.user_id) + .bind(filament.material.to_string()) + .bind(&filament.brand) + .bind(&filament.model) + .bind(&filament.color_hex) + .bind(filament.spool_preset_id) + .bind(filament.total_weight_g) + .bind(filament.net_weight_g) + .bind(filament.temp_hotend_c) + .bind(filament.temp_bed_c) + .bind(filament.flow_factor_pct) + .bind(&filament.notes) + .bind(filament.updated_at) + .bind(filament.created_at) + .fetch_one(self.db.as_ref()) + .await?; + + Ok(Filament::from(row)) + } + + async fn update(&self, filament: &Filament) -> Result { + let row = sqlx::query_as::<_, FilamentRow>( + r#"UPDATE filaments SET + material = $3, brand = $4, model = $5, color_hex = $6, + spool_preset_id = $7, total_weight_g = $8, net_weight_g = $9, + temp_hotend_c = $10, temp_bed_c = $11, flow_factor_pct = $12, + notes = $13, updated_at = $14 + WHERE id = $1 AND user_id = $2 + RETURNING id, user_id, material, brand, model, color_hex, spool_preset_id, + total_weight_g, net_weight_g, temp_hotend_c, temp_bed_c, + flow_factor_pct, notes, updated_at, created_at"#, + ) + .bind(filament.id) + .bind(filament.user_id) + .bind(filament.material.to_string()) + .bind(&filament.brand) + .bind(&filament.model) + .bind(&filament.color_hex) + .bind(filament.spool_preset_id) + .bind(filament.total_weight_g) + .bind(filament.net_weight_g) + .bind(filament.temp_hotend_c) + .bind(filament.temp_bed_c) + .bind(filament.flow_factor_pct) + .bind(&filament.notes) + .bind(filament.updated_at) + .fetch_one(self.db.as_ref()) + .await?; + + Ok(Filament::from(row)) + } + + async fn delete(&self, id: Uuid, user_id: Uuid) -> Result<(), AppError> { + sqlx::query("DELETE FROM filaments WHERE id = $1 AND user_id = $2") + .bind(id) + .bind(user_id) + .execute(self.db.as_ref()) + .await?; + + Ok(()) + } +} + +// ------------------------------------------------------------------------- +// Mapeamento linha do banco → entidade de domínio +// ------------------------------------------------------------------------- + +#[derive(sqlx::FromRow)] +struct FilamentRow { + id: Uuid, + user_id: Uuid, + material: String, + brand: String, + model: Option, + color_hex: String, + spool_preset_id: Uuid, + total_weight_g: i32, + net_weight_g: i32, + temp_hotend_c: Option, + temp_bed_c: Option, + flow_factor_pct: Option, + notes: Option, + updated_at: time::OffsetDateTime, + created_at: time::OffsetDateTime, +} + +impl From for Filament { + fn from(row: FilamentRow) -> Self { + Self { + id: row.id, + user_id: row.user_id, + material: Material::from(row.material), + brand: row.brand, + model: row.model, + color_hex: row.color_hex, + spool_preset_id: row.spool_preset_id, + total_weight_g: row.total_weight_g, + net_weight_g: row.net_weight_g, + temp_hotend_c: row.temp_hotend_c, + temp_bed_c: row.temp_bed_c, + flow_factor_pct: row.flow_factor_pct, + notes: row.notes, + updated_at: row.updated_at, + created_at: row.created_at, + } + } +} diff --git a/backend/src/adapters/outbound/postgres_spool_preset_repo.rs b/backend/src/adapters/outbound/postgres_spool_preset_repo.rs new file mode 100644 index 0000000..945223e --- /dev/null +++ b/backend/src/adapters/outbound/postgres_spool_preset_repo.rs @@ -0,0 +1,122 @@ +use async_trait::async_trait; +use sqlx::PgPool; +use std::sync::Arc; +use uuid::Uuid; + +use crate::{ + domain::spool_preset::SpoolPreset, + error::AppError, + ports::SpoolPresetRepository, +}; + +pub struct PostgresSpoolPresetRepository { + db: Arc, +} + +impl PostgresSpoolPresetRepository { + pub fn new(db: Arc) -> Self { + Self { db } + } +} + +#[async_trait] +impl SpoolPresetRepository for PostgresSpoolPresetRepository { + async fn list_for_user(&self, user_id: Uuid) -> Result, AppError> { + let rows = sqlx::query_as::<_, SpoolPresetRow>( + r#"SELECT id, name, spool_weight_g, is_system, user_id, created_at + FROM spool_presets + WHERE is_system = true OR user_id = $1 + ORDER BY is_system DESC, name ASC"#, + ) + .bind(user_id) + .fetch_all(self.db.as_ref()) + .await?; + + Ok(rows.into_iter().map(SpoolPreset::from).collect()) + } + + async fn find_by_id(&self, id: Uuid) -> Result, AppError> { + let row = sqlx::query_as::<_, SpoolPresetRow>( + r#"SELECT id, name, spool_weight_g, is_system, user_id, created_at + FROM spool_presets WHERE id = $1"#, + ) + .bind(id) + .fetch_optional(self.db.as_ref()) + .await?; + + Ok(row.map(SpoolPreset::from)) + } + + async fn create(&self, preset: &SpoolPreset) -> Result { + let row = sqlx::query_as::<_, SpoolPresetRow>( + r#"INSERT INTO spool_presets (id, name, spool_weight_g, is_system, user_id, created_at) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id, name, spool_weight_g, is_system, user_id, created_at"#, + ) + .bind(preset.id) + .bind(&preset.name) + .bind(preset.spool_weight_g) + .bind(preset.is_system) + .bind(preset.user_id) + .bind(preset.created_at) + .fetch_one(self.db.as_ref()) + .await?; + + Ok(SpoolPreset::from(row)) + } + + async fn update(&self, preset: &SpoolPreset) -> Result { + let row = sqlx::query_as::<_, SpoolPresetRow>( + r#"UPDATE spool_presets + SET name = $2, spool_weight_g = $3 + WHERE id = $1 AND is_system = false + RETURNING id, name, spool_weight_g, is_system, user_id, created_at"#, + ) + .bind(preset.id) + .bind(&preset.name) + .bind(preset.spool_weight_g) + .fetch_one(self.db.as_ref()) + .await?; + + Ok(SpoolPreset::from(row)) + } + + async fn delete(&self, id: Uuid, user_id: Uuid) -> Result<(), AppError> { + sqlx::query( + "DELETE FROM spool_presets WHERE id = $1 AND user_id = $2 AND is_system = false", + ) + .bind(id) + .bind(user_id) + .execute(self.db.as_ref()) + .await?; + + Ok(()) + } +} + +// ------------------------------------------------------------------------- +// Mapeamento linha do banco → entidade de domínio +// ------------------------------------------------------------------------- + +#[derive(sqlx::FromRow)] +struct SpoolPresetRow { + id: Uuid, + name: String, + spool_weight_g: i32, + is_system: bool, + user_id: Option, + created_at: time::OffsetDateTime, +} + +impl From for SpoolPreset { + fn from(row: SpoolPresetRow) -> Self { + Self { + id: row.id, + name: row.name, + spool_weight_g: row.spool_weight_g, + is_system: row.is_system, + user_id: row.user_id, + created_at: row.created_at, + } + } +} diff --git a/backend/src/adapters/outbound/postgres_user_repo.rs b/backend/src/adapters/outbound/postgres_user_repo.rs new file mode 100644 index 0000000..526e31e --- /dev/null +++ b/backend/src/adapters/outbound/postgres_user_repo.rs @@ -0,0 +1,132 @@ +use async_trait::async_trait; +use sqlx::PgPool; +use std::sync::Arc; +use uuid::Uuid; + +use crate::{domain::user::User, error::AppError, ports::UserRepository}; + +pub struct PostgresUserRepository { + db: Arc, +} + +impl PostgresUserRepository { + pub fn new(db: Arc) -> Self { + Self { db } + } +} + +#[async_trait] +impl UserRepository for PostgresUserRepository { + async fn find_by_id(&self, id: Uuid) -> Result, AppError> { + let row = sqlx::query_as::<_, UserRow>( + r#"SELECT id, email, password_hash, google_id, email_verified, created_at, updated_at + FROM users WHERE id = $1"#, + ) + .bind(id) + .fetch_optional(self.db.as_ref()) + .await?; + + Ok(row.map(User::from)) + } + + async fn find_by_email(&self, email: &str) -> Result, AppError> { + let row = sqlx::query_as::<_, UserRow>( + r#"SELECT id, email, password_hash, google_id, email_verified, created_at, updated_at + FROM users WHERE email = $1"#, + ) + .bind(email) + .fetch_optional(self.db.as_ref()) + .await?; + + Ok(row.map(User::from)) + } + + async fn find_by_google_id(&self, google_id: &str) -> Result, AppError> { + let row = sqlx::query_as::<_, UserRow>( + r#"SELECT id, email, password_hash, google_id, email_verified, created_at, updated_at + FROM users WHERE google_id = $1"#, + ) + .bind(google_id) + .fetch_optional(self.db.as_ref()) + .await?; + + Ok(row.map(User::from)) + } + + async fn create(&self, user: &User) -> Result { + let row = sqlx::query_as::<_, UserRow>( + r#"INSERT INTO users (id, email, password_hash, google_id, email_verified, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id, email, password_hash, google_id, email_verified, created_at, updated_at"#, + ) + .bind(user.id) + .bind(&user.email) + .bind(&user.password_hash) + .bind(&user.google_id) + .bind(user.email_verified) + .bind(user.created_at) + .bind(user.updated_at) + .fetch_one(self.db.as_ref()) + .await?; + + Ok(User::from(row)) + } + + async fn update(&self, user: &User) -> Result { + let row = sqlx::query_as::<_, UserRow>( + r#"UPDATE users + SET email = $2, password_hash = $3, google_id = $4, + email_verified = $5, updated_at = $6 + WHERE id = $1 + RETURNING id, email, password_hash, google_id, email_verified, created_at, updated_at"#, + ) + .bind(user.id) + .bind(&user.email) + .bind(&user.password_hash) + .bind(&user.google_id) + .bind(user.email_verified) + .bind(user.updated_at) + .fetch_one(self.db.as_ref()) + .await?; + + Ok(User::from(row)) + } + + async fn verify_email(&self, id: Uuid) -> Result<(), AppError> { + sqlx::query("UPDATE users SET email_verified = true, updated_at = NOW() WHERE id = $1") + .bind(id) + .execute(self.db.as_ref()) + .await?; + + Ok(()) + } +} + +// ------------------------------------------------------------------------- +// Mapeamento da linha do banco para entidade de domínio +// ------------------------------------------------------------------------- + +#[derive(sqlx::FromRow)] +struct UserRow { + id: Uuid, + email: String, + password_hash: Option, + google_id: Option, + email_verified: bool, + created_at: time::OffsetDateTime, + updated_at: time::OffsetDateTime, +} + +impl From for User { + fn from(row: UserRow) -> Self { + Self { + id: row.id, + email: row.email, + password_hash: row.password_hash, + google_id: row.google_id, + email_verified: row.email_verified, + created_at: row.created_at, + updated_at: row.updated_at, + } + } +} diff --git a/backend/src/application/auth_service.rs b/backend/src/application/auth_service.rs new file mode 100644 index 0000000..b535ea3 --- /dev/null +++ b/backend/src/application/auth_service.rs @@ -0,0 +1,210 @@ +use std::sync::Arc; + +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Argon2, +}; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::{ + config::Config, + domain::user::User, + error::AppError, + ports::UserRepository, +}; + +/// Claims do JWT de acesso. +#[derive(Debug, Serialize, Deserialize)] +pub struct AccessClaims { + pub sub: Uuid, + pub email: String, + pub exp: i64, + pub iat: i64, +} + +/// Claims do JWT de refresh. +#[derive(Debug, Serialize, Deserialize)] +pub struct RefreshClaims { + pub sub: Uuid, + pub exp: i64, + pub iat: i64, +} + +/// Par de tokens emitido após login/registro bem-sucedido. +#[derive(Debug)] +pub struct TokenPair { + pub access_token: String, + pub refresh_token: String, +} + +/// Caso de uso de autenticação: registro, login, OAuth, refresh e logout. +pub struct AuthService { + user_repo: Arc, + config: Config, +} + +impl AuthService { + pub fn new(user_repo: Arc, config: Config) -> Self { + Self { user_repo, config } + } + + /// Expõe o repositório de usuários para uso em outros handlers (ex: user_handler). + pub fn user_repo_ref(&self) -> &dyn UserRepository { + self.user_repo.as_ref() + } + + // ------------------------------------------------------------------------- + // Registro com email/senha + // ------------------------------------------------------------------------- + + pub async fn register(&self, email: String, password: String) -> Result { + if let Some(_) = self.user_repo.find_by_email(&email).await? { + return Err(AppError::Conflict("email already registered".into())); + } + + let password_hash = hash_password(&password)?; + let user = User::new_with_password(email, password_hash); + let user = self.user_repo.create(&user).await?; + + self.issue_token_pair(&user) + } + + // ------------------------------------------------------------------------- + // Login com email/senha + // ------------------------------------------------------------------------- + + pub async fn login(&self, email: String, password: String) -> Result { + let user = self + .user_repo + .find_by_email(&email) + .await? + .ok_or(AppError::Unauthorized)?; + + let hash = user.password_hash.as_deref().ok_or(AppError::Unauthorized)?; + verify_password(&password, hash)?; + + self.issue_token_pair(&user) + } + + // ------------------------------------------------------------------------- + // OAuth Google + // ------------------------------------------------------------------------- + + pub async fn google_oauth(&self, google_id: String, email: String) -> Result { + let user = match self.user_repo.find_by_google_id(&google_id).await? { + Some(u) => u, + None => { + // Verificar se o email já existe (conta pode ter sido criada com email/senha) + match self.user_repo.find_by_email(&email).await? { + Some(mut u) => { + u.google_id = Some(google_id); + u.email_verified = true; + self.user_repo.update(&u).await? + } + None => { + let new_user = User::new_with_google(email, google_id); + self.user_repo.create(&new_user).await? + } + } + } + }; + + self.issue_token_pair(&user) + } + + // ------------------------------------------------------------------------- + // Refresh de token + // ------------------------------------------------------------------------- + + pub async fn refresh_token(&self, refresh_token: &str) -> Result { + let claims = self.decode_refresh_token(refresh_token)?; + let user = self + .user_repo + .find_by_id(claims.sub) + .await? + .ok_or(AppError::Unauthorized)?; + + self.issue_token_pair(&user) + } + + // ------------------------------------------------------------------------- + // Validação de access token (usado pelo middleware) + // ------------------------------------------------------------------------- + + pub fn validate_access_token(&self, token: &str) -> Result { + decode::( + token, + &DecodingKey::from_secret(self.config.jwt_secret.as_bytes()), + &Validation::default(), + ) + .map(|data| data.claims) + .map_err(|_| AppError::Unauthorized) + } + + // ------------------------------------------------------------------------- + // Helpers privados + // ------------------------------------------------------------------------- + + fn issue_token_pair(&self, user: &User) -> Result { + let now = OffsetDateTime::now_utc().unix_timestamp(); + let access_exp = now + self.config.jwt_expiry_secs as i64; + let refresh_exp = now + self.config.jwt_refresh_expiry_secs as i64; + + let access_claims = AccessClaims { + sub: user.id, + email: user.email.clone(), + exp: access_exp, + iat: now, + }; + + let refresh_claims = RefreshClaims { + sub: user.id, + exp: refresh_exp, + iat: now, + }; + + let key = EncodingKey::from_secret(self.config.jwt_secret.as_bytes()); + + let access_token = encode(&Header::default(), &access_claims, &key) + .map_err(|e| AppError::Internal(e.into()))?; + + let refresh_token = encode(&Header::default(), &refresh_claims, &key) + .map_err(|e| AppError::Internal(e.into()))?; + + Ok(TokenPair { access_token, refresh_token }) + } + + fn decode_refresh_token(&self, token: &str) -> Result { + decode::( + token, + &DecodingKey::from_secret(self.config.jwt_secret.as_bytes()), + &Validation::default(), + ) + .map(|data| data.claims) + .map_err(|_| AppError::Unauthorized) + } +} + +// ------------------------------------------------------------------------- +// Funções utilitárias de hash (fora do impl para facilitar testes) +// ------------------------------------------------------------------------- + +pub fn hash_password(password: &str) -> Result { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + argon2 + .hash_password(password.as_bytes(), &salt) + .map(|h| h.to_string()) + .map_err(|e| AppError::Internal(anyhow::anyhow!("Password hashing failed: {e}"))) +} + +pub fn verify_password(password: &str, hash: &str) -> Result<(), AppError> { + let parsed_hash = PasswordHash::new(hash) + .map_err(|e| AppError::Internal(anyhow::anyhow!("Invalid password hash: {e}")))?; + Argon2::default() + .verify_password(password.as_bytes(), &parsed_hash) + .map_err(|_| AppError::Unauthorized) +} diff --git a/backend/src/application/filament_service.rs b/backend/src/application/filament_service.rs new file mode 100644 index 0000000..9941006 --- /dev/null +++ b/backend/src/application/filament_service.rs @@ -0,0 +1,316 @@ +use std::sync::Arc; + +use base64::{engine::general_purpose::STANDARD, Engine}; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::{ + domain::filament::{Filament, Material}, + error::AppError, + ports::{FilamentFilter, FilamentRepository, SpoolPresetRepository}, +}; + +/// DTO de entrada para criação de filamento. +#[derive(Debug)] +pub struct CreateFilamentInput { + pub material: String, + pub brand: String, + pub model: Option, + pub color_hex: String, + pub spool_preset_id: Uuid, + pub total_weight_g: i32, + pub temp_hotend_c: Option, + pub temp_bed_c: Option, + pub flow_factor_pct: Option, + pub notes: Option, +} + +/// DTO de entrada para atualização de filamento. +#[derive(Debug)] +pub struct UpdateFilamentInput { + pub material: String, + pub brand: String, + pub model: Option, + pub color_hex: String, + pub spool_preset_id: Uuid, + pub total_weight_g: i32, + pub temp_hotend_c: Option, + pub temp_bed_c: Option, + pub flow_factor_pct: Option, + pub notes: Option, + /// Timestamp do cliente para resolução de conflito offline-first + pub client_updated_at: Option, +} + +/// Estatísticas do dashboard. +#[derive(Debug)] +pub struct DashboardStats { + pub total_stock_kg: f64, + pub low_stock_count: i64, + pub by_material: Vec, + pub low_stock_filaments: Vec, + pub recent_filaments: Vec, +} + +#[derive(Debug)] +pub struct MaterialSummary { + pub material: String, + pub count: i64, + pub total_kg: f64, +} + +/// Casos de uso relacionados ao inventário de filamentos. +pub struct FilamentService { + repo: Arc, + preset_repo: Arc, +} + +impl FilamentService { + pub fn new(repo: Arc, preset_repo: Arc) -> Self { + Self { repo, preset_repo } + } + + // ------------------------------------------------------------------------- + // CRUD + // ------------------------------------------------------------------------- + + pub async fn create(&self, user_id: Uuid, input: CreateFilamentInput) -> Result { + let preset = self + .preset_repo + .find_by_id(input.spool_preset_id) + .await? + .ok_or(AppError::NotFound)?; + + let net_weight_g = (input.total_weight_g - preset.spool_weight_g).max(0); + let now = OffsetDateTime::now_utc(); + + let filament = Filament { + id: Uuid::new_v4(), + user_id, + material: Material::from(input.material), + brand: input.brand, + model: input.model, + color_hex: input.color_hex, + spool_preset_id: input.spool_preset_id, + total_weight_g: input.total_weight_g, + net_weight_g, + temp_hotend_c: input.temp_hotend_c, + temp_bed_c: input.temp_bed_c, + flow_factor_pct: input.flow_factor_pct, + notes: input.notes, + updated_at: now, + created_at: now, + }; + + self.repo.create(&filament).await + } + + pub async fn get(&self, id: Uuid, user_id: Uuid) -> Result { + self.repo + .find_by_id(id, user_id) + .await? + .ok_or(AppError::NotFound) + } + + pub async fn list( + &self, + user_id: Uuid, + filter: FilamentFilter, + ) -> Result, AppError> { + self.repo.list(user_id, filter).await + } + + pub async fn update( + &self, + id: Uuid, + user_id: Uuid, + input: UpdateFilamentInput, + ) -> Result { + let mut existing = self + .repo + .find_by_id(id, user_id) + .await? + .ok_or(AppError::NotFound)?; + + // Resolução de conflito offline-first: rejeita se servidor é mais recente + if let Some(client_ts) = input.client_updated_at { + if existing.updated_at > client_ts { + return Err(AppError::Conflict( + "server version is newer than client version".into(), + )); + } + } + + let preset = self + .preset_repo + .find_by_id(input.spool_preset_id) + .await? + .ok_or(AppError::NotFound)?; + + let net_weight_g = (input.total_weight_g - preset.spool_weight_g).max(0); + + existing.material = Material::from(input.material); + existing.brand = input.brand; + existing.model = input.model; + existing.color_hex = input.color_hex; + existing.spool_preset_id = input.spool_preset_id; + existing.total_weight_g = input.total_weight_g; + existing.net_weight_g = net_weight_g; + existing.temp_hotend_c = input.temp_hotend_c; + existing.temp_bed_c = input.temp_bed_c; + existing.flow_factor_pct = input.flow_factor_pct; + existing.notes = input.notes; + existing.updated_at = OffsetDateTime::now_utc(); + + self.repo.update(&existing).await + } + + pub async fn delete(&self, id: Uuid, user_id: Uuid) -> Result<(), AppError> { + self.repo + .find_by_id(id, user_id) + .await? + .ok_or(AppError::NotFound)?; + + self.repo.delete(id, user_id).await + } + + // ------------------------------------------------------------------------- + // Dashboard + // ------------------------------------------------------------------------- + + pub async fn dashboard(&self, user_id: Uuid) -> Result { + let all = self.repo.list(user_id, FilamentFilter::default()).await?; + + let total_stock_kg = all.iter().map(|f| f.net_weight_g as f64).sum::() / 1000.0; + + let low_stock_count = all.iter().filter(|f| f.is_low_stock()).count() as i64; + + // Agrupamento por material + let mut by_material: std::collections::HashMap = Default::default(); + for f in &all { + let entry = by_material.entry(f.material.to_string()).or_default(); + entry.0 += 1; + entry.1 += f.net_weight_g as f64 / 1000.0; + } + let mut by_material: Vec = by_material + .into_iter() + .map(|(material, (count, total_kg))| MaterialSummary { + material, + count, + total_kg, + }) + .collect(); + by_material.sort_by(|a, b| b.count.cmp(&a.count)); + + let low_stock_filaments: Vec = all + .iter() + .filter(|f| f.is_low_stock()) + .cloned() + .collect(); + + let mut recent = all.clone(); + recent.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + let recent_filaments = recent.into_iter().take(5).collect(); + + Ok(DashboardStats { + total_stock_kg, + low_stock_count, + by_material, + low_stock_filaments, + recent_filaments, + }) + } + + // ------------------------------------------------------------------------- + // QR Code + // ------------------------------------------------------------------------- + + pub async fn generate_qrcode_png(&self, id: Uuid, user_id: Uuid) -> Result, AppError> { + self.get(id, user_id).await?; + + let deep_link = format!("meowspool://filaments/{id}"); + let code = qrcode::QrCode::new(deep_link.as_bytes()) + .map_err(|e| AppError::Internal(anyhow::anyhow!("QR Code generation failed: {e}")))?; + + let image = code + .render::>() + .min_dimensions(200, 200) + .build(); + + let mut buf = Vec::new(); + image::DynamicImage::ImageLuma8(image) + .write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png) + .map_err(|e| AppError::Internal(anyhow::anyhow!("PNG encoding failed: {e}")))?; + + Ok(buf) + } + + // ------------------------------------------------------------------------- + // SVG Label + // ------------------------------------------------------------------------- + + pub async fn generate_label_svg( + &self, + id: Uuid, + user_id: Uuid, + width_mm: u32, + height_mm: u32, + ) -> Result { + let filament = self.get(id, user_id).await?; + + // Gerar QR Code como PNG base64 para embutir no SVG + let qr_png = self.generate_qrcode_png(id, user_id).await?; + let qr_b64 = STANDARD.encode(&qr_png); + + // Escala: 1mm = ~3.78px (96dpi) + let px_per_mm = 3.78_f64; + let width_px = (width_mm as f64 * px_per_mm) as u32; + let height_px = (height_mm as f64 * px_per_mm) as u32; + let qr_size = (height_px as f64 * 0.8) as u32; + + let model_name = filament.model.as_deref().unwrap_or("—"); + let net_weight = filament.net_weight_g; + let material = filament.material.to_string(); + let brand = &filament.brand; + let color = &filament.color_hex; + + let svg = format!( + r##" + + + + + + {model_name} + {brand} · {material} + {net_weight}g + + +"##, + width_px = width_px, + height_px = height_px, + h = height_px - 12, + color = color, + model_name = escape_xml(model_name), + brand = escape_xml(brand), + material = escape_xml(&material), + net_weight = net_weight, + qr_size = qr_size, + qr_x = width_px - qr_size - 4, + qr_y = (height_px - qr_size) / 2, + qr_b64 = qr_b64, + ); + + Ok(svg) + } +} + +fn escape_xml(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} diff --git a/backend/src/application/mod.rs b/backend/src/application/mod.rs new file mode 100644 index 0000000..b3e2352 --- /dev/null +++ b/backend/src/application/mod.rs @@ -0,0 +1,7 @@ +pub mod auth_service; +pub mod filament_service; +pub mod spool_preset_service; + +pub use auth_service::AuthService; +pub use filament_service::FilamentService; +pub use spool_preset_service::SpoolPresetService; diff --git a/backend/src/application/spool_preset_service.rs b/backend/src/application/spool_preset_service.rs new file mode 100644 index 0000000..4bf996a --- /dev/null +++ b/backend/src/application/spool_preset_service.rs @@ -0,0 +1,72 @@ +use std::sync::Arc; + +use uuid::Uuid; + +use crate::{ + domain::spool_preset::SpoolPreset, + error::AppError, + ports::SpoolPresetRepository, +}; + +/// Casos de uso para gerenciamento de presets de carretel. +pub struct SpoolPresetService { + repo: Arc, +} + +impl SpoolPresetService { + pub fn new(repo: Arc) -> Self { + Self { repo } + } + + /// Retorna presets do sistema + presets customizados do usuário. + pub async fn list_for_user(&self, user_id: Uuid) -> Result, AppError> { + self.repo.list_for_user(user_id).await + } + + pub async fn create( + &self, + user_id: Uuid, + name: String, + spool_weight_g: i32, + ) -> Result { + let preset = SpoolPreset::new_custom(name, spool_weight_g, user_id); + self.repo.create(&preset).await + } + + pub async fn update( + &self, + id: Uuid, + user_id: Uuid, + name: String, + spool_weight_g: i32, + ) -> Result { + let mut preset = self + .repo + .find_by_id(id) + .await? + .ok_or(AppError::NotFound)?; + + if !preset.is_editable_by(user_id) { + return Err(AppError::Forbidden); + } + + preset.name = name; + preset.spool_weight_g = spool_weight_g; + + self.repo.update(&preset).await + } + + pub async fn delete(&self, id: Uuid, user_id: Uuid) -> Result<(), AppError> { + let preset = self + .repo + .find_by_id(id) + .await? + .ok_or(AppError::NotFound)?; + + if !preset.is_editable_by(user_id) { + return Err(AppError::Forbidden); + } + + self.repo.delete(id, user_id).await + } +} diff --git a/backend/src/config.rs b/backend/src/config.rs new file mode 100644 index 0000000..cec7032 --- /dev/null +++ b/backend/src/config.rs @@ -0,0 +1,72 @@ +use anyhow::Context; + +/// Configuração central da aplicação, lida de variáveis de ambiente. +#[derive(Debug, Clone)] +pub struct Config { + pub database_url: String, + pub jwt_secret: String, + pub jwt_expiry_secs: u64, + pub jwt_refresh_expiry_secs: u64, + pub google_client_id: String, + pub google_client_secret: String, + pub host: String, + pub port: u16, + pub app_env: AppEnv, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum AppEnv { + Development, + Production, +} + +impl Config { + pub fn from_env() -> anyhow::Result { + Ok(Self { + database_url: required("DATABASE_URL")?, + jwt_secret: required("JWT_SECRET")?, + jwt_expiry_secs: optional("JWT_EXPIRY_SECS", 3600)?, + jwt_refresh_expiry_secs: optional("JWT_REFRESH_EXPIRY_SECS", 2_592_000)?, + google_client_id: optional_string("GOOGLE_CLIENT_ID"), + google_client_secret: optional_string("GOOGLE_CLIENT_SECRET"), + host: optional_string_default("HOST", "0.0.0.0"), + port: optional("PORT", 8080)?, + app_env: parse_app_env(), + }) + } + + pub fn is_production(&self) -> bool { + self.app_env == AppEnv::Production + } +} + +fn required(key: &str) -> anyhow::Result { + std::env::var(key).with_context(|| format!("Missing required env var: {key}")) +} + +fn optional(key: &str, default: T) -> anyhow::Result +where + T::Err: std::fmt::Debug, +{ + match std::env::var(key) { + Ok(val) => val + .parse::() + .map_err(|e| anyhow::anyhow!("Invalid value for {key}: {e:?}")), + Err(_) => Ok(default), + } +} + +fn optional_string(key: &str) -> String { + std::env::var(key).unwrap_or_default() +} + +fn optional_string_default(key: &str, default: &str) -> String { + std::env::var(key).unwrap_or_else(|_| default.to_string()) +} + +fn parse_app_env() -> AppEnv { + match std::env::var("APP_ENV").as_deref() { + Ok("production") => AppEnv::Production, + _ => AppEnv::Development, + } +} diff --git a/backend/src/domain/filament.rs b/backend/src/domain/filament.rs new file mode 100644 index 0000000..a820e61 --- /dev/null +++ b/backend/src/domain/filament.rs @@ -0,0 +1,99 @@ +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use uuid::Uuid; + +/// Entidade de domínio que representa um carretel de filamento no inventário. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Filament { + pub id: Uuid, + pub user_id: Uuid, + pub material: Material, + pub brand: String, + pub model: Option, + /// Cor em formato hex, ex: "#FF5733" + pub color_hex: String, + pub spool_preset_id: Uuid, + /// Peso total medido na balança (gramas) + pub total_weight_g: i32, + /// Calculado: total_weight_g - preset.spool_weight_g + pub net_weight_g: i32, + /// Temperatura do hotend em °C (opcional) + pub temp_hotend_c: Option, + /// Temperatura da mesa em °C (opcional) + pub temp_bed_c: Option, + /// Fator de fluxo/extrusão em % (opcional) + pub flow_factor_pct: Option, + pub notes: Option, + pub updated_at: OffsetDateTime, + pub created_at: OffsetDateTime, +} + +/// Tipos de material suportados pelo sistema. +/// Armazenado como TEXT no banco de dados. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Material { + #[serde(rename = "PLA")] + Pla, + #[serde(rename = "ABS")] + Abs, + #[serde(rename = "PETG")] + Petg, + #[serde(rename = "TPU")] + Tpu, + #[serde(rename = "ASA")] + Asa, + #[serde(rename = "PA")] + Pa, + #[serde(rename = "PC")] + Pc, + /// Outros materiais não listados acima + Other(String), +} + +impl std::fmt::Display for Material { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Pla => write!(f, "PLA"), + Self::Abs => write!(f, "ABS"), + Self::Petg => write!(f, "PETG"), + Self::Tpu => write!(f, "TPU"), + Self::Asa => write!(f, "ASA"), + Self::Pa => write!(f, "PA"), + Self::Pc => write!(f, "PC"), + Self::Other(s) => write!(f, "{s}"), + } + } +} + +impl From for Material { + fn from(s: String) -> Self { + match s.to_uppercase().as_str() { + "PLA" => Self::Pla, + "ABS" => Self::Abs, + "PETG" => Self::Petg, + "TPU" => Self::Tpu, + "ASA" => Self::Asa, + "PA" => Self::Pa, + "PC" => Self::Pc, + other => Self::Other(other.to_string()), + } + } +} + +impl Filament { + /// Calcula o percentual de filamento restante em relação ao peso nominal de 1kg. + /// Usado para alertas de estoque: <=15% = crítico, <=35% = baixo. + pub fn stock_percentage(&self) -> u8 { + let nominal_g = 1000_f64; + let pct = (self.net_weight_g as f64 / nominal_g * 100.0).round() as i32; + pct.clamp(0, 100) as u8 + } + + pub fn is_low_stock(&self) -> bool { + self.stock_percentage() <= 35 + } + + pub fn is_critical_stock(&self) -> bool { + self.stock_percentage() <= 15 + } +} diff --git a/backend/src/domain/mod.rs b/backend/src/domain/mod.rs new file mode 100644 index 0000000..a4e2825 --- /dev/null +++ b/backend/src/domain/mod.rs @@ -0,0 +1,3 @@ +pub mod filament; +pub mod spool_preset; +pub mod user; diff --git a/backend/src/domain/spool_preset.rs b/backend/src/domain/spool_preset.rs new file mode 100644 index 0000000..deef234 --- /dev/null +++ b/backend/src/domain/spool_preset.rs @@ -0,0 +1,48 @@ +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use uuid::Uuid; + +/// Entidade de domínio que representa um preset de carretel. +/// Presets do sistema têm `is_system = true` e `user_id = None`. +/// Presets customizados têm `is_system = false` e `user_id = Some(uuid)`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SpoolPreset { + pub id: Uuid, + pub name: String, + /// Peso do carretel vazio em gramas + pub spool_weight_g: i32, + /// true = preset built-in do sistema (somente leitura) + pub is_system: bool, + /// None para presets do sistema + pub user_id: Option, + pub created_at: OffsetDateTime, +} + +impl SpoolPreset { + pub fn new_system(name: String, spool_weight_g: i32) -> Self { + Self { + id: Uuid::new_v4(), + name, + spool_weight_g, + is_system: true, + user_id: None, + created_at: OffsetDateTime::now_utc(), + } + } + + pub fn new_custom(name: String, spool_weight_g: i32, user_id: Uuid) -> Self { + Self { + id: Uuid::new_v4(), + name, + spool_weight_g, + is_system: false, + user_id: Some(user_id), + created_at: OffsetDateTime::now_utc(), + } + } + + /// Retorna true se o usuário informado pode editar ou deletar este preset. + pub fn is_editable_by(&self, user_id: Uuid) -> bool { + !self.is_system && self.user_id == Some(user_id) + } +} diff --git a/backend/src/domain/user.rs b/backend/src/domain/user.rs new file mode 100644 index 0000000..448f926 --- /dev/null +++ b/backend/src/domain/user.rs @@ -0,0 +1,62 @@ +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use uuid::Uuid; + +/// Entidade de domínio que representa um usuário da plataforma. +/// Esta struct não tem dependências de infraestrutura — apenas tipos base. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct User { + pub id: Uuid, + pub email: String, + /// Hash bcrypt/argon2. None quando o usuário usa OAuth exclusivamente. + pub password_hash: Option, + /// ID do usuário no Google OAuth. None quando usa email/senha. + pub google_id: Option, + pub email_verified: bool, + pub created_at: OffsetDateTime, + pub updated_at: OffsetDateTime, +} + +/// Provedor de autenticação utilizado pelo usuário. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AuthProvider { + EmailPassword, + Google, +} + +impl User { + pub fn new_with_password(email: String, password_hash: String) -> Self { + let now = OffsetDateTime::now_utc(); + Self { + id: Uuid::new_v4(), + email, + password_hash: Some(password_hash), + google_id: None, + email_verified: false, + created_at: now, + updated_at: now, + } + } + + pub fn new_with_google(email: String, google_id: String) -> Self { + let now = OffsetDateTime::now_utc(); + Self { + id: Uuid::new_v4(), + email, + password_hash: None, + google_id: Some(google_id), + email_verified: true, + created_at: now, + updated_at: now, + } + } + + pub fn auth_provider(&self) -> AuthProvider { + if self.google_id.is_some() { + AuthProvider::Google + } else { + AuthProvider::EmailPassword + } + } +} diff --git a/backend/src/error.rs b/backend/src/error.rs new file mode 100644 index 0000000..dc6b288 --- /dev/null +++ b/backend/src/error.rs @@ -0,0 +1,78 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use serde_json::json; + +/// Erro centralizado da aplicação. +/// Toda camada retorna `Result`. +#[derive(Debug, thiserror::Error)] +pub enum AppError { + #[error("not found")] + NotFound, + + #[error("unauthorized")] + Unauthorized, + + #[error("forbidden")] + Forbidden, + + #[error("validation error: {0}")] + Validation(String), + + #[error("conflict: {0}")] + Conflict(String), + + #[error("unprocessable entity: {0}")] + UnprocessableEntity(String), + + #[error("internal error")] + Internal(#[from] anyhow::Error), +} + +impl AppError { + fn status_and_code(&self) -> (StatusCode, &'static str) { + match self { + Self::NotFound => (StatusCode::NOT_FOUND, "NOT_FOUND"), + Self::Unauthorized => (StatusCode::UNAUTHORIZED, "UNAUTHORIZED"), + Self::Forbidden => (StatusCode::FORBIDDEN, "FORBIDDEN"), + Self::Validation(_) => (StatusCode::BAD_REQUEST, "VALIDATION_ERROR"), + Self::Conflict(_) => (StatusCode::CONFLICT, "CONFLICT"), + Self::UnprocessableEntity(_) => (StatusCode::UNPROCESSABLE_ENTITY, "UNPROCESSABLE_ENTITY"), + Self::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR"), + } + } +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, code) = self.status_and_code(); + + if status == StatusCode::INTERNAL_SERVER_ERROR { + tracing::error!(error = %self, "Internal server error"); + } + + let body = Json(json!({ + "error": self.to_string(), + "code": code, + })); + + (status, body).into_response() + } +} + +impl From for AppError { + fn from(err: sqlx::Error) -> Self { + match err { + sqlx::Error::RowNotFound => Self::NotFound, + _ => Self::Internal(err.into()), + } + } +} + +impl From for AppError { + fn from(err: validator::ValidationErrors) -> Self { + Self::Validation(err.to_string()) + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs new file mode 100644 index 0000000..355fd29 --- /dev/null +++ b/backend/src/main.rs @@ -0,0 +1,43 @@ +use dotenvy::dotenv; +use sqlx::postgres::PgPoolOptions; +use std::net::SocketAddr; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + +mod adapters; +mod application; +mod config; +mod domain; +mod error; +mod ports; +mod router; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + dotenv().ok(); + + tracing_subscriber::registry() + .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| "meowspool=debug,tower_http=debug".into())) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let config = config::Config::from_env()?; + + tracing::info!("Connecting to database..."); + let db_pool = PgPoolOptions::new() + .max_connections(10) + .connect(&config.database_url) + .await?; + + tracing::info!("Running migrations..."); + sqlx::migrate!("./migrations").run(&db_pool).await?; + + let app = router::build(db_pool, config.clone()); + + let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?; + tracing::info!("Server listening on {}", addr); + + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app).await?; + + Ok(()) +} diff --git a/backend/src/ports/filament_repository.rs b/backend/src/ports/filament_repository.rs new file mode 100644 index 0000000..8e610ed --- /dev/null +++ b/backend/src/ports/filament_repository.rs @@ -0,0 +1,43 @@ +use async_trait::async_trait; +use uuid::Uuid; + +use crate::{domain::filament::Filament, error::AppError}; + +/// Filtros disponíveis para listagem de filamentos. +#[derive(Debug, Default, Clone)] +pub struct FilamentFilter { + pub material: Option, + pub brand: Option, + /// Busca full-text em marca, modelo e notas + pub search: Option, + /// "low" (<=15%), "medium" (<=35%), "ok" (>35%) + pub stock_level: Option, + pub sort: Option, + pub page: u32, + pub per_page: u32, +} + +#[derive(Debug, Clone)] +pub enum SortOrder { + NetWeightAsc, + NetWeightDesc, + CreatedAtDesc, +} + +/// Port (interface) para operações de persistência de filamentos. +/// Implementado pelo adapter `PostgresFilamentRepository`. +#[async_trait] +pub trait FilamentRepository: Send + Sync { + /// Busca um filamento pelo ID, garantindo que pertence ao user_id informado. + async fn find_by_id(&self, id: Uuid, user_id: Uuid) -> Result, AppError>; + + /// Lista filamentos do usuário com filtros e paginação. + async fn list(&self, user_id: Uuid, filter: FilamentFilter) -> Result, AppError>; + + /// Conta o total de filamentos (para paginação). + async fn count(&self, user_id: Uuid, filter: &FilamentFilter) -> Result; + + async fn create(&self, filament: &Filament) -> Result; + async fn update(&self, filament: &Filament) -> Result; + async fn delete(&self, id: Uuid, user_id: Uuid) -> Result<(), AppError>; +} diff --git a/backend/src/ports/mod.rs b/backend/src/ports/mod.rs new file mode 100644 index 0000000..2b6a8df --- /dev/null +++ b/backend/src/ports/mod.rs @@ -0,0 +1,7 @@ +pub mod filament_repository; +pub mod spool_preset_repository; +pub mod user_repository; + +pub use filament_repository::{FilamentFilter, FilamentRepository}; +pub use spool_preset_repository::SpoolPresetRepository; +pub use user_repository::UserRepository; diff --git a/backend/src/ports/spool_preset_repository.rs b/backend/src/ports/spool_preset_repository.rs new file mode 100644 index 0000000..d74040b --- /dev/null +++ b/backend/src/ports/spool_preset_repository.rs @@ -0,0 +1,18 @@ +use async_trait::async_trait; +use uuid::Uuid; + +use crate::{domain::spool_preset::SpoolPreset, error::AppError}; + +/// Port (interface) para operações de persistência de presets de carretel. +/// Implementado pelo adapter `PostgresSpoolPresetRepository`. +#[async_trait] +pub trait SpoolPresetRepository: Send + Sync { + /// Retorna todos os presets do sistema mais os presets customizados do usuário. + async fn list_for_user(&self, user_id: Uuid) -> Result, AppError>; + + async fn find_by_id(&self, id: Uuid) -> Result, AppError>; + + async fn create(&self, preset: &SpoolPreset) -> Result; + async fn update(&self, preset: &SpoolPreset) -> Result; + async fn delete(&self, id: Uuid, user_id: Uuid) -> Result<(), AppError>; +} diff --git a/backend/src/ports/user_repository.rs b/backend/src/ports/user_repository.rs new file mode 100644 index 0000000..a2597c1 --- /dev/null +++ b/backend/src/ports/user_repository.rs @@ -0,0 +1,16 @@ +use async_trait::async_trait; +use uuid::Uuid; + +use crate::{domain::user::User, error::AppError}; + +/// Port (interface) para operações de persistência de usuários. +/// Implementado pelo adapter `PostgresUserRepository`. +#[async_trait] +pub trait UserRepository: Send + Sync { + async fn find_by_id(&self, id: Uuid) -> Result, AppError>; + async fn find_by_email(&self, email: &str) -> Result, AppError>; + async fn find_by_google_id(&self, google_id: &str) -> Result, AppError>; + async fn create(&self, user: &User) -> Result; + async fn update(&self, user: &User) -> Result; + async fn verify_email(&self, id: Uuid) -> Result<(), AppError>; +} diff --git a/backend/src/router.rs b/backend/src/router.rs new file mode 100644 index 0000000..293cb78 --- /dev/null +++ b/backend/src/router.rs @@ -0,0 +1,100 @@ +use axum::{middleware, routing::{delete, get, post, put}, Router}; +use sqlx::PgPool; +use std::sync::Arc; +use tower_http::{cors::CorsLayer, trace::TraceLayer}; + +use crate::{ + adapters::{ + inbound::{ + auth_handler, + filament_handler, + spool_preset_handler, + user_handler, + middleware::auth::auth_middleware, + }, + outbound::{ + PostgresFilamentRepository, + PostgresSpoolPresetRepository, + PostgresUserRepository, + }, + }, + application::{AuthService, FilamentService, SpoolPresetService}, + config::Config, + ports::{FilamentRepository, SpoolPresetRepository, UserRepository}, +}; + +/// Estado compartilhado injetado em todos os handlers via Axum State. +#[derive(Clone)] +pub struct AppState { + pub auth_service: Arc, + pub filament_service: Arc, + pub spool_preset_service: Arc, + pub config: Config, +} + +/// Constrói o router Axum com todas as rotas e middlewares. +pub fn build(db: PgPool, config: Config) -> Router { + let db = Arc::new(db); + + // Repositórios (outbound adapters) + let user_repo: Arc = Arc::new(PostgresUserRepository::new(Arc::clone(&db))); + let filament_repo: Arc = Arc::new(PostgresFilamentRepository::new(Arc::clone(&db))); + let preset_repo: Arc = Arc::new(PostgresSpoolPresetRepository::new(Arc::clone(&db))); + + // Serviços (application layer) + let auth_service = Arc::new(AuthService::new( + Arc::clone(&user_repo), + config.clone(), + )); + let filament_service = Arc::new(FilamentService::new( + Arc::clone(&filament_repo), + Arc::clone(&preset_repo), + )); + let spool_preset_service = Arc::new(SpoolPresetService::new(Arc::clone(&preset_repo))); + + let state = AppState { + auth_service, + filament_service, + spool_preset_service, + config: config.clone(), + }; + + // Rotas públicas (sem autenticação) + let public_routes = Router::new() + .route("/auth/register", post(auth_handler::register_handler)) + .route("/auth/login", post(auth_handler::login_handler)) + .route("/auth/oauth/google", post(auth_handler::google_oauth_handler)) + .route("/auth/refresh", post(auth_handler::refresh_token_handler)) + .route("/auth/forgot-password", post(auth_handler::forgot_password_handler)) + .route("/auth/verify-email", post(auth_handler::verify_email_handler)) + .route("/auth/reset-password", post(auth_handler::reset_password_handler)); + + // Rotas protegidas (requerem JWT válido) + let protected_routes = Router::new() + .route("/auth/logout", post(auth_handler::logout_handler)) + // Users + .route("/users/me", get(user_handler::get_me_handler)) + .route("/users/me", put(user_handler::update_me_handler)) + // Dashboard + .route("/dashboard", get(filament_handler::dashboard_handler)) + // Filaments + .route("/filaments", get(filament_handler::list_filaments_handler)) + .route("/filaments", post(filament_handler::create_filament_handler)) + .route("/filaments/:id", get(filament_handler::get_filament_handler)) + .route("/filaments/:id", put(filament_handler::update_filament_handler)) + .route("/filaments/:id", delete(filament_handler::delete_filament_handler)) + .route("/filaments/:id/qrcode", get(filament_handler::get_qrcode_handler)) + .route("/filaments/:id/label.svg", get(filament_handler::export_label_handler)) + // Spool Presets + .route("/spool-presets", get(spool_preset_handler::list_presets_handler)) + .route("/spool-presets", post(spool_preset_handler::create_preset_handler)) + .route("/spool-presets/:id", put(spool_preset_handler::update_preset_handler)) + .route("/spool-presets/:id", delete(spool_preset_handler::delete_preset_handler)) + .route_layer(middleware::from_fn_with_state(state.clone(), auth_middleware)); + + Router::new() + .nest("/api/v1", public_routes.merge(protected_routes)) + .with_state(state) + .layer(TraceLayer::new_for_http()) + .layer(CorsLayer::permissive()) +}