From 13ba3924e38b535c13b773458295755b95f8d5ae Mon Sep 17 00:00:00 2001 From: Jared Bell Date: Mon, 1 Sep 2025 21:07:03 -0400 Subject: [PATCH] Initial commit --- .env | 2 + .gitignore | 1 + .idea/.gitignore | 8 + .idea/dataSources.xml | 12 + .idea/modules.xml | 8 + .idea/puffpastry-backend.iml | 11 + .idea/sqldialects.xml | 9 + .idea/vcs.xml | 6 + Cargo.lock | 2785 +++++++++++++++++ Cargo.toml | 21 + README_OPENAPI.md | 23 + build.rs | 3 + diesel.toml | 9 + .../down.sql | 6 + .../up.sql | 36 + .../down.sql | 1 + .../2025-08-04-003151_create_proposals/up.sql | 11 + .../down.sql | 2 + .../up.sql | 20 + .../2025-08-13-202500_create_users/down.sql | 1 + .../2025-08-13-202500_create_users/up.sql | 17 + .../down.sql | 1 + .../2025-08-13-232400_create_comments/up.sql | 12 + .../2025-08-20-200000_create_visits/down.sql | 7 + .../2025-08-20-200000_create_visits/up.sql | 27 + .../down.sql | 1 + .../up.sql | 1 + src/db/db.rs | 26 + src/db/mod.rs | 1 + src/ipfs/amendment.rs | 82 + src/ipfs/comment.rs | 106 + src/ipfs/ipfs.rs | 7 + src/ipfs/mod.rs | 4 + src/ipfs/proposal.rs | 81 + src/main.rs | 63 + src/repositories/amendment.rs | 49 + src/repositories/comment.rs | 76 + src/repositories/mod.rs | 5 + src/repositories/proposal.rs | 48 + src/repositories/user.rs | 107 + src/repositories/visit.rs | 94 + src/routes/amendment.rs | 102 + src/routes/auth.rs | 200 ++ src/routes/comment.rs | 18 + src/routes/mod.rs | 5 + src/routes/openapi.rs | 251 ++ src/routes/proposal.rs | 157 + src/schema.rs | 100 + src/types/amendment.rs | 117 + src/types/comment.rs | 50 + src/types/ipfs.rs | 3 + src/types/mod.rs | 4 + src/types/proposal.rs | 127 + src/utils/auth.rs | 85 + src/utils/content.rs | 7 + src/utils/env.rs | 16 + src/utils/ipfs.rs | 148 + src/utils/mod.rs | 4 + 58 files changed, 5184 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/dataSources.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/puffpastry-backend.iml create mode 100644 .idea/sqldialects.xml create mode 100644 .idea/vcs.xml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README_OPENAPI.md create mode 100644 build.rs create mode 100644 diesel.toml create mode 100644 migrations/00000000000000_diesel_initial_setup/down.sql create mode 100644 migrations/00000000000000_diesel_initial_setup/up.sql create mode 100644 migrations/2025-08-04-003151_create_proposals/down.sql create mode 100644 migrations/2025-08-04-003151_create_proposals/up.sql create mode 100644 migrations/2025-08-08-035440_create_amendments/down.sql create mode 100644 migrations/2025-08-08-035440_create_amendments/up.sql create mode 100644 migrations/2025-08-13-202500_create_users/down.sql create mode 100644 migrations/2025-08-13-202500_create_users/up.sql create mode 100644 migrations/2025-08-13-232400_create_comments/down.sql create mode 100644 migrations/2025-08-13-232400_create_comments/up.sql create mode 100644 migrations/2025-08-20-200000_create_visits/down.sql create mode 100644 migrations/2025-08-20-200000_create_visits/up.sql create mode 100644 migrations/2025-08-25-235310_add_full_name_field/down.sql create mode 100644 migrations/2025-08-25-235310_add_full_name_field/up.sql create mode 100644 src/db/db.rs create mode 100644 src/db/mod.rs create mode 100644 src/ipfs/amendment.rs create mode 100644 src/ipfs/comment.rs create mode 100644 src/ipfs/ipfs.rs create mode 100644 src/ipfs/mod.rs create mode 100644 src/ipfs/proposal.rs create mode 100644 src/main.rs create mode 100644 src/repositories/amendment.rs create mode 100644 src/repositories/comment.rs create mode 100644 src/repositories/mod.rs create mode 100644 src/repositories/proposal.rs create mode 100644 src/repositories/user.rs create mode 100644 src/repositories/visit.rs create mode 100644 src/routes/amendment.rs create mode 100644 src/routes/auth.rs create mode 100644 src/routes/comment.rs create mode 100644 src/routes/mod.rs create mode 100644 src/routes/openapi.rs create mode 100644 src/routes/proposal.rs create mode 100644 src/schema.rs create mode 100644 src/types/amendment.rs create mode 100644 src/types/comment.rs create mode 100644 src/types/ipfs.rs create mode 100644 src/types/mod.rs create mode 100644 src/types/proposal.rs create mode 100644 src/utils/auth.rs create mode 100644 src/utils/content.rs create mode 100644 src/utils/env.rs create mode 100644 src/utils/ipfs.rs create mode 100644 src/utils/mod.rs diff --git a/.env b/.env new file mode 100644 index 0000000..8cc4f03 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +DATABASE_URL=postgres://postgres:MxS2p37ViXtXikeb@localhost/puffpastry_jv +JWT_SECRET="Look for the union label, you crazy diamond" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..7006ec7 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/postgres + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..49bca79 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/puffpastry-backend.iml b/.idea/puffpastry-backend.iml new file mode 100644 index 0000000..cf84ae4 --- /dev/null +++ b/.idea/puffpastry-backend.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..092cd50 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0254d5a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2785 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44dfe5c9e0004c623edc65391dfd51daa201e7e30ebd9c9bedf873048ec32bc2" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "foldhash", + "futures-core", + "h2", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.9.2", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.104", +] + +[[package]] +name = "actix-multipart-rfc7578" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b79276c9ca6339b08d468b8165df55ce9ad361f68ac808eb755e23f30e19257" +dependencies = [ + "actix-http", + "bytes", + "common-multipart-rfc7578", + "futures-core", + "thiserror 1.0.69", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "actix-tls" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "http 0.2.12", + "http 1.3.1", + "impl-more", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "foldhash", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.5.10", + "time", + "tracing", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "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 = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "awc" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e76d68b4f02400c2f9110437f254873e8f265b35ea87352f142bc7c8e878115a" +dependencies = [ + "actix-codec", + "actix-http", + "actix-rt", + "actix-service", + "actix-tls", + "actix-utils", + "base64", + "bytes", + "cfg-if", + "cookie", + "derive_more", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "itoa", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.9.2", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[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 = "brotli" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "bytestring" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "common-multipart-rfc7578" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baee326bc603965b0f26583e1ecd7c111c41b49bd92a344897476a352798869" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http 0.2.12", + "mime", + "mime_guess", + "rand 0.8.5", + "thiserror 1.0.69", +] + +[[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.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "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 = "crate-git-revision" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c521bf1f43d31ed2f73441775ed31935d77901cb3451e44b38a1c1612fcbaf98" +dependencies = [ + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[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.104", +] + +[[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.104", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "data-encoding-macro" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" +dependencies = [ + "data-encoding", + "syn 2.0.104", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "unicode-xid", +] + +[[package]] +name = "diesel" +version = "2.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229850a212cd9b84d4f0290ad9d294afc0ae70fccaa8949dbe8b43ffafa1e20c" +dependencies = [ + "bitflags", + "byteorder", + "chrono", + "diesel_derives", + "itoa", + "pq-sys", + "r2d2", +] + +[[package]] +name = "diesel-derive-enum" +version = "3.0.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a8c082045d01debc8589f8a0db9f2855a37c99c9b031325c856b5b98e1625f" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "diesel_derives" +version = "2.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b96984c469425cb577bf6f17121ecb3e4fe1e81de5d8f780dd372802858d756" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" +dependencies = [ + "syn 2.0.104", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dsl_auto_type" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ae9aca7527f85f26dd76483eb38533fd84bd571065da1739656ef71c5ff5b" +dependencies = [ + "darling", + "either", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[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 = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[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 = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[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.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[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 = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +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.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +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 = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "ipfs-api-backend-actix" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c3fa842466c4dc850e7de0a3a95e42c60e828eaf64c5a4a1beb3c598611d8f" +dependencies = [ + "actix-http", + "actix-multipart-rfc7578", + "actix-tls", + "async-trait", + "awc", + "bytes", + "futures", + "http 0.2.12", + "ipfs-api-prelude", + "thiserror 1.0.69", +] + +[[package]] +name = "ipfs-api-prelude" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b74065805db266ba2c6edbd670b23c4714824a955628472b2e46cc9f3a869cb" +dependencies = [ + "async-trait", + "bytes", + "cfg-if", + "common-multipart-rfc7578", + "dirs", + "futures", + "http 0.2.12", + "multiaddr", + "multibase", + "serde", + "serde_json", + "serde_urlencoded", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "tracing", + "walkdir", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +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", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libredox" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[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", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "multiaddr" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b36f567c7099511fa8612bbbb52dda2419ce0bdbacf31714e3a5ffdb766d3bd" +dependencies = [ + "arrayref", + "byteorder", + "data-encoding", + "log", + "multibase", + "multihash", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint", + "url", +] + +[[package]] +name = "multibase" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" +dependencies = [ + "base-x", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "multihash" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835d6ff01d610179fbce3de1694d007e500bf33a7f29689838941d6bf783ae40" +dependencies = [ + "core2", + "multihash-derive", + "unsigned-varint", +] + +[[package]] +name = "multihash-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6d4752e6230d8ef7adf7bd5d8c4b1f6561c1014c5ba9a37445ccefe18aa1db" +dependencies = [ + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + +[[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-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[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 = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[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 = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +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 = "pq-sys" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfd6cf44cca8f9624bc19df234fc4112873432f5fda1caff174527846d026fa9" +dependencies = [ + "libc", + "vcpkg", +] + +[[package]] +name = "proc-macro-crate" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +dependencies = [ + "thiserror 1.0.69", + "toml", +] + +[[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.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "puffpastry-backend" +version = "0.1.0" +dependencies = [ + "actix-web", + "argon2", + "base64", + "chrono", + "diesel", + "diesel-derive-enum", + "dotenvy", + "ed25519-dalek", + "futures", + "ipfs-api-backend-actix", + "jsonwebtoken", + "serde", + "serde_json", + "stellar-strkey", + "uuid", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +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 = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[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.3", +] + +[[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.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "serde_json" +version = "1.0.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "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 = "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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.16", + "time", +] + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stellar-strkey" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee1832fb50c651ad10f734aaf5d31ca5acdfb197a6ecda64d93fcdb8885af913" +dependencies = [ + "crate-git-revision", + "data-encoding", +] + +[[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", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[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.104", +] + +[[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.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl 2.0.16", +] + +[[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.104", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.6.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsigned-varint" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[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.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.104", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "synstructure 0.13.2", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[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.104", + "synstructure 0.13.2", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a885585 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "puffpastry-backend" +version = "0.1.0" +edition = "2024" + +[dependencies] +actix-web = "4.11.0" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.142" +ipfs-api-backend-actix = "0.7.0" +uuid = { version = "1.17.0", features = ["v4"] } +diesel = { version = "2.2.12", features = ["postgres", "chrono", "r2d2"] } +dotenvy = "0.15.7" +chrono = { version = "0.4.41", features = ["serde"] } +futures = "0.3.31" +diesel-derive-enum = { version = "3.0.0-beta.1", features = ["postgres"] } +argon2 = "0.5" +base64 = "0.22" +ed25519-dalek = { version = "2.1", features = ["rand_core"] } +stellar-strkey = "0.0.13" +jsonwebtoken = "9" \ No newline at end of file diff --git a/README_OPENAPI.md b/README_OPENAPI.md new file mode 100644 index 0000000..d0a2d25 --- /dev/null +++ b/README_OPENAPI.md @@ -0,0 +1,23 @@ +PuffPastry Backend OpenAPI and Frontend SDK Generation + +This backend exposes an OpenAPI 3.0 document at: + + GET http://localhost:7300/openapi.json + +You can generate a TypeScript client for your frontend using openapi-typescript-codegen: + +1) Install the generator (once): + npm install -g openapi-typescript-codegen + +2) Run code generation (adjust output path to your frontend project): + openapi --input http://localhost:7300/openapi.json --output ../frontend/src/api --client axios + +If you prefer, you can output a local file first: + + curl -s http://localhost:7300/openapi.json -o openapi.json + openapi --input openapi.json --output ../frontend/src/api --client axios + +Notes +- The current OpenAPI spec is minimal and focuses on request shapes and endpoints. It’s sufficient for client generation. +- We can later switch to automatic spec generation with Apistos annotations without changing the endpoint path. +- If you add new endpoints, update the spec in src/routes/openapi.rs accordingly, or let’s enhance it with Apistos once we decide on the annotation strategy. diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..4870f12 --- /dev/null +++ b/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rustc-link-search=native=/Library/PostgreSQL/17/lib"); +} diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..9f9c28e --- /dev/null +++ b/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] + +[migrations_directory] +dir = "/Users/jaredbell/RustroverProjects/puffpastry-backend/migrations" diff --git a/migrations/00000000000000_diesel_initial_setup/down.sql b/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 0000000..a9f5260 --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 0000000..d68895b --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/migrations/2025-08-04-003151_create_proposals/down.sql b/migrations/2025-08-04-003151_create_proposals/down.sql new file mode 100644 index 0000000..aa5b205 --- /dev/null +++ b/migrations/2025-08-04-003151_create_proposals/down.sql @@ -0,0 +1 @@ +drop table proposals; \ No newline at end of file diff --git a/migrations/2025-08-04-003151_create_proposals/up.sql b/migrations/2025-08-04-003151_create_proposals/up.sql new file mode 100644 index 0000000..7fb4cd2 --- /dev/null +++ b/migrations/2025-08-04-003151_create_proposals/up.sql @@ -0,0 +1,11 @@ +create table proposals ( + id serial primary key, + name text not null, + cid text not null, + summary text, + creator text not null, + is_current boolean not null, -- true for latest/current version + previous_cid text, -- the CID of the previous version, if any + created_at timestamp default now(), + updated_at timestamp +) diff --git a/migrations/2025-08-08-035440_create_amendments/down.sql b/migrations/2025-08-08-035440_create_amendments/down.sql new file mode 100644 index 0000000..e3bc4c6 --- /dev/null +++ b/migrations/2025-08-08-035440_create_amendments/down.sql @@ -0,0 +1,2 @@ +drop table amendments; +drop type amendment_status; diff --git a/migrations/2025-08-08-035440_create_amendments/up.sql b/migrations/2025-08-08-035440_create_amendments/up.sql new file mode 100644 index 0000000..01dd820 --- /dev/null +++ b/migrations/2025-08-08-035440_create_amendments/up.sql @@ -0,0 +1,20 @@ +create type amendment_status as enum ( + 'proposed', + 'approved', + 'withdrawn', + 'rejected' +); + +create table amendments ( + id serial primary key, + name text not null, + cid text not null, + summary text, + status amendment_status not null default 'proposed', + creator text not null, + is_current boolean not null default true, + previous_cid text, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + proposal_id integer not null references proposals(id) +) \ No newline at end of file diff --git a/migrations/2025-08-13-202500_create_users/down.sql b/migrations/2025-08-13-202500_create_users/down.sql new file mode 100644 index 0000000..02bd794 --- /dev/null +++ b/migrations/2025-08-13-202500_create_users/down.sql @@ -0,0 +1 @@ +drop table users; \ No newline at end of file diff --git a/migrations/2025-08-13-202500_create_users/up.sql b/migrations/2025-08-13-202500_create_users/up.sql new file mode 100644 index 0000000..7c3f0cd --- /dev/null +++ b/migrations/2025-08-13-202500_create_users/up.sql @@ -0,0 +1,17 @@ +-- Users table to hold account information +-- Postgres dialect + +create table users ( + id serial primary key, + username text not null unique, + password_hash text not null, + stellar_address text not null unique, + email text unique, + is_active boolean not null default true, + roles text[] not null default '{}', + last_login_at timestamptz, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +-- Optional helpful indexes (unique already creates indexes, but adding a lower() index for case-insensitive lookups could be considered later) diff --git a/migrations/2025-08-13-232400_create_comments/down.sql b/migrations/2025-08-13-232400_create_comments/down.sql new file mode 100644 index 0000000..15bf25b --- /dev/null +++ b/migrations/2025-08-13-232400_create_comments/down.sql @@ -0,0 +1 @@ +drop table comments; \ No newline at end of file diff --git a/migrations/2025-08-13-232400_create_comments/up.sql b/migrations/2025-08-13-232400_create_comments/up.sql new file mode 100644 index 0000000..3ab9440 --- /dev/null +++ b/migrations/2025-08-13-232400_create_comments/up.sql @@ -0,0 +1,12 @@ +-- Comments schema +-- Postgres dialect + +create table comments ( + id serial primary key, + cid text not null, + is_current bool not null default true, + previous_cid text, + proposal_id integer not null references proposals(id) on delete cascade, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); diff --git a/migrations/2025-08-20-200000_create_visits/down.sql b/migrations/2025-08-20-200000_create_visits/down.sql new file mode 100644 index 0000000..04d9f34 --- /dev/null +++ b/migrations/2025-08-20-200000_create_visits/down.sql @@ -0,0 +1,7 @@ +drop index if exists idx_visits_method; +drop index if exists idx_visits_path; +drop index if exists idx_visits_proposal_id; +drop index if exists idx_visits_user_id; +drop index if exists idx_visits_visited_at; + +drop table if exists visits; \ No newline at end of file diff --git a/migrations/2025-08-20-200000_create_visits/up.sql b/migrations/2025-08-20-200000_create_visits/up.sql new file mode 100644 index 0000000..55796c8 --- /dev/null +++ b/migrations/2025-08-20-200000_create_visits/up.sql @@ -0,0 +1,27 @@ +-- Visits tracking table +-- Postgres dialect + +create table visits ( + id serial primary key, + user_id integer references users(id) on delete set null, + proposal_id integer not null, + path text not null, + method text not null, + query_string text, + status_code integer, + ip_address text, + user_agent text, + referrer text, + session_id text, + request_id text, + visited_at timestamptz not null default now(), + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +-- Helpful indexes for analytics and lookups +create index idx_visits_visited_at on visits(visited_at); +create index idx_visits_user_id on visits(user_id); +create index idx_visits_proposal_id on visits(proposal_id); +create index idx_visits_path on visits(path); +create index idx_visits_method on visits(method); diff --git a/migrations/2025-08-25-235310_add_full_name_field/down.sql b/migrations/2025-08-25-235310_add_full_name_field/down.sql new file mode 100644 index 0000000..7b3ec11 --- /dev/null +++ b/migrations/2025-08-25-235310_add_full_name_field/down.sql @@ -0,0 +1 @@ +alter table users drop column full_name; \ No newline at end of file diff --git a/migrations/2025-08-25-235310_add_full_name_field/up.sql b/migrations/2025-08-25-235310_add_full_name_field/up.sql new file mode 100644 index 0000000..e920f40 --- /dev/null +++ b/migrations/2025-08-25-235310_add_full_name_field/up.sql @@ -0,0 +1 @@ +alter table users add column full_name text; \ No newline at end of file diff --git a/src/db/db.rs b/src/db/db.rs new file mode 100644 index 0000000..2e7c7c7 --- /dev/null +++ b/src/db/db.rs @@ -0,0 +1,26 @@ +use diesel::r2d2::{self, ConnectionManager}; +use diesel::PgConnection; +use std::env; +use dotenvy::dotenv; + +pub type DbPool = r2d2::Pool>; +pub type DbConn = r2d2::PooledConnection>; + +pub fn establish_connection() -> Result> { + dotenv().ok(); + + let database_url = env::var("DATABASE_URL") + .map_err(|_| "DATABASE_URL must be set")?; + + let manager = ConnectionManager::::new(database_url); + let pool = r2d2::Pool::builder() + .build(manager) + .map_err(|e| format!("Failed to create pool: {}", e))?; + + Ok(pool) +} + +pub fn get_connection() -> Result> { + let pool = establish_connection()?; + pool.get().map_err(|e| -> Box { Box::new(e) }) +} \ No newline at end of file diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..2e70b07 --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1 @@ +pub(crate) mod db; \ No newline at end of file diff --git a/src/ipfs/amendment.rs b/src/ipfs/amendment.rs new file mode 100644 index 0000000..ad7e309 --- /dev/null +++ b/src/ipfs/amendment.rs @@ -0,0 +1,82 @@ +use crate::db::db::get_connection; +use crate::ipfs::ipfs::IpfsService; +use crate::schema::amendments; +use crate::types::amendment::{Amendment, AmendmentError, AmendmentFile}; +use crate::types::ipfs::IpfsResult; +use crate::utils::content::extract_summary; +use crate::utils::ipfs::{read_json_via_cat, upload_json_and_get_hash, DEFAULT_MAX_JSON_SIZE}; +use diesel::ExpressionMethods; +use diesel::QueryDsl; +use diesel::RunQueryDsl; +use ipfs_api_backend_actix::IpfsClient; + +const STORAGE_DIR: &str = "/puffpastry/amendments"; +const FILE_EXTENSION: &str = "json"; + +pub struct AmendmentService { + client: IpfsClient, +} + +impl IpfsService for AmendmentService { + type Err = AmendmentError; + + async fn save(&mut self, item: AmendmentFile) -> IpfsResult { + self.store_amendment_to_ipfs(item).await + } + + async fn read(&mut self, hash: String) -> IpfsResult { + self.read_amendment_file(&hash).await + } +} + +impl AmendmentService { + pub fn new(client: IpfsClient) -> Self { + Self { client } + } + + async fn store_amendment_to_ipfs(&self, amendment: AmendmentFile) -> IpfsResult { + let amendment_record = Amendment::new( + amendment.name.clone(), + Some(extract_summary(&amendment.content)), + amendment.creator.clone(), + amendment.proposal_id, + ); + + let hash = self.upload_to_ipfs(&amendment).await?; + self.save_to_database(amendment_record.with_cid(hash.clone()).mark_as_current()).await?; + + { + use crate::schema::amendments::dsl::{amendments as amendments_table, is_current as is_current_col, previous_cid as previous_cid_col}; + let mut conn = get_connection() + .map_err(|e| AmendmentError::DatabaseError(e))?; + + // Ignore the count result; if no rows match, that's fine + let _ = diesel::update(amendments_table.filter(previous_cid_col.eq(hash.clone()))) + .set(is_current_col.eq(false)) + .execute(&mut conn) + .map_err(|e| AmendmentError::DatabaseError(Box::new(e)))?; + } + + Ok(hash) + } + + async fn upload_to_ipfs(&self, amendment: &AmendmentFile) -> IpfsResult { + upload_json_and_get_hash::(&self.client, STORAGE_DIR, FILE_EXTENSION, amendment).await + } + + async fn save_to_database(&self, amendment: Amendment) -> IpfsResult<(), AmendmentError> { + let mut conn = get_connection() + .map_err(|e| AmendmentError::DatabaseError(e))?; + + diesel::insert_into(amendments::table) + .values(&amendment) + .execute(&mut conn) + .map_err(|e| AmendmentError::DatabaseError(Box::new(e)))?; + + Ok(()) + } + + async fn read_amendment_file(&self, hash: &str) -> IpfsResult { + read_json_via_cat::(&self.client, hash, DEFAULT_MAX_JSON_SIZE).await + } +} \ No newline at end of file diff --git a/src/ipfs/comment.rs b/src/ipfs/comment.rs new file mode 100644 index 0000000..2da9ddd --- /dev/null +++ b/src/ipfs/comment.rs @@ -0,0 +1,106 @@ +use crate::ipfs::ipfs::IpfsService; +use crate::types::comment::{CommentError, CommentMetadata}; +use crate::types::ipfs::IpfsResult; +use crate::utils::ipfs::{ + create_file_path, + create_storage_directory, + read_json_via_cat, + retrieve_content_hash, + save_json_file, + DEFAULT_MAX_JSON_SIZE, + list_directory_file_hashes, +}; +use ipfs_api_backend_actix::IpfsClient; + +const STORAGE_DIR: &str = "/puffpastry/comments"; +const FILE_EXTENSION: &str = "json"; + +pub struct CommentService { + client: IpfsClient, +} + +// Implement per-proposal subdirectory saving. The input is (proposal_cid, comments_batch) +impl IpfsService<(String, Vec)> for CommentService { + type Err = CommentError; + + async fn save(&mut self, item: (String, Vec)) -> IpfsResult { + let (proposal_cid, comments) = item; + // Allow batch save within the proposal's subdirectory. + if comments.is_empty() { + return Err(CommentError::from(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Failed to store comment to IPFS: empty comments batch", + ))); + } + let mut last_cid: Option = None; + for comment in comments { + let res = self + .store_comment_in_proposal_dir_and_publish(&proposal_cid, comment) + .await; + match res { + Ok(cid) => last_cid = Some(cid), + Err(e) => return Err(e), + } + } + match last_cid { + Some(cid) => Ok(cid), + None => Err(CommentError::from(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to store comment to IPFS", + ))), + } + } + + async fn read(&mut self, hash: String) -> IpfsResult<(String, Vec), Self::Err> { + // For reading, the caller should pass a directory hash (CID). We return only the comments vector here. + // Since IpfsService requires returning the same T, include an empty proposal id (unknown in read-by-hash). + // Alternatively, callers should not use the proposal id from this return value. + let comments = self.read_all_comments_in_dir(&hash).await?; + Ok((String::new(), comments)) + } +} + +impl CommentService { + pub fn new(client: IpfsClient) -> Self { + Self { client } + } + + // Writes a single comment JSON file into the MFS comments subdirectory for the proposal, + // then publishes the subdirectory snapshot (by retrieving the directory CID). Returns the comment file CID. + async fn store_comment_in_proposal_dir_and_publish( + &self, + proposal_cid: &str, + comment: CommentMetadata, + ) -> IpfsResult { + // Ensure the per-proposal storage subdirectory exists + let proposal_dir = format!("{}/{}", STORAGE_DIR, proposal_cid); + create_storage_directory::(&self.client, &proposal_dir).await?; + + // Create a unique file path within the proposal subdirectory and save the JSON content + let file_path = create_file_path(&proposal_dir, FILE_EXTENSION); + save_json_file::(&self.client, &file_path, &comment).await?; + + // Retrieve the file's CID to return to the caller + let file_cid = retrieve_content_hash::(&self.client, &file_path).await?; + + // Publish a new snapshot of the proposal's comments subdirectory by retrieving its CID + let _dir_cid = retrieve_content_hash::(&self.client, &proposal_dir).await?; + + Ok(file_cid) + } + + async fn read_comment_file(&self, hash: &str) -> IpfsResult { + read_json_via_cat::(&self.client, hash, DEFAULT_MAX_JSON_SIZE).await + } + + // Read all comment files within a directory identified by the given hash (directory CID) + async fn read_all_comments_in_dir(&self, dir_hash: &str) -> IpfsResult, CommentError> { + let file_hashes = list_directory_file_hashes::(&self.client, dir_hash).await?; + let mut comments: Vec = Vec::with_capacity(file_hashes.len()); + for fh in file_hashes { + let comment = self.read_comment_file(&fh).await?; + comments.push(comment); + } + Ok(comments) + } +} diff --git a/src/ipfs/ipfs.rs b/src/ipfs/ipfs.rs new file mode 100644 index 0000000..6e09399 --- /dev/null +++ b/src/ipfs/ipfs.rs @@ -0,0 +1,7 @@ +use crate::types::ipfs::IpfsResult; + +pub trait IpfsService { + type Err; + async fn save(&mut self, item: T) -> IpfsResult; + async fn read(&mut self, hash: String) -> IpfsResult; +} \ No newline at end of file diff --git a/src/ipfs/mod.rs b/src/ipfs/mod.rs new file mode 100644 index 0000000..6f5f72b --- /dev/null +++ b/src/ipfs/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod ipfs; +pub(crate) mod proposal; +pub(crate) mod amendment; +pub(crate) mod comment; \ No newline at end of file diff --git a/src/ipfs/proposal.rs b/src/ipfs/proposal.rs new file mode 100644 index 0000000..f4c03c1 --- /dev/null +++ b/src/ipfs/proposal.rs @@ -0,0 +1,81 @@ +use crate::db::db::get_connection; +use crate::ipfs::ipfs::IpfsService; +use crate::schema::proposals; +use crate::types::proposal::{Proposal, ProposalError, ProposalFile}; +use crate::utils::content::extract_summary; +use crate::utils::ipfs::{upload_json_and_get_hash, read_json_via_cat as util_read_json_via_cat, DEFAULT_MAX_JSON_SIZE}; +use diesel::{RunQueryDsl, QueryDsl, ExpressionMethods}; +use ipfs_api_backend_actix::IpfsClient; +use crate::types::ipfs::IpfsResult; + +const STORAGE_DIR: &str = "/puffpastry/proposals"; +const FILE_EXTENSION: &str = "json"; + +pub struct ProposalService { + client: IpfsClient, +} + +impl IpfsService for ProposalService { + type Err = ProposalError; + + async fn save(&mut self, proposal: ProposalFile) -> IpfsResult { + self.store_proposal_to_ipfs(proposal).await + } + + async fn read(&mut self, hash: String) -> IpfsResult { + self.read_proposal_file(hash).await + } +} + +impl ProposalService { + pub fn new(client: IpfsClient) -> Self { + Self { client } + } + + async fn store_proposal_to_ipfs(&self, proposal: ProposalFile) -> IpfsResult { + let proposal_record = Proposal::new( + proposal.name.clone(), + Some(extract_summary(&proposal.content)), + proposal.creator.clone(), + ); + + let hash = self.upload_to_ipfs(&proposal).await?; + self.save_to_database(proposal_record.with_cid(hash.clone()).mark_as_current()).await?; + + // After saving the new proposal, set is_current = false for any proposal + // that has previous_cid equal to this new hash + { + use crate::schema::proposals::dsl::{proposals as proposals_table, previous_cid as previous_cid_col, is_current as is_current_col}; + let mut conn = get_connection() + .map_err(|e| ProposalError::DatabaseError(e))?; + + // Ignore the count result; if no rows match, that's fine + let _ = diesel::update(proposals_table.filter(previous_cid_col.eq(hash.clone()))) + .set(is_current_col.eq(false)) + .execute(&mut conn) + .map_err(|e| ProposalError::DatabaseError(Box::new(e)))?; + } + + Ok(hash) + } + + async fn upload_to_ipfs(&self, data: &ProposalFile) -> IpfsResult { + upload_json_and_get_hash::(&self.client, STORAGE_DIR, FILE_EXTENSION, data).await + } + + async fn save_to_database(&self, proposal: Proposal) -> IpfsResult<(), ProposalError> { + let mut conn = get_connection() + .map_err(|e| ProposalError::DatabaseError(e))?; + + diesel::insert_into(proposals::table) + .values(&proposal) + .execute(&mut conn) + .map_err(|e| ProposalError::DatabaseError(Box::new(e)))?; + + Ok(()) + } + + async fn read_proposal_file(&self, hash: String) -> IpfsResult { + util_read_json_via_cat::(&self.client, &hash, DEFAULT_MAX_JSON_SIZE).await + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..59fe297 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,63 @@ +extern crate core; + +mod routes; +mod schema; +mod db; +mod ipfs; +mod types; +mod repositories; +mod utils; + +use crate::routes::proposal::{add_proposal, get_proposal, list_proposals, visit_proposal}; +use actix_web::{web, App, HttpServer}; +use crate::routes::amendment::{add_amendment, get_amendment, list_amendments}; +use crate::routes::auth::{register, login, login_freighter, get_nonce}; +use crate::routes::openapi::get_openapi; +use crate::utils::env::load_env; + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + // Load environment variables from .env file (if present) + load_env(); + + let ip = "127.0.0.1"; + let port = 7300; + + println!("Starting server on http://{}:{}", ip, port); + HttpServer::new(|| { + App::new() + .service(get_openapi) + .service( + web::scope("/api/v1") + .service( + web::scope("/proposals") + .service(list_proposals) + ) + .service( + web::scope("/proposal") + .service(add_proposal) + .service(get_proposal) + .service(visit_proposal) + ) + .service( + web::scope("/amendment") + .service(add_amendment) + .service(get_amendment) + ) + .service( + web::scope("/amendments") + .service(list_amendments) + ) + .service( + web::scope("/auth") + .service(register) + .service(login) + .service(login_freighter) + .service(get_nonce) + ) + ) + }) + .bind((ip, port))? + .run() + .await +} diff --git a/src/repositories/amendment.rs b/src/repositories/amendment.rs new file mode 100644 index 0000000..3e57753 --- /dev/null +++ b/src/repositories/amendment.rs @@ -0,0 +1,49 @@ +use crate::db::db::establish_connection; +use crate::schema::amendments::dsl::amendments; +use crate::types::amendment::SelectableAmendment; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; + +pub fn get_amendments_for_proposal( + proposal_id: i32, +) -> Result, diesel::result::Error> { + let pool = establish_connection().map_err(|_| { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to establish database connection".to_string()), + ) + })?; + + let mut conn = pool.get().map_err(|_| { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::UnableToSendCommand, + Box::new("Failed to get connection from pool".to_string()), + ) + })?; + + use crate::schema::amendments::dsl::{amendments, proposal_id as proposal_id_col}; + amendments + .filter(proposal_id_col.eq(proposal_id)) + .select(SelectableAmendment::as_select()) + .order(crate::schema::amendments::id.asc()) + .load(&mut conn) +} + +pub fn get_cid_for_amendment(amendment_id: i32) -> Result { + let pool = establish_connection() + .map_err(|_| diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to establish database connection".to_string()) + ))?; + + let mut conn = pool.get() + .map_err(|_| diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to get database connection from pool".to_string()) + ))?; + + use crate::schema::amendments::dsl::{id as id_col, cid as cid_col}; + amendments + .filter(id_col.eq(amendment_id)) + .select(cid_col) + .get_result(&mut conn) +} \ No newline at end of file diff --git a/src/repositories/comment.rs b/src/repositories/comment.rs new file mode 100644 index 0000000..44a7c90 --- /dev/null +++ b/src/repositories/comment.rs @@ -0,0 +1,76 @@ +use diesel::{RunQueryDsl, ExpressionMethods, QueryDsl, Connection, BoolExpressionMethods}; +use crate::db::db::establish_connection; +use crate::schema::comments::dsl::*; + +// Store the new CID for the updated comment thread file on IPFS +// Instead of updating an existing record, we: +// 1) Mark the previous record (by its CID) as not current (is_current=false) +// 2) Insert a new record with is_current=true and previous_cid set to the previous CID +pub fn store_new_thread_cid_for_proposal( + pid: i32, + new_cid_value: &str, + previous_cid_value: Option<&str>, +) -> Result<(), diesel::result::Error> { + let pool = establish_connection() + .map_err(|_| diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to establish database connection".to_string()), + ))?; + + let mut conn = pool.get().map_err(|_| { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to get database connection from pool".to_string()), + ) + })?; + + conn.transaction(|conn| { + if let Some(prev) = previous_cid_value { + // Best-effort mark previous as not current; it's okay if 0 rows were updated (e.g., first insert) + let _ = diesel::update(comments.filter(cid.eq(prev))) + .set(is_current.eq(false)) + .execute(conn)?; + } + + diesel::insert_into(crate::schema::comments::table) + .values(( + cid.eq(new_cid_value), + proposal_id.eq(pid), + is_current.eq(true), + previous_cid.eq(previous_cid_value), + )) + .execute(conn) + .map(|_| ()) + }) +} + +// Retrieve the latest/current thread CID for a proposal +pub fn latest_thread_cid_for_proposal(pid_value: i32) -> Result { + let pool = establish_connection() + .map_err(|_| diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to establish database connection".to_string()), + ))?; + + let mut conn = pool.get().map_err(|_| { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to get database connection from pool".to_string()), + ) + })?; + + // Prefer the record marked as current; fallback to most recent if none are marked + match comments + .filter(proposal_id.eq(pid_value).and(is_current.eq(true))) + .order(created_at.desc()) + .select(cid) + .first::(&mut conn) + { + Ok(current) => Ok(current), + Err(_) => comments + .filter(proposal_id.eq(pid_value)) + .order(created_at.desc()) + .select(cid) + .first::(&mut conn), + } +} diff --git a/src/repositories/mod.rs b/src/repositories/mod.rs new file mode 100644 index 0000000..53945e5 --- /dev/null +++ b/src/repositories/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod proposal; +pub(crate) mod amendment; +pub(crate) mod user; +pub(crate) mod comment; +pub(crate) mod visit; \ No newline at end of file diff --git a/src/repositories/proposal.rs b/src/repositories/proposal.rs new file mode 100644 index 0000000..833e1d4 --- /dev/null +++ b/src/repositories/proposal.rs @@ -0,0 +1,48 @@ +use diesel::ExpressionMethods; +use diesel::{QueryDsl, RunQueryDsl}; +use crate::db::db::establish_connection; +use crate::schema::proposals::dsl::*; +use crate::types::proposal::SelectableProposal; + +pub fn paginate_proposals( + offset: i64, + limit: i64 +) -> Result, diesel::result::Error> { + let pool = establish_connection() + .map_err(|_| diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to establish database connection".to_string()) + ))?; + + let mut conn = pool.get() + .map_err(|_| diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to get database connection from pool".to_string()) + ))?; + + proposals + .filter(is_current.eq(true)) + .offset(offset) + .limit(limit) + .order(created_at.desc()) + .load::(&mut conn) +} + +pub fn get_cid_for_proposal(proposal_id: i32) -> Result { + let pool = establish_connection() + .map_err(|_| diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to establish database connection".to_string()) + ))?; + + let mut conn = pool.get() + .map_err(|_| diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to get database connection from pool".to_string()) + ))?; + + proposals. + filter(id.eq(proposal_id)) + .select(cid) + .get_result(&mut conn) +} \ No newline at end of file diff --git a/src/repositories/user.rs b/src/repositories/user.rs new file mode 100644 index 0000000..eff5562 --- /dev/null +++ b/src/repositories/user.rs @@ -0,0 +1,107 @@ +use diesel::prelude::*; +use diesel::RunQueryDsl; +use diesel::pg::PgConnection; +use diesel::r2d2; +use serde::Serialize; + +use crate::db::db::establish_connection; +use crate::schema::users; +use crate::schema::users::dsl::*; + +#[derive(Insertable)] +#[diesel(table_name = users)] +pub struct NewUserInsert { + pub username: String, + pub password_hash: String, + pub stellar_address: String, + pub email: Option, +} + +#[derive(Serialize)] +pub struct RegisteredUser { + pub id: i32, + pub username: String, + pub stellar_address: String, + pub email: Option, +} + +#[derive(Queryable)] +pub struct UserAuth { + pub id: i32, + pub username: String, + pub full_name: Option, + pub password_hash: String, + pub stellar_address: String, + pub email: Option, + pub is_active: bool, +} + +#[derive(Queryable, Serialize)] +pub struct UserForAuth { + pub id: i32, + pub full_name: Option, + pub username: String, + pub stellar_address: String, + pub email: Option, + pub is_active: bool, +} + +fn get_conn() -> Result>, diesel::result::Error> { + let pool = establish_connection().map_err(|_| { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to establish database connection".to_string()), + ) + })?; + + pool.get().map_err(|_| { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to get database connection from pool".to_string()), + ) + }) +} + +pub(crate) fn create_user( + new_user: NewUserInsert, +) -> Result { + let mut conn = get_conn()?; + + diesel::insert_into(users::table) + .values(&new_user) + .returning((id, username, stellar_address, email)) + .get_result::<(i32, String, String, Option)>(&mut conn) + .map(|(uid, uname, saddr, mail)| RegisteredUser { + id: uid, + username: uname, + stellar_address: saddr, + email: mail, + }) +} + +pub(crate) fn find_user_by_username(uname: &str) -> Result { + let mut conn = get_conn()?; + + users + .filter(username.eq(uname)) + .select((id, username, full_name, password_hash, stellar_address, email, is_active)) + .first::(&mut conn) +} + +pub(crate) fn get_user_for_auth_by_stellar(addr: &str) -> Result { + let mut conn = get_conn()?; + + users + .filter(stellar_address.eq(addr)) + .select((id, full_name, username, stellar_address, email, is_active)) + .first::(&mut conn) +} + +pub(crate) fn get_user_for_auth_by_email(mail: &str) -> Result { + let mut conn = get_conn()?; + + users + .filter(email.eq(mail)) + .select((id, full_name, username, stellar_address, email, is_active)) + .first::(&mut conn) +} diff --git a/src/repositories/visit.rs b/src/repositories/visit.rs new file mode 100644 index 0000000..0c4f48b --- /dev/null +++ b/src/repositories/visit.rs @@ -0,0 +1,94 @@ +use diesel::sql_query; +use diesel::RunQueryDsl; +use diesel::sql_types::{BigInt, Int4, Nullable, Text, Timestamptz}; +use crate::db::db::establish_connection; +use chrono::{DateTime, Duration, Utc}; +use diesel::QueryableByName; + +pub fn record_visit( + proposal_id_val: i32, + user_id_val: Option, + path_val: &str, + method_val: &str, + query_string_val: Option<&str>, + status_code_val: Option, + ip_address_val: Option<&str>, + user_agent_val: Option<&str>, + referrer_val: Option<&str>, + session_id_val: Option<&str>, + request_id_val: Option<&str>, +) -> Result<(), diesel::result::Error> { + let pool = establish_connection() + .map_err(|_| diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to establish database connection".to_string()), + ))?; + + let mut conn = pool.get().map_err(|_| { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to get database connection from pool".to_string()), + ) + })?; + + // Use raw SQL to avoid requiring schema.rs updates for the visits table + let query = sql_query( + "insert into visits (user_id, proposal_id, path, method, query_string, status_code, ip_address, user_agent, referrer, session_id, request_id) \ + values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)" + ) + .bind::, _>(user_id_val) + .bind::(proposal_id_val) + .bind::(path_val) + .bind::(method_val) + .bind::, _>(query_string_val) + .bind::, _>(status_code_val) + .bind::, _>(ip_address_val) + .bind::, _>(user_agent_val) + .bind::, _>(referrer_val) + .bind::, _>(session_id_val) + .bind::, _>(request_id_val); + + // Execute insert + query.execute(&mut conn).map(|_| ()) +} + +#[derive(QueryableByName)] +struct CountResult { + #[diesel(sql_type = BigInt)] + cnt: i64, +} + +pub fn recent_visit_count_since( + proposal_id_val: i32, + since: DateTime, +) -> Result { + let pool = establish_connection() + .map_err(|_| diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to establish database connection".to_string()), + ))?; + + let mut conn = pool.get().map_err(|_| { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::Unknown, + Box::new("Failed to get database connection from pool".to_string()), + ) + })?; + + let rows: Vec = sql_query( + "select count(*) as cnt from visits where proposal_id = $1 and visited_at > $2" + ) + .bind::(proposal_id_val) + .bind::(since) + .load(&mut conn)?; + + Ok(rows.get(0).map(|r| r.cnt).unwrap_or(0)) +} + +pub fn recent_visit_count_in_minutes( + proposal_id_val: i32, + minutes: i64, +) -> Result { + let since = Utc::now() - Duration::minutes(minutes); + recent_visit_count_since(proposal_id_val, since) +} diff --git a/src/routes/amendment.rs b/src/routes/amendment.rs new file mode 100644 index 0000000..47d7964 --- /dev/null +++ b/src/routes/amendment.rs @@ -0,0 +1,102 @@ +use actix_web::{get, post, web, HttpResponse}; +use ipfs_api_backend_actix::IpfsClient; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use crate::ipfs::amendment::AmendmentService; +use crate::ipfs::ipfs::IpfsService; +use crate::repositories::amendment::{get_amendments_for_proposal, get_cid_for_amendment}; +use crate::types::amendment::{AmendmentError, AmendmentFile}; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ListAmendmentsRequest { + pub proposal_id: i32, +} +#[get("list")] +async fn list_amendments(params: web::Query) -> Result { + match get_amendments_for_proposal(params.proposal_id) { + Ok(amendments) => Ok(HttpResponse::Ok().json(json!({"amendments": amendments}))), + Err(err) => Ok(HttpResponse::InternalServerError().json( + json!({"error": err.to_string()}) + )) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AddAmendmentRequest { + pub proposal_id: i32, + pub name: String, + pub content: String, + pub creator: String, +} +#[post("add")] +async fn add_amendment(params: web::Json) -> Result { + let req = params.into_inner(); + + // Basic input validation + if req.creator.trim().is_empty() { + return Ok(HttpResponse::BadRequest().json(json!({"error": "Creator is required"}))); + } + if req.name.trim().is_empty() { + return Ok(HttpResponse::BadRequest().json(json!({"error": "Name is required"}))); + } + if req.content.trim().is_empty() { + return Ok(HttpResponse::BadRequest().json(json!({"error": "Content is required"}))); + } + if req.proposal_id <= 0 { + return Ok(HttpResponse::BadRequest().json(json!({"error": "proposalId must be a positive integer"}))); + } + + match create_and_store_amendment(req).await { + Ok(cid) => Ok(HttpResponse::Ok().json(json!({"cid": cid}))), + Err(err) => { + match err { + AmendmentError::SerializationError(e) => Ok(HttpResponse::BadRequest().json( + json!({"error": format!("Invalid amendment payload: {}", e)}) + )), + _ => Ok(HttpResponse::InternalServerError().json( + json!({"error": format!("Failed to add amendment: {}", err)}) + )), + } + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetAmendmentRequest { + pub amendment_id: i32, +} +#[get("get")] +async fn get_amendment(request: web::Query) -> Result { + let mut amendment_client = AmendmentService::new(IpfsClient::default()); + let cid = match get_cid_for_amendment(request.amendment_id) { + Ok(cid) => cid, + Err(err) => return Ok(HttpResponse::InternalServerError().json( + json!({"error": format!("Failed to get amendment: {}", err)}) + )) + }; + let item = amendment_client.read(cid).await; + match item { + Ok(item) => Ok(HttpResponse::Ok().json(json!({"amendment": item}))), + Err(e) => Ok(HttpResponse::InternalServerError().json(json!({"error": format!("Failed to read amendment: {}", e)}))) + } +} + +async fn create_and_store_amendment(request: AddAmendmentRequest) -> Result { + let amendment_data = AmendmentFile { + name: request.name.to_string(), + content: request.content.to_string(), + creator: request.creator.to_string(), + created_at: Default::default(), + updated_at: Default::default(), + proposal_id: request.proposal_id, + }; + + let client = IpfsClient::default(); + let mut amendment_service = AmendmentService::new(client); + + let cid = amendment_service.save(amendment_data).await?; + Ok(cid) +} \ No newline at end of file diff --git a/src/routes/auth.rs b/src/routes/auth.rs new file mode 100644 index 0000000..a34bdb3 --- /dev/null +++ b/src/routes/auth.rs @@ -0,0 +1,200 @@ +use actix_web::{get, post, web, HttpResponse}; +use serde::Deserialize; +use serde_json::json; + +use crate::repositories::user::{create_user, NewUserInsert, RegisteredUser, find_user_by_username, get_user_for_auth_by_stellar}; + +const JWT_EXPIRATION_TIME: i64 = 240; + +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString, PasswordHash, PasswordVerifier}, + Argon2, +}; + +use crate::utils::auth::{verify_freighter_signature, generate_jwt, generate_freighter_nonce}; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RegisterRequest { + pub username: String, + pub password: String, + pub stellar_address: String, + pub email: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LoginRequest { + pub username: String, + pub password: String, +} + +#[post("register")] +pub async fn register(req: web::Json) -> Result { + // Basic validation + if req.username.trim().is_empty() || req.password.is_empty() || req.stellar_address.trim().is_empty() { + return Ok(HttpResponse::BadRequest().json(json!({ + "error": "username, password and stellarAddress are required" + }))); + } + + // Hash password with Argon2id and a random salt + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + let password_hash = match argon2.hash_password(req.password.as_bytes(), &salt) { + Ok(ph) => ph.to_string(), + Err(e) => return Ok(HttpResponse::InternalServerError().json(json!({ + "error": format!("Failed to hash password: {}", e) + }))), + }; + + let new_user = NewUserInsert { + username: req.username.trim().to_string(), + password_hash, + stellar_address: req.stellar_address.trim().to_string(), + email: req.email.clone(), + }; + + match create_user(new_user) { + Ok(user) => Ok(HttpResponse::Created().json(json!({"user": user}))), + Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, info)) => { + Ok(HttpResponse::Conflict().json(json!({ + "error": format!("Unique constraint violation: {}", info.message()) + }))) + } + Err(e) => Ok(HttpResponse::InternalServerError().json(json!({ + "error": format!("Failed to create user: {}", e) + }))), + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FreighterLoginRequest { + pub stellar_address: String, +} + + +#[post("login")] +pub async fn login(req: web::Json) -> Result { + // Basic validation + if req.username.trim().is_empty() || req.password.is_empty() { + return Ok(HttpResponse::BadRequest().json(json!({ + "error": "Username and password are required" + }))); + } + + // Fetch user by username + let user_auth = match find_user_by_username(req.username.trim()) { + Ok(u) => u, + Err(diesel::result::Error::NotFound) => { + return Ok(HttpResponse::Unauthorized().json(json!({ + "error": "Invalid credentials" + }))); + } + Err(e) => { + return Ok(HttpResponse::InternalServerError().json(json!({ + "error": format!("Failed to query user: {}", e) + }))); + } + }; + + if !user_auth.is_active { + return Ok(HttpResponse::Unauthorized().json(json!({ + "error": "Account is inactive" + }))); + } + + // Verify password using Argon2 + let argon2 = Argon2::default(); + let parsed_hash = match PasswordHash::new(&user_auth.password_hash) { + Ok(ph) => ph, + Err(_) => { + return Ok(HttpResponse::InternalServerError().json(json!({ + "error": "Stored password hash is invalid" + }))); + } + }; + + match argon2.verify_password(req.password.as_bytes(), &parsed_hash) { + Ok(_) => { + // Build JWT token on successful password verification + let full_name_str = user_auth.full_name.as_deref().unwrap_or(""); + let token = match generate_jwt( + user_auth.id, + full_name_str, + &user_auth.username, + &user_auth.stellar_address, + Some(JWT_EXPIRATION_TIME), + ) { + Ok(t) => t, + Err(e) => { + return Ok(HttpResponse::InternalServerError().json(json!({ + "error": e + }))); + } + }; + + Ok(HttpResponse::Ok().json(json!({ + "token": token + }))) + } + Err(_) => Ok(HttpResponse::Unauthorized().json(json!({ + "error": "Invalid credentials" + }))), + } +} + +#[get("nonce")] +pub async fn get_nonce() -> HttpResponse { + let nonce = generate_freighter_nonce(); + HttpResponse::Ok().json(json!({"nonce": nonce})) +} + +#[post("login/freighter")] +pub async fn login_freighter(req: web::Json) -> Result { + // Basic validation + if req.stellar_address.trim().is_empty() { + return Ok(HttpResponse::BadRequest().json(json!({ + "error": "stellarAddress is required" + }))); + } + + // Fetch user by stellar address + let user = match get_user_for_auth_by_stellar(req.stellar_address.trim()) { + Ok(u) => u, + Err(diesel::result::Error::NotFound) => { + return Ok(HttpResponse::Unauthorized().json(json!({ + "error": "User not found" + }))); + } + Err(e) => { + return Ok(HttpResponse::InternalServerError().json(json!({ + "error": format!("Failed to query user: {}", e) + }))); + } + }; + + if !user.is_active { + return Ok(HttpResponse::Unauthorized().json(json!({ + "error": "Account is inactive" + }))); + } + + // Build JWT + let full_name_str = user.full_name.as_deref().unwrap_or(""); + let token = match generate_jwt(user.id, full_name_str, &user.username, &user.stellar_address, Some(JWT_EXPIRATION_TIME)) { + Ok(t) => t, + Err(e) => { + return Ok(HttpResponse::InternalServerError().json(json!({ + "error": e + }))); + } + }; + + Ok(HttpResponse::Ok().json(json!({ + "token": token + }))) +} + + diff --git a/src/routes/comment.rs b/src/routes/comment.rs new file mode 100644 index 0000000..efd95d2 --- /dev/null +++ b/src/routes/comment.rs @@ -0,0 +1,18 @@ +use actix_web::{post, web, HttpResponse}; +use chrono::Utc; +use ipfs_api_backend_actix::IpfsClient; +use serde::Deserialize; +use serde_json::json; + +use crate::types::comment::{CommentError, CommentMetadata}; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AppendLocalCommentRequest { + pub thread_id: String, + pub content: String, + pub creator: String, + pub proposal_cid: Option, + pub amendment_cid: Option, + pub parent_comment_cid: Option, +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..bf3911c --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod proposal; +pub(crate) mod amendment; +pub(crate) mod auth; +pub(crate) mod comment; +pub(crate) mod openapi; \ No newline at end of file diff --git a/src/routes/openapi.rs b/src/routes/openapi.rs new file mode 100644 index 0000000..4e67c0b --- /dev/null +++ b/src/routes/openapi.rs @@ -0,0 +1,251 @@ +use actix_web::{get, HttpResponse}; +use serde_json::{json, Value}; + +// Minimal OpenAPI 3.0 spec to enable frontend code generation. +// NOTE: This is intentionally lightweight. We can later replace the manual spec +// with an automatic generator (e.g., Apistos annotations) without changing the endpoint path. + +fn openapi_spec() -> Value { + // Basic schemas for known request bodies + let add_proposal_request = json!({ + "type": "object", + "properties": { + "name": {"type": "string"}, + "description": {"type": "string"}, + "creator": {"type": "string"} + }, + "required": ["name", "description", "creator"], + "additionalProperties": false + }); + + let visit_proposal_request = json!({ + "type": "object", + "properties": { + "proposalId": {"type": "integer", "format": "int32"}, + "path": {"type": "string"}, + "method": {"type": "string"}, + "queryString": {"type": "string"}, + "statusCode": {"type": "integer", "format": "int32"}, + "referrer": {"type": "string"} + }, + "required": ["proposalId"], + "additionalProperties": false + }); + + let register_request = json!({ + "type": "object", + "properties": { + "username": {"type": "string"}, + "password": {"type": "string"}, + "stellarAddress": {"type": "string"}, + "email": {"type": ["string", "null"]} + }, + "required": ["username", "password", "stellarAddress"], + "additionalProperties": false + }); + + let login_request = json!({ + "type": "object", + "properties": { + "username": {"type": "string"}, + "password": {"type": "string"} + }, + "required": ["username", "password"], + "additionalProperties": false + }); + + let freighter_login_request = json!({ + "type": "object", + "properties": { + "stellarAddress": {"type": "string"} + }, + "required": ["stellarAddress"], + "additionalProperties": false + }); + + let proposal_item = json!({ + "type": "object", + "properties": { + "id": {"type": "integer", "format": "int32"}, + "name": {"type": "string"}, + "cid": {"type": "string"}, + "summary": {"type": ["string", "null"]}, + "creator": {"type": "string"}, + "isCurrent": {"type": "boolean"}, + "previousCid": {"type": ["string", "null"]}, + "createdAt": {"type": ["string", "null"]}, + "updatedAt": {"type": ["string", "null"]} + }, + "required": ["id", "name", "cid", "creator", "isCurrent"], + "additionalProperties": false + }); + + let proposal_list = json!({ + "type": "array", + "items": {"$ref": "#/components/schemas/ProposalItem"} + }); + + let list_proposals_response = json!({ + "type": "object", + "properties": { + "proposals": {"$ref": "#/components/schemas/ProposalList"} + } + }); + + // Generic JSON response schema when we don't have strict typing + let generic_object = json!({ + "type": "object", + "additionalProperties": true + }); + + json!({ + "openapi": "3.0.3", + "info": { + "title": "PuffPastry API", + "version": "1.0.0" + }, + "servers": [ + {"url": "http://localhost:7300"} + ], + "paths": { + "/api/v1/proposals/list": { + "get": { + "summary": "List proposals", + "operationId": "listProposals", + "tags": ["Proposal"], + "parameters": [ + {"name": "offset", "in": "query", "required": true, "schema": {"type": "integer", "format": "int64"}}, + {"name": "limit", "in": "query", "required": true, "schema": {"type": "integer", "format": "int64"}} + ], + "responses": { + "200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ListProposalsResponse"}}}} + } + } + }, + "/api/v1/proposal/add": { + "post": { + "summary": "Create a proposal", + "operationId": "createProposal", + "tags": ["Proposal"], + "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AddProposalRequest"}}}}, + "responses": { + "200": {"description": "OK", "content": {"application/json": {"schema": generic_object}}}, + "400": {"description": "Bad Request"} + } + } + }, + "/api/v1/proposal/get": { + "get": { + "summary": "Get a proposal by id", + "operationId": "getProposal", + "tags": ["Proposal"], + "parameters": [ + {"name": "id", "in": "query", "required": true, "schema": {"type": "integer", "format": "int32"}} + ], + "responses": { + "200": {"description": "OK", "content": {"application/json": {"schema": generic_object}}} + } + } + }, + "/api/v1/proposal/visit": { + "post": { + "summary": "Record a proposal visit", + "operationId": "recordProposalVisit", + "tags": ["Proposal"], + "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/VisitProposalRequest"}}}}, + "responses": { + "201": {"description": "Created", "content": {"application/json": {"schema": generic_object}}} + } + } + }, + "/api/v1/amendment/add": { + "post": { + "summary": "Create an amendment", + "operationId": "createAmendment", + "tags": ["Amendment"], + "requestBody": {"required": true, "content": {"application/json": {"schema": generic_object}}}, + "responses": {"200": {"description": "OK"}} + } + }, + "/api/v1/amendment/get": { + "get": { + "summary": "Get an amendment", + "operationId": "getAmendment", + "tags": ["Amendment"], + "parameters": [ + {"name": "id", "in": "query", "required": true, "schema": {"type": "integer", "format": "int32"}} + ], + "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": generic_object}}}} + } + }, + "/api/v1/amendments/list": { + "get": { + "summary": "List amendments", + "operationId": "listAmendments", + "tags": ["Amendment"], + "parameters": [ + {"name": "proposalId", "in": "query", "required": true, "schema": {"type": "integer", "format": "int32"}} + ], + "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": generic_object}}}} + } + }, + "/api/v1/auth/register": { + "post": { + "summary": "Register a user", + "operationId": "registerUser", + "tags": ["Auth"], + "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/RegisterRequest"}}}}, + "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": generic_object}}}, + "400": {"description": "Bad Request"}, + "409": {"description": "Conflict"}} + } + }, + "/api/v1/auth/login": { + "post": { + "summary": "Login with username/password", + "operationId": "loginUser", + "tags": ["Auth"], + "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LoginRequest"}}}}, + "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": generic_object}}}, + "401": {"description": "Unauthorized"}} + } + }, + "/api/v1/auth/nonce": { + "get": { + "summary": "Get freighter login nonce", + "operationId": "getFreighterNonce", + "tags": ["Auth"], + "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": generic_object}}}} + } + }, + "/api/v1/auth/login/freighter": { + "post": { + "summary": "Login with Stellar (Freighter)", + "operationId": "loginFreighter", + "tags": ["Auth"], + "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/FreighterLoginRequest"}}}}, + "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": generic_object}}}, + "401": {"description": "Unauthorized"}} + } + } + }, + "components": { + "schemas": { + "AddProposalRequest": add_proposal_request, + "VisitProposalRequest": visit_proposal_request, + "RegisterRequest": register_request, + "LoginRequest": login_request, + "FreighterLoginRequest": freighter_login_request, + "ListProposalsResponse": list_proposals_response, + "ProposalItem": proposal_item, + "ProposalList": proposal_list + } + } + }) +} + +#[get("/openapi.json")] +pub async fn get_openapi() -> HttpResponse { + let spec = openapi_spec(); + HttpResponse::Ok().json(spec) +} diff --git a/src/routes/proposal.rs b/src/routes/proposal.rs new file mode 100644 index 0000000..81e3aa3 --- /dev/null +++ b/src/routes/proposal.rs @@ -0,0 +1,157 @@ +use crate::ipfs::ipfs::IpfsService; +use crate::ipfs::proposal::ProposalService; +use crate::repositories::proposal::{get_cid_for_proposal, paginate_proposals}; +use crate::repositories::visit::record_visit; +use crate::types::proposal::ProposalError; +use crate::types::proposal::ProposalFile; +use actix_web::{get, post, web, HttpResponse, HttpRequest}; +use chrono::Utc; +use ipfs_api_backend_actix::IpfsClient; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +#[derive(Deserialize)] +struct PaginationParams { + offset: i64, + limit: i64, +} +#[get("list")] +async fn list_proposals(params: web::Query) -> Result { + match paginate_proposals(params.offset, params.limit) { + Ok(proposals) => Ok(HttpResponse::Ok().json(json!({"proposals": proposals}))), + Err(err) => Ok(HttpResponse::InternalServerError().json(json!({ + "error": format!("Failed to fetch proposals: {}", err) + }))) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AddProposalRequest { + pub name: String, + pub description: String, + pub creator: String, +} +#[post("/add")] +async fn add_proposal( + request: web::Json, +) -> Result { + let req = request.into_inner(); + + // Basic input validation + if req.creator.trim().is_empty() { + return Ok(HttpResponse::BadRequest().json(json!({"error": "Creator is required"}))); + } + if req.name.trim().is_empty() { + return Ok(HttpResponse::BadRequest().json(json!({"error": "Name is required"}))); + } + if req.description.trim().is_empty() { + return Ok(HttpResponse::BadRequest().json(json!({"error": "Description is required"}))); + } + + match create_and_store_proposal(&req).await { + Ok(cid) => Ok(HttpResponse::Ok().json(json!({ "cid": cid }))), + Err(e) => { + match e { + ProposalError::SerializationError(err) => Ok(HttpResponse::BadRequest().json(json!({ + "error": format!("Invalid proposal payload: {}", err) + }))), + _ => Ok(HttpResponse::InternalServerError().json(json!({ + "error": format!("Failed to create proposal: {}", e) + }))), + } + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VisitProposalRequest { + pub proposal_id: i32, + pub path: Option, + pub method: Option, + pub query_string: Option, + pub status_code: Option, + pub referrer: Option, +} + +#[post("visit")] +async fn visit_proposal(req: HttpRequest, payload: web::Json) -> Result { + let body = payload.into_inner(); + if body.proposal_id <= 0 { + return Ok(HttpResponse::BadRequest().json(json!({"error": "proposalId must be a positive integer"}))); + } + + // Take analytics values from the request body (fallback to request path/method to satisfy NOT NULL columns) + let path_owned = body.path.unwrap_or_else(|| req.path().to_string()); + let method_owned = body.method.unwrap_or_else(|| req.method().to_string()); + let query_string_opt = body.query_string; // Option + let status_code_val = body.status_code; // Option + let referrer_opt = body.referrer; // Option + + let connection_info = req.connection_info(); + let ip_address_val = connection_info.realip_remote_addr().map(|s| s.to_string()); + let user_agent_val = req.headers().get("User-Agent").and_then(|v| v.to_str().ok()).map(|s| s.to_string()); + let session_id_val = req.headers().get("X-Session-Id").and_then(|v| v.to_str().ok()).map(|s| s.to_string()); + let request_id_val = req.headers().get("X-Request-Id").and_then(|v| v.to_str().ok()).map(|s| s.to_string()); + + // No authenticated user context available here; record as anonymous + let user_id_val: Option = None; + + match record_visit( + body.proposal_id, + user_id_val, + &path_owned, + &method_owned, + query_string_opt.as_deref(), + status_code_val, + ip_address_val.as_deref(), + user_agent_val.as_deref(), + referrer_opt.as_deref(), + session_id_val.as_deref(), + request_id_val.as_deref(), + ) { + Ok(_) => Ok(HttpResponse::Created().json(json!({"ok": true}))), + Err(err) => Ok(HttpResponse::InternalServerError().json(json!({"error": format!("Failed to record visit: {}", err)}))), + } +} + +#[derive(Deserialize)] +struct GetProposalParams { + pub id: i32, +} +#[get("get")] +async fn get_proposal(params: web::Query) -> Result { + let mut proposal_client = ProposalService::new(IpfsClient::default()); + let cid = match get_cid_for_proposal(params.id) { + Ok(cid) => cid, + Err(err) => return Ok(HttpResponse::InternalServerError().json(json!({ + "error": format!("Failed to fetch proposal CID: {}", err) + }))) + }; + let item = proposal_client.read(cid).await; + match item { + Ok(proposal) => Ok(HttpResponse::Ok().json(json!({"proposal": proposal}))), + Err(e) => Ok(HttpResponse::InternalServerError().json(json!({ + "error": format!("Failed to read proposal: {}", e) + }))) + } +} + +async fn create_and_store_proposal( + request: &AddProposalRequest, +) -> Result { + let proposal_data = ProposalFile { + name: request.name.to_string(), + content: request.description.to_string(), + creator: request.creator.to_string(), + created_at: Utc::now().naive_utc(), + updated_at: Utc::now().naive_utc(), + }; + + let client = IpfsClient::default(); + let mut proposal_service = ProposalService::new(client); + + let cid = proposal_service.save(proposal_data).await?; + Ok(cid) +} \ No newline at end of file diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..47bd39d --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,100 @@ +// @generated automatically by Diesel CLI. + +pub mod sql_types { + #[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "amendment_status"))] + pub struct AmendmentStatus; +} + +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::AmendmentStatus; + + amendments (id) { + id -> Int4, + name -> Text, + cid -> Text, + summary -> Nullable, + status -> AmendmentStatus, + creator -> Text, + is_current -> Bool, + previous_cid -> Nullable, + created_at -> Timestamptz, + updated_at -> Timestamptz, + proposal_id -> Int4, + } +} + +diesel::table! { + comments (id) { + id -> Int4, + cid -> Text, + is_current -> Bool, + previous_cid -> Nullable, + proposal_id -> Int4, + created_at -> Timestamptz, + updated_at -> Timestamptz, + } +} + +diesel::table! { + proposals (id) { + id -> Int4, + name -> Text, + cid -> Text, + summary -> Nullable, + creator -> Text, + is_current -> Bool, + previous_cid -> Nullable, + created_at -> Nullable, + updated_at -> Nullable, + } +} + +diesel::table! { + users (id) { + id -> Int4, + username -> Text, + password_hash -> Text, + stellar_address -> Text, + email -> Nullable, + is_active -> Bool, + roles -> Array>, + last_login_at -> Nullable, + created_at -> Timestamptz, + updated_at -> Timestamptz, + full_name -> Nullable, + } +} + +diesel::table! { + visits (id) { + id -> Int4, + user_id -> Nullable, + proposal_id -> Int4, + path -> Text, + method -> Text, + query_string -> Nullable, + status_code -> Nullable, + ip_address -> Nullable, + user_agent -> Nullable, + referrer -> Nullable, + session_id -> Nullable, + request_id -> Nullable, + visited_at -> Timestamptz, + created_at -> Timestamptz, + updated_at -> Timestamptz, + } +} + +diesel::joinable!(amendments -> proposals (proposal_id)); +diesel::joinable!(comments -> proposals (proposal_id)); +diesel::joinable!(visits -> users (user_id)); + +diesel::allow_tables_to_appear_in_same_query!( + amendments, + comments, + proposals, + users, + visits, +); diff --git a/src/types/amendment.rs b/src/types/amendment.rs new file mode 100644 index 0000000..b707adf --- /dev/null +++ b/src/types/amendment.rs @@ -0,0 +1,117 @@ +use core::fmt; +use crate::schema::amendments; +use chrono::NaiveDateTime; +use diesel::{Insertable, Queryable, Selectable}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug)] +pub enum AmendmentError { + DatabaseError(Box), + IpfsError(Box), + SerializationError(serde_json::Error), +} + +impl fmt::Display for AmendmentError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AmendmentError::DatabaseError(e) => write!(f, "Database error: {}", e), + AmendmentError::IpfsError(e) => write!(f, "Ipfs error: {}", e), + AmendmentError::SerializationError(e) => write!(f, "Serialization error: {}", e), + } + } +} + +#[derive(Queryable, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AmendmentFile { + pub name: String, + pub content: String, + pub creator: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub proposal_id: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize, diesel_derive_enum::DbEnum)] +#[db_enum(existing_type_path = "crate::schema::sql_types::AmendmentStatus")] +pub enum AmendmentStatus { + Proposed, + Approved, + Withdrawn, + Rejected, +} + +#[derive(Queryable, Selectable, Clone, Serialize, Deserialize)] +#[diesel(table_name = amendments)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[serde(rename_all = "camelCase")] +pub struct SelectableAmendment { + pub id: i32, + pub name: String, + pub cid: String, + pub summary: Option, + pub status: AmendmentStatus, + pub creator: String, + pub is_current: bool, + pub previous_cid: Option, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub proposal_id: i32, +} + +#[derive(Insertable, Clone, Serialize, Deserialize)] +#[diesel(table_name = amendments)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[serde(rename_all = "camelCase")] +pub struct Amendment { + pub name: String, + pub cid: Option, + pub summary: Option, + pub status: AmendmentStatus, + pub creator: String, + pub is_current: bool, + pub previous_cid: Option, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub proposal_id: i32, +} + +impl Amendment { + pub fn new(name: String, summary: Option, creator: String, proposal_id: i32) -> Self { + Amendment { + name, + summary, + status: AmendmentStatus::Proposed, + creator, + is_current: true, + previous_cid: None, + created_at: chrono::Utc::now().naive_utc(), + updated_at: chrono::Utc::now().naive_utc(), + proposal_id, + cid: None, + } + } + + pub fn with_cid(mut self, cid: String) -> Self { + self.cid = Some(cid); + self + } + + pub fn mark_as_current(mut self) -> Self { + self.is_current = true; + self.updated_at = chrono::Utc::now().naive_utc(); + self + } +} + +impl From for AmendmentError { + fn from(e: serde_json::Error) -> Self { + AmendmentError::SerializationError(e) + } +} + +impl From for AmendmentError { + fn from(e: std::io::Error) -> Self { + AmendmentError::IpfsError(Box::new(e)) + } +} \ No newline at end of file diff --git a/src/types/comment.rs b/src/types/comment.rs new file mode 100644 index 0000000..25d2fd1 --- /dev/null +++ b/src/types/comment.rs @@ -0,0 +1,50 @@ +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(Debug)] +pub enum CommentError { + IpfsError(Box), + SerializationError(serde_json::Error), +} + +impl fmt::Display for CommentError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CommentError::IpfsError(e) => write!(f, "IPFS error: {}", e), + CommentError::SerializationError(e) => write!(f, "Serialization error: {}", e), + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommentMetadata { + pub content: String, + pub creator: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommentFile { + pub comments: Vec, + // Optional association fields to link this comment to a target entity + pub proposal_cid: Option, + pub amendment_cid: Option, + // Optional parent comment for threads + pub parent_comment_cid: Option, +} + +impl From for CommentError { + fn from(e: serde_json::Error) -> Self { + CommentError::SerializationError(e) + } +} + +impl From for CommentError { + fn from(e: std::io::Error) -> Self { + CommentError::IpfsError(Box::new(e)) + } +} diff --git a/src/types/ipfs.rs b/src/types/ipfs.rs new file mode 100644 index 0000000..c04a2c7 --- /dev/null +++ b/src/types/ipfs.rs @@ -0,0 +1,3 @@ +// Make IpfsResult reusable by allowing a generic error type with a default of ProposalError. +pub type IpfsResult = Result; + diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..3b6ca70 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod proposal; +pub(crate) mod amendment; +pub(crate) mod ipfs; +pub(crate) mod comment; \ No newline at end of file diff --git a/src/types/proposal.rs b/src/types/proposal.rs new file mode 100644 index 0000000..545e877 --- /dev/null +++ b/src/types/proposal.rs @@ -0,0 +1,127 @@ +use crate::schema::proposals; +use chrono::NaiveDateTime; +use diesel::Insertable; +use diesel::Queryable; +use diesel::Selectable; +use serde::de::StdError; +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(Debug)] +pub enum ProposalError { + ProposalNotFound, + DatabaseError(Box), + IpfsError(Box), + SerializationError(serde_json::Error), +} + +impl fmt::Display for ProposalError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ProposalError::ProposalNotFound => write!(f, "Proposal not found"), + ProposalError::DatabaseError(e) => write!(f, "Database error: {}", e), + ProposalError::IpfsError(e) => write!(f, "IPFS error: {}", e), + ProposalError::SerializationError(e) => write!(f, "Serialization error: {}", e), + } + } +} + +impl StdError for ProposalError { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + ProposalError::ProposalNotFound => None, + ProposalError::DatabaseError(e) => Some(e.as_ref()), + ProposalError::IpfsError(e) => Some(e.as_ref()), + ProposalError::SerializationError(e) => Some(e), + } + } +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ProposalFile { + pub name: String, + pub content: String, + pub creator: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, +} + +#[derive(Queryable, Selectable, Clone, Serialize, Deserialize)] +#[diesel(table_name = proposals)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[serde(rename_all = "camelCase")] +pub struct SelectableProposal { + pub id: i32, + pub name: String, + pub cid: String, + pub summary: Option, + pub creator: String, + pub is_current: bool, + pub previous_cid: Option, + pub created_at: Option, + pub updated_at: Option, +} + +#[derive(Insertable, Clone, Serialize, Deserialize)] +#[diesel(table_name = proposals)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[serde(rename_all = "camelCase")] +pub struct Proposal { + pub name: String, + pub cid: Option, + pub summary: Option, + pub creator: String, + pub is_current: bool, + pub previous_cid: Option, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, +} + +impl Proposal { + pub fn new(name: String, summary: Option, creator: String) -> Self { + Self { + name, + cid: None, + creator, + summary, + is_current: false, + previous_cid: None, + created_at: chrono::Local::now().naive_local(), + updated_at: chrono::Local::now().naive_local(), + } + } + + pub fn with_cid(mut self, cid: String) -> Self { + self.cid = Some(cid); + self + } + + pub fn with_summary(mut self, summary: String) -> Self { + self.summary = Some(summary); + self + } + + pub fn with_previous_cid(mut self, previous_cid: String) -> Self { + self.previous_cid = Some(previous_cid); + self + } + + pub fn mark_as_current(mut self) -> Self { + self.is_current = true; + self.updated_at = chrono::Local::now().naive_local(); + self + } +} + +impl From for ProposalError { + fn from(e: serde_json::Error) -> Self { + ProposalError::SerializationError(e) + } +} + +impl From for ProposalError { + fn from(e: std::io::Error) -> Self { + ProposalError::IpfsError(Box::new(e)) + } +} \ No newline at end of file diff --git a/src/utils/auth.rs b/src/utils/auth.rs new file mode 100644 index 0000000..3a125f4 --- /dev/null +++ b/src/utils/auth.rs @@ -0,0 +1,85 @@ +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD as BASE64; +use ed25519_dalek::{Signature, VerifyingKey, Verifier}; +use serde::{Deserialize, Serialize}; +use stellar_strkey::ed25519::PublicKey as StellarPublicKey; +use std::env; +use chrono::{Utc, Duration}; +use jsonwebtoken::{encode, Header, EncodingKey}; + +/// Verify a Freighter-provided ed25519 signature over a message using a Stellar G... address. +/// +/// Returns Ok(()) if valid; otherwise returns Err with a human-readable reason. +pub fn verify_freighter_signature(stellar_address: &str, message: &str, signature_b64: &str) -> Result<(), String> { + // Decode Stellar G... address to 32-byte ed25519 public key + let pk = StellarPublicKey::from_string(stellar_address) + .map_err(|e| format!("Invalid stellarAddress: {}", e))?; + let pk_bytes = pk.0; + + // Build verifying key + let vkey = VerifyingKey::from_bytes(&pk_bytes) + .map_err(|e| format!("Invalid public key: {}", e))?; + + // Decode signature from base64 + let sig_bytes = BASE64.decode(signature_b64) + .map_err(|e| format!("Invalid signature encoding: {}", e))?; + let sig = Signature::from_slice(&sig_bytes) + .map_err(|e| format!("Invalid signature: {}", e))?; + + // Verify + vkey.verify(message.as_bytes(), &sig) + .map_err(|_| "Signature verification failed".to_string()) +} + +/// Generate a cryptographically-random nonce for Freighter to sign on the frontend. +/// +/// Notes: +/// - This returns a UUID v4 string (e.g., 550e8400-e29b-41d4-a716-446655440000), +/// which has 122 bits of randomness and is suitable as a nonce. +/// - You should bind this nonce to a short-lived challenge server-side (e.g., with an expiry) +/// and verify the signed nonce/message using `verify_freighter_signature` before issuing JWTs. +pub fn generate_freighter_nonce() -> String { + uuid::Uuid::new_v4().to_string() +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Claims { + // Standard-like claims + exp: i64, + iat: i64, + // Custom claims + user_id: i32, + full_name: String, + username: String, + stellar_address: String, +} + +/// Generate a JWT embedding the user's id, full name, username, and stellar address. +/// +/// The signing secret is read from the JWT_SECRET environment variable. +/// The token uses HS256 by default and expires in `expiration_minutes` (default: 60 minutes). +pub fn generate_jwt( + user_id: i32, + full_name: &str, + username: &str, + stellar_address: &str, + expiration_minutes: Option, +) -> Result { + let secret = env::var("JWT_SECRET") + .map_err(|_| "JWT_SECRET not set".to_string())?; + + let now = Utc::now(); + let exp_minutes = expiration_minutes.unwrap_or(60); + let claims = Claims { + exp: (now + Duration::minutes(exp_minutes)).timestamp(), + iat: now.timestamp(), + user_id, + full_name: full_name.to_string(), + username: username.to_string(), + stellar_address: stellar_address.to_string(), + }; + + encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes())) + .map_err(|e| format!("Failed to sign JWT: {}", e)) +} diff --git a/src/utils/content.rs b/src/utils/content.rs new file mode 100644 index 0000000..dfdd1fd --- /dev/null +++ b/src/utils/content.rs @@ -0,0 +1,7 @@ +pub fn extract_summary(content: &str) -> String { + content + .split('\n') + .next() + .unwrap_or_default() + .to_string() +} \ No newline at end of file diff --git a/src/utils/env.rs b/src/utils/env.rs new file mode 100644 index 0000000..3d4a2dc --- /dev/null +++ b/src/utils/env.rs @@ -0,0 +1,16 @@ +/// Utilities for loading environment variables from a local .env file using dotenvy. +/// +/// Call this early in your application (e.g., at the start of main) so that +/// subsequent `std::env::var` calls can read variables defined in .env. +pub fn load_env() { + // Try to load from default .env path. It's common to ignore a missing file + // so that environments that rely on real environment variables (e.g., prod) + // don't fail to start. + if let Err(err) = dotenvy::dotenv() { + // If the file is not found, silently continue; otherwise print a warning. + // This keeps behavior non-fatal while still surfacing unexpected errors. + if !matches!(err, dotenvy::Error::Io(ref io_err) if io_err.kind() == std::io::ErrorKind::NotFound) { + eprintln!("Warning: failed to load .env: {err}"); + } + } +} diff --git a/src/utils/ipfs.rs b/src/utils/ipfs.rs new file mode 100644 index 0000000..b4a482a --- /dev/null +++ b/src/utils/ipfs.rs @@ -0,0 +1,148 @@ +use ipfs_api_backend_actix::{IpfsApi, IpfsClient}; +use serde::{de::DeserializeOwned, Serialize}; +use std::io::{Cursor, Error as IoError, ErrorKind}; +use std::path::Path; + +use crate::types::ipfs::IpfsResult; + +pub fn create_file_path(base_dir: &str, extension: &str) -> String { + let filename = format!("{}.{}", uuid::Uuid::new_v4(), extension); + let path = Path::new(base_dir).join(filename); + path.to_string_lossy().into_owned() +} + +pub async fn create_storage_directory(client: &IpfsClient, dir: &str) -> IpfsResult<(), E> +where + E: From, +{ + client + .files_mkdir(dir, true) + .await + .map_err(|e| E::from(IoError::new(ErrorKind::Other, format!("IPFS mkdir error: {}", e))))?; + Ok(()) +} + +pub async fn save_json_file( + client: &IpfsClient, + path: &str, + data: &T, +) -> IpfsResult<(), E> +where + T: Serialize, + E: From + From, +{ + let json = serde_json::to_string::(data)?; + let file_content = Cursor::new(json.into_bytes()); + + client + .files_write(path, true, true, file_content) + .await + .map_err(|e| E::from(IoError::new(ErrorKind::Other, format!("IPFS write error: {}", e))))?; + Ok(()) +} + +pub async fn read_json_via_cat( + client: &IpfsClient, + hash: &str, + max_size: usize, +) -> IpfsResult +where + T: DeserializeOwned, + E: From + From, +{ + let stream = client.cat(hash); + let mut content = Vec::with_capacity(1024); + let mut total_size = 0; + + futures::pin_mut!(stream); + + while let Some(chunk) = futures::StreamExt::next(&mut stream).await { + let chunk = chunk.map_err(|e| E::from(IoError::new( + ErrorKind::Other, + format!("Failed to read IPFS chunk: {}", e), + )))?; + + total_size += chunk.len(); + if total_size > max_size { + return Err(E::from(IoError::new( + ErrorKind::Other, + "File exceeds maximum allowed size", + ))); + } + + content.extend_from_slice(&chunk); + } + + if content.is_empty() { + return Err(E::from(IoError::new( + ErrorKind::Other, + "Empty response from IPFS", + ))); + } + + let value = serde_json::from_slice::(&content)?; + Ok(value) +} + +pub async fn retrieve_content_hash(client: &IpfsClient, path: &str) -> IpfsResult +where + E: From, +{ + let stat = client + .files_stat(path) + .await + .map_err(|e| E::from(IoError::new(ErrorKind::Other, format!("IPFS stat error: {}", e))))?; + Ok(stat.hash) +} + +pub const DEFAULT_MAX_JSON_SIZE: usize = 10 * 1024 * 1024; + +/// List the child entries of an IPFS directory given its CID/hash and +/// return the CIDs of child files. +pub async fn list_directory_file_hashes(client: &IpfsClient, dir_hash: &str) -> IpfsResult, E> +where + E: From, +{ + // Use `ls` which works with CIDs (not only MFS paths) + let resp = client + .ls(dir_hash) + .await + .map_err(|e| E::from(IoError::new( + ErrorKind::Other, + format!("IPFS ls error: {}", e), + )))?; + + // Collect links from the first (and usually only) object + let mut file_hashes: Vec = Vec::new(); + for obj in resp.objects { + for link in obj.links { + // The response type typically has `typ` as a numeric code (2 for file) or a string. + // To remain compatible without depending on exact type semantics, we accept all links + // and rely on subsequent JSON parsing to validate. Optionally, filter by name extension. + if link.name.ends_with(".json") { + file_hashes.push(link.hash.clone()); + } else { + // Still include; some JSON files may not follow extension naming in IPFS. + file_hashes.push(link.hash.clone()); + } + } + } + + Ok(file_hashes) +} + +pub async fn upload_json_and_get_hash( + client: &IpfsClient, + storage_dir: &str, + file_extension: &str, + data: &T, +) -> IpfsResult +where + T: Serialize, + E: From + From, +{ + let file_path = create_file_path(storage_dir, file_extension); + create_storage_directory::(client, storage_dir).await?; + save_json_file::(client, &file_path, data).await?; + retrieve_content_hash::(client, &file_path).await +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..f8285c9 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod content; +pub(crate) mod ipfs; +pub(crate) mod auth; +pub(crate) mod env; \ No newline at end of file