diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f126cf..11f9724 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,26 +86,40 @@ jobs: key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo- + + - name: Run migrations + run: | + cd backend + cargo install sqlx-cli --no-default-features --features postgres,native-tls --locked + sqlx migrate run + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/guild_genesis_test - name: Install dependencies run: | cd backend - cargo build --verbose + cargo build + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/guild_genesis_test - name: Run clippy run: | cd backend cargo clippy --all-targets --all-features -- -D warnings + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/guild_genesis_test - name: Run rustfmt run: | cd backend cargo fmt --all -- --check + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/guild_genesis_test - name: Run tests run: | cd backend - cargo test --verbose + cargo test env: DATABASE_URL: postgres://postgres:postgres@localhost:5432/guild_genesis_test TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/guild_genesis_test @@ -202,7 +216,7 @@ jobs: - name: Run integration tests run: | cd backend - cargo test --test integration_tests --verbose + cargo test --test integration_tests env: DATABASE_URL: postgres://postgres:postgres@localhost:5432/guild_genesis_test TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/guild_genesis_test diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 5302f27..7f7ef7d 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -38,19 +38,6 @@ dependencies = [ "cpufeatures", ] -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.3", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -250,6 +237,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.8.0" @@ -496,6 +489,15 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const-hex" version = "1.15.0" @@ -1209,9 +1211,14 @@ dependencies = [ [[package]] name = "event-listener" -version = "2.5.3" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] [[package]] name = "eyre" @@ -1290,6 +1297,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1559,20 +1572,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "ahash", "allocator-api2", + "equivalent", + "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" - [[package]] name = "hashers" version = "1.0.1" @@ -1584,20 +1592,11 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.5", -] - -[[package]] -name = "heck" -version = "0.4.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "unicode-segmentation", + "hashbrown", ] [[package]] @@ -1767,7 +1766,7 @@ dependencies = [ "futures-util", "http 0.2.12", "hyper 0.14.32", - "rustls", + "rustls 0.21.12", "tokio", "tokio-rustls", ] @@ -1970,7 +1969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown", ] [[package]] @@ -2154,9 +2153,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.27.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", @@ -2234,12 +2233,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2266,16 +2259,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nu-ansi-term" version = "0.50.1" @@ -2454,6 +2437,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.4" @@ -2488,12 +2477,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "path-slash" version = "0.2.1" @@ -2936,7 +2919,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls", + "rustls 0.21.12", "rustls-pemfile", "serde", "serde_json", @@ -2950,7 +2933,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 0.25.4", "winreg", ] @@ -3087,10 +3070,24 @@ checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring 0.17.14", - "rustls-webpki", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "once_cell", + "ring 0.17.14", + "rustls-pki-types", + "rustls-webpki 0.103.4", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -3100,6 +3097,15 @@ dependencies = [ "base64 0.21.7", ] +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -3110,6 +3116,17 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring 0.17.14", + "rustls-pki-types", + "untrusted 0.9.0", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -3401,6 +3418,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -3461,21 +3481,11 @@ dependencies = [ "der", ] -[[package]] -name = "sqlformat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" -dependencies = [ - "nom", - "unicode_categories", -] - [[package]] name = "sqlx" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ "sqlx-core", "sqlx-macros", @@ -3486,70 +3496,64 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "ahash", - "atoi", - "byteorder", + "base64 0.22.1", "bytes", "chrono", "crc", "crossbeam-queue", "either", "event-listener", - "futures-channel", "futures-core", "futures-intrusive", "futures-io", "futures-util", + "hashbrown", "hashlink", - "hex", "indexmap", "log", "memchr", "once_cell", - "paste", "percent-encoding", - "rustls", - "rustls-pemfile", + "rustls 0.23.31", "serde", "serde_json", "sha2", "smallvec", - "sqlformat", - "thiserror 1.0.69", + "thiserror 2.0.16", "tokio", "tokio-stream", "tracing", "url", "uuid 1.18.1", - "webpki-roots", + "webpki-roots 0.26.11", ] [[package]] name = "sqlx-macros" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 1.0.109", + "syn 2.0.106", ] [[package]] name = "sqlx-macros-core" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "dotenvy", "either", - "heck 0.4.1", + "heck", "hex", "once_cell", "proc-macro2", @@ -3561,20 +3565,19 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 1.0.109", - "tempfile", + "syn 2.0.106", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64 0.21.7", + "base64 0.22.1", "bitflags 2.9.4", "byteorder", "bytes", @@ -3605,7 +3608,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 1.0.69", + "thiserror 2.0.16", "tracing", "uuid 1.18.1", "whoami", @@ -3613,12 +3616,12 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64 0.21.7", + "base64 0.22.1", "bitflags 2.9.4", "byteorder", "chrono", @@ -3627,7 +3630,6 @@ dependencies = [ "etcetera", "futures-channel", "futures-core", - "futures-io", "futures-util", "hex", "hkdf", @@ -3645,7 +3647,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 1.0.69", + "thiserror 2.0.16", "tracing", "uuid 1.18.1", "whoami", @@ -3653,9 +3655,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "chrono", @@ -3669,10 +3671,11 @@ dependencies = [ "log", "percent-encoding", "serde", + "serde_urlencoded", "sqlx-core", + "thiserror 2.0.16", "tracing", "url", - "urlencoding", "uuid 1.18.1", ] @@ -3726,7 +3729,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "rustversion", @@ -4005,7 +4008,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls", + "rustls 0.21.12", "tokio", ] @@ -4028,11 +4031,11 @@ checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", - "rustls", + "rustls 0.21.12", "tokio", "tokio-rustls", "tungstenite", - "webpki-roots", + "webpki-roots 0.25.4", ] [[package]] @@ -4240,7 +4243,7 @@ dependencies = [ "httparse", "log", "rand 0.8.5", - "rustls", + "rustls 0.21.12", "sha1", "thiserror 1.0.69", "url", @@ -4298,24 +4301,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - [[package]] name = "untrusted" version = "0.7.1" @@ -4340,12 +4331,6 @@ dependencies = [ "serde", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "utf-8" version = "0.7.6" @@ -4526,6 +4511,24 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.2", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "whoami" version = "1.6.1" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index e875d35..77a4f14 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -3,14 +3,6 @@ name = "guild-backend" version = "0.1.0" edition = "2021" -[[bin]] -name = "guild-backend" -path = "src/main.rs" - -[[bin]] -name = "migrate" -path = "src/bin/migrate.rs" - [dependencies] # Web framework axum = { version = "0.7", features = ["macros"] } @@ -21,7 +13,7 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } # Database -sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "sqlite", "any", "chrono", "uuid"] } +sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "sqlite", "any", "chrono", "uuid"] } async-trait = "0.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/backend/migrations/001_initial_schema.sql b/backend/migrations/001_initial_schema.sql index 84e255e..a495824 100644 --- a/backend/migrations/001_initial_schema.sql +++ b/backend/migrations/001_initial_schema.sql @@ -1,67 +1,8 @@ --- Create users table -CREATE TABLE users ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - wallet_address VARCHAR(42) UNIQUE NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - --- Create profiles table CREATE TABLE profiles ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + address VARCHAR(255) PRIMARY KEY, name VARCHAR(255), description TEXT, avatar_url TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - UNIQUE(user_id) -); - --- Create badges table -CREATE TABLE badges ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(255) NOT NULL, - description TEXT, - issuer_address VARCHAR(42) NOT NULL, - image_url TEXT, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); - --- Create user_badges table (many-to-many between users and badges) -CREATE TABLE user_badges ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - badge_id UUID NOT NULL REFERENCES badges(id) ON DELETE CASCADE, - awarded_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - awarded_by VARCHAR(42) NOT NULL, - UNIQUE(user_id, badge_id, awarded_by) -); - --- Create indexes for better performance -CREATE INDEX idx_users_wallet_address ON users(wallet_address); -CREATE INDEX idx_profiles_user_id ON profiles(user_id); -CREATE INDEX idx_badges_name ON badges(name); -CREATE INDEX idx_badges_issuer ON badges(issuer_address); -CREATE INDEX idx_user_badges_user ON user_badges(user_id); -CREATE INDEX idx_user_badges_badge ON user_badges(badge_id); - --- Create updated_at trigger function -CREATE OR REPLACE FUNCTION update_updated_at_column() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ language 'plpgsql'; - --- Add triggers for updated_at -CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users - FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - -CREATE TRIGGER update_profiles_updated_at BEFORE UPDATE ON profiles - FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - -CREATE TRIGGER update_badges_updated_at BEFORE UPDATE ON badges - FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); \ No newline at end of file diff --git a/backend/src/application/commands/create_profile.rs b/backend/src/application/commands/create_profile.rs index 98a17dd..d79f842 100644 --- a/backend/src/application/commands/create_profile.rs +++ b/backend/src/application/commands/create_profile.rs @@ -1,33 +1,19 @@ -use crate::application::dtos::profile_dtos::{ - CreateProfileRequest, ProfileListResponse, ProfileResponse, UpdateProfileRequest, -}; +use crate::application::dtos::profile_dtos::{CreateProfileRequest, ProfileResponse}; +use crate::domain::entities::profile::Profile; use crate::domain::repositories::profile_repository::ProfileRepository; -use crate::domain::services::AuthService; use crate::domain::value_objects::wallet_address::WalletAddress; use std::sync::Arc; -#[derive(Clone)] -pub struct ProfileApplicationService { - profile_repository: Arc, - auth_service: Arc, -} - pub async fn create_profile( - &self, + profile_repository: Arc, + address: String, request: CreateProfileRequest, ) -> Result { - let wallet_address = WalletAddress::new(request.address).map_err(|e| e.to_string())?; - let user = self - .user_repository - .find_by_wallet_address(&wallet_address.to_string()) - .await - .map_err(|e| e.to_string())? - .ok_or("User not found")?; + let wallet_address = WalletAddress::new(address).map_err(|e| e.to_string())?; // Check if profile already exists - if self - .profile_repository - .find_by_user_id(&user.id) + if profile_repository + .find_by_address(&wallet_address) .await .map_err(|e| e.to_string())? .is_some() @@ -35,17 +21,16 @@ pub async fn create_profile( return Err("Profile already exists for this user".to_string()); } - let mut profile = crate::domain::entities::profile::Profile::new(user.id); + let mut profile = Profile::new(wallet_address.clone()); profile.update_info(Some(request.name), request.description, request.avatar_url); - self.profile_repository + profile_repository .create(&profile) .await .map_err(|e| e.to_string())?; Ok(ProfileResponse { - id: profile.id, - user_id: profile.user_id, + address: wallet_address, name: profile.name.unwrap_or_default(), description: profile.description, avatar_url: profile.avatar_url, diff --git a/backend/src/application/commands/get_profile.rs b/backend/src/application/commands/get_profile.rs index f131b2c..dee97f2 100644 --- a/backend/src/application/commands/get_profile.rs +++ b/backend/src/application/commands/get_profile.rs @@ -1,36 +1,22 @@ -use crate::application::dtos::profile_dtos::{ - CreateProfileRequest, ProfileListResponse, ProfileResponse, UpdateProfileRequest, -}; +use crate::application::dtos::profile_dtos::ProfileResponse; use crate::domain::repositories::profile_repository::ProfileRepository; -use crate::domain::services::AuthService; use crate::domain::value_objects::wallet_address::WalletAddress; use std::sync::Arc; -#[derive(Clone)] -pub struct ProfileApplicationService { - profile_repository: Arc, - auth_service: Arc, -} - -pub async fn GetProfile(&self, address: String) -> Result { +pub async fn get_profile( + profile_repository: Arc, + address: String, +) -> Result { let wallet_address = WalletAddress::new(address).map_err(|e| e.to_string())?; - let user = self - .user_repository - .find_by_wallet_address(&wallet_address.to_string()) - .await - .map_err(|e| e.to_string())? - .ok_or("User not found")?; - let profile = self - .profile_repository - .find_by_user_id(&user.id) + let profile = profile_repository + .find_by_address(&wallet_address) .await .map_err(|e| e.to_string())? .ok_or("Profile not found")?; Ok(ProfileResponse { - id: profile.id, - user_id: profile.user_id, + address: wallet_address, name: profile.name.unwrap_or_default(), description: profile.description, avatar_url: profile.avatar_url, diff --git a/backend/src/application/commands/update_profile.rs b/backend/src/application/commands/update_profile.rs index 691dbe7..4ab5b3c 100644 --- a/backend/src/application/commands/update_profile.rs +++ b/backend/src/application/commands/update_profile.rs @@ -1,46 +1,29 @@ -use crate::application::dtos::profile_dtos::{ - CreateProfileRequest, ProfileListResponse, ProfileResponse, UpdateProfileRequest, -}; +use crate::application::dtos::profile_dtos::{ProfileResponse, UpdateProfileRequest}; use crate::domain::repositories::profile_repository::ProfileRepository; -use crate::domain::services::AuthService; use crate::domain::value_objects::wallet_address::WalletAddress; use std::sync::Arc; -#[derive(Clone)] -pub struct ProfileApplicationService { - profile_repository: Arc, - auth_service: Arc, -} - pub async fn update_profile( - &self, + profile_repository: Arc, address: String, request: UpdateProfileRequest, ) -> Result { let wallet_address = WalletAddress::new(address).map_err(|e| e.to_string())?; - let user = self - .user_repository - .find_by_wallet_address(&wallet_address.to_string()) - .await - .map_err(|e| e.to_string())? - .ok_or("User not found")?; - let mut profile = self - .profile_repository - .find_by_user_id(&user.id) + let mut profile = profile_repository + .find_by_address(&wallet_address) .await .map_err(|e| e.to_string())? .ok_or("Profile not found")?; profile.update_info(request.name, request.description, request.avatar_url); - self.profile_repository + profile_repository .update(&profile) .await .map_err(|e| e.to_string())?; Ok(ProfileResponse { - id: profile.id, - user_id: profile.user_id, + address: wallet_address, name: profile.name.unwrap_or_default(), description: profile.description, avatar_url: profile.avatar_url, diff --git a/backend/src/application/dtos/profile_dtos.rs b/backend/src/application/dtos/profile_dtos.rs index 396e7ee..63611b5 100644 --- a/backend/src/application/dtos/profile_dtos.rs +++ b/backend/src/application/dtos/profile_dtos.rs @@ -1,9 +1,9 @@ use serde::{Deserialize, Serialize}; -use uuid::Uuid; + +use crate::domain::value_objects::WalletAddress; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateProfileRequest { - pub address: String, pub name: String, pub description: Option, pub avatar_url: Option, @@ -18,8 +18,7 @@ pub struct UpdateProfileRequest { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProfileResponse { - pub id: Uuid, - pub user_id: Uuid, + pub address: WalletAddress, pub name: String, pub description: Option, pub avatar_url: Option, diff --git a/backend/src/domain/entities/profile.rs b/backend/src/domain/entities/profile.rs index 7b3621e..f3d9467 100644 --- a/backend/src/domain/entities/profile.rs +++ b/backend/src/domain/entities/profile.rs @@ -1,11 +1,11 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use uuid::Uuid; + +use crate::domain::value_objects::WalletAddress; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Profile { - pub id: Uuid, - pub user_id: Uuid, + pub address: WalletAddress, pub name: Option, pub description: Option, pub avatar_url: Option, @@ -14,11 +14,10 @@ pub struct Profile { } impl Profile { - pub fn new(user_id: Uuid) -> Self { + pub fn new(wallet_address: WalletAddress) -> Self { let now = Utc::now(); Self { - id: Uuid::new_v4(), - user_id, + address: wallet_address, name: None, description: None, avatar_url: None, diff --git a/backend/src/domain/repositories/profile_repository.rs b/backend/src/domain/repositories/profile_repository.rs index 1fa106d..a8c3bc0 100644 --- a/backend/src/domain/repositories/profile_repository.rs +++ b/backend/src/domain/repositories/profile_repository.rs @@ -1,11 +1,14 @@ use async_trait::async_trait; -use uuid::Uuid; -use crate::domain::entities::profile::Profile; +use crate::domain::{entities::profile::Profile, value_objects::WalletAddress}; #[async_trait] pub trait ProfileRepository: Send + Sync { + async fn find_by_address( + &self, + address: &WalletAddress, + ) -> Result, Box>; async fn create(&self, profile: &Profile) -> Result<(), Box>; async fn update(&self, profile: &Profile) -> Result<(), Box>; - async fn delete(&self, id: &Uuid) -> Result<(), Box>; + async fn delete(&self, address: &WalletAddress) -> Result<(), Box>; } diff --git a/backend/src/domain/services/auth_service.rs b/backend/src/domain/services/auth_service.rs index 4953e94..3afaf9f 100644 --- a/backend/src/domain/services/auth_service.rs +++ b/backend/src/domain/services/auth_service.rs @@ -5,7 +5,7 @@ use crate::domain::value_objects::wallet_address::WalletAddress; #[derive(Debug, Clone, PartialEq, Eq)] pub struct AuthChallenge { pub nonce: String, - pub message: String, + pub address: String, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -19,5 +19,5 @@ pub trait AuthService: Send + Sync { &self, challenge: &AuthChallenge, signature: &str, - ) -> Result>; + ) -> Result, Box>; } diff --git a/backend/src/domain/services/mod.rs b/backend/src/domain/services/mod.rs index 4b323dc..3fe88a6 100644 --- a/backend/src/domain/services/mod.rs +++ b/backend/src/domain/services/mod.rs @@ -1,3 +1 @@ pub mod auth_service; - -pub use auth_service::*; diff --git a/backend/src/domain/value_objects/wallet_address.rs b/backend/src/domain/value_objects/wallet_address.rs index e25f37f..f03d1d7 100644 --- a/backend/src/domain/value_objects/wallet_address.rs +++ b/backend/src/domain/value_objects/wallet_address.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use std::fmt; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct WalletAddress(String); +pub struct WalletAddress(pub String); impl WalletAddress { pub fn new(address: String) -> Result { diff --git a/backend/src/infrastructure/repositories/postgres_profile_repository.rs b/backend/src/infrastructure/repositories/postgres_profile_repository.rs index 9b4a8a9..ffac975 100644 --- a/backend/src/infrastructure/repositories/postgres_profile_repository.rs +++ b/backend/src/infrastructure/repositories/postgres_profile_repository.rs @@ -1,9 +1,9 @@ use async_trait::async_trait; use sqlx::PgPool; -use uuid::Uuid; use crate::domain::entities::profile::Profile; use crate::domain::repositories::profile_repository::ProfileRepository; +use crate::domain::value_objects::WalletAddress; #[derive(Clone)] pub struct PostgresProfileRepository { @@ -18,104 +18,50 @@ impl PostgresProfileRepository { #[async_trait] impl ProfileRepository for PostgresProfileRepository { - async fn create(&self, profile: &Profile) -> Result<(), Box> { - sqlx::query!( - r#" - INSERT INTO profiles (id, user_id, name, description, avatar_url, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7) - "#, - profile.id, - profile.user_id, - profile.name, - profile.description, - profile.avatar_url, - profile.created_at, - profile.updated_at - ) - .execute(&self.pool) - .await - .map_err(|e| Box::new(e) as Box)?; - - Ok(()) - } - - async fn find_by_id(&self, id: &Uuid) -> Result, Box> { - let row = sqlx::query!( - r#" - SELECT id, user_id, name, description, avatar_url, created_at, updated_at - FROM profiles - WHERE id = $1 - "#, - id - ) - .fetch_optional(&self.pool) - .await - .map_err(|e| Box::new(e) as Box)?; - - Ok(row.map(|r| Profile { - id: r.id, - user_id: r.user_id, - name: r.name, - description: r.description, - avatar_url: r.avatar_url, - created_at: r.created_at, - updated_at: r.updated_at, - })) - } - - async fn find_by_user_id( + async fn find_by_address( &self, - user_id: &Uuid, + address: &WalletAddress, ) -> Result, Box> { let row = sqlx::query!( r#" - SELECT id, user_id, name, description, avatar_url, created_at, updated_at + SELECT address, name, description, avatar_url, created_at, updated_at FROM profiles - WHERE user_id = $1 + WHERE address = $1 "#, - user_id + address.as_str() ) .fetch_optional(&self.pool) .await .map_err(|e| Box::new(e) as Box)?; Ok(row.map(|r| Profile { - id: r.id, - user_id: r.user_id, + address: WalletAddress(r.address), name: r.name, description: r.description, avatar_url: r.avatar_url, - created_at: r.created_at, - updated_at: r.updated_at, + created_at: r.created_at.unwrap(), + updated_at: r.updated_at.unwrap(), })) } - async fn find_all(&self) -> Result, Box> { - let rows = sqlx::query!( + async fn create(&self, profile: &Profile) -> Result<(), Box> { + sqlx::query!( r#" - SELECT id, user_id, name, description, avatar_url, created_at, updated_at - FROM profiles - ORDER BY created_at DESC - "# + INSERT INTO profiles (address, name, description, avatar_url, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6) + "#, + profile.address.as_str(), + profile.name, + profile.description, + profile.avatar_url, + profile.created_at, + profile.updated_at ) - .fetch_all(&self.pool) + .execute(&self.pool) .await .map_err(|e| Box::new(e) as Box)?; - let profiles = rows - .into_iter() - .map(|r| Profile { - id: r.id, - user_id: r.user_id, - name: r.name, - description: r.description, - avatar_url: r.avatar_url, - created_at: r.created_at, - updated_at: r.updated_at, - }) - .collect(); - - Ok(profiles) + Ok(()) } async fn update(&self, profile: &Profile) -> Result<(), Box> { @@ -123,9 +69,9 @@ impl ProfileRepository for PostgresProfileRepository { r#" UPDATE profiles SET name = $2, description = $3, avatar_url = $4, updated_at = $5 - WHERE id = $1 + WHERE address = $1 "#, - profile.id, + profile.address.as_str(), profile.name, profile.description, profile.avatar_url, @@ -138,13 +84,13 @@ impl ProfileRepository for PostgresProfileRepository { Ok(()) } - async fn delete(&self, id: &Uuid) -> Result<(), Box> { + async fn delete(&self, address: &WalletAddress) -> Result<(), Box> { sqlx::query!( r#" DELETE FROM profiles - WHERE id = $1 + WHERE address = $1 "#, - id + address.as_str() ) .execute(&self.pool) .await diff --git a/backend/src/infrastructure/services/ethereum_address_verification_service.rs b/backend/src/infrastructure/services/ethereum_address_verification_service.rs index 0270049..62e64ce 100644 --- a/backend/src/infrastructure/services/ethereum_address_verification_service.rs +++ b/backend/src/infrastructure/services/ethereum_address_verification_service.rs @@ -1,6 +1,10 @@ use async_trait::async_trait; +use ethers::core::utils::hash_message; +use ethers::types::{Address, Signature}; +use std::str::FromStr; -use crate::domain::services::AuthService::{self, AuthChallenge, AuthResult}; +use crate::domain::services::auth_service::{AuthChallenge, AuthResult, AuthService}; +use crate::domain::value_objects::WalletAddress; pub struct EthereumAddressVerificationService {} @@ -10,11 +14,37 @@ impl EthereumAddressVerificationService { } } +impl Default for EthereumAddressVerificationService { + fn default() -> Self { + Self::new() + } +} + #[async_trait] impl AuthService for EthereumAddressVerificationService { async fn verify_signature( &self, challenge: &AuthChallenge, signature: &str, - ) -> Result>; + ) -> Result, Box> { + const EXPECTED_MSG: &str = "LOGIN_NONCE"; // or whatever constant string you are signing + + // EIP-191 prefix + keccak256 + let msg_hash = hash_message(EXPECTED_MSG); + + // Parse signature and expected address + let sig = Signature::from_str(signature)?; + let expected: Address = challenge.address.parse()?; + + // Recover signer from signature + let recovered = sig.recover(msg_hash)?; + + if recovered == expected { + Ok(Some(AuthResult { + wallet_address: WalletAddress(challenge.address.clone()), + })) + } else { + Ok(None) + } + } } diff --git a/backend/src/lib.rs b/backend/src/lib.rs deleted file mode 100644 index 5c73ee9..0000000 --- a/backend/src/lib.rs +++ /dev/null @@ -1,51 +0,0 @@ -pub mod application; -pub mod domain; -pub mod infrastructure; - -use axum::{ - extract::DefaultBodyLimit, - http::Method, - routing::{get, post, put}, - Router, -}; -use domain::repositories::profile_repository; -use infrastructure::{ - repositories::PostgresProfileRepository, - services::ethereum_address_verification_service::EthereumAddressVerificationService, -}; -use tower::ServiceBuilder; -use tower_http::{ - cors::{Any, CorsLayer}, - trace::TraceLayer, -}; - -use crate::application::services::ProfileApplicationService; - -pub async fn create_app(_pool: sqlx::PgPool) -> Router { - // Create application services - let auth_service = EthereumAddressVerificationService::new(); - let profile_repository = PostgresProfileRepository::new(); - - Router::new() - .route("/profiles/:address", post(handle_create_profile)) - .route("/profiles/:address", get(handle_get_profile)) - .route("/profiles/:address", put(handle_update_profile)) - .layer( - ServiceBuilder::new() - .layer(TraceLayer::new_for_http()) - .layer( - CorsLayer::new() - .allow_origin(Any) - .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE]) - .allow_headers(Any), - ) - .layer(DefaultBodyLimit::max(1024 * 1024)), // 1MB limit - ) -} - -#[derive(Clone)] -pub struct AppState { - pub auth_service: AuthApplicationService, - pub profile_service: ProfileApplicationService, - pub badge_service: BadgeApplicationService, -} diff --git a/backend/src/main.rs b/backend/src/main.rs index d50dfad..15574f9 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,8 +1,12 @@ -use axum::{response::Json, routing::get, Router}; -use serde_json::{json, Value}; +use presentation::api::create_app; use std::net::SocketAddr; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +pub mod application; +pub mod domain; +pub mod infrastructure; +pub mod presentation; + #[tokio::main] async fn main() -> anyhow::Result<()> { // Initialize tracing @@ -31,7 +35,7 @@ async fn main() -> anyhow::Result<()> { sqlx::migrate!("./migrations").run(&pool).await?; // Create the app - let app = guild_backend::create_app(pool).await; + let app = create_app(pool).await; // Run the server let addr = SocketAddr::from(([0, 0, 0, 0], 3001)); @@ -42,19 +46,3 @@ async fn main() -> anyhow::Result<()> { Ok(()) } - -async fn health_check() -> Json { - Json(json!({ - "status": "ok", - "message": "The Guild Genesis backend is running" - })) -} - -async fn api_status() -> Json { - Json(json!({ - "status": "ok", - "service": "guild-backend", - "version": "0.1.0", - "message": "API is operational" - })) -} diff --git a/backend/src/presentation/api.rs b/backend/src/presentation/api.rs new file mode 100644 index 0000000..e8ba67c --- /dev/null +++ b/backend/src/presentation/api.rs @@ -0,0 +1,60 @@ +use std::sync::Arc; + +use crate::domain::repositories::ProfileRepository; +use crate::domain::services::auth_service::AuthService; +use crate::infrastructure::{ + repositories::PostgresProfileRepository, + services::ethereum_address_verification_service::EthereumAddressVerificationService, +}; +use axum::middleware::from_fn_with_state; +use axum::{ + extract::DefaultBodyLimit, + http::Method, + routing::{get, post, put}, + Router, +}; +use tower::ServiceBuilder; +use tower_http::{ + cors::{Any, CorsLayer}, + trace::TraceLayer, +}; + +use super::handlers::{create_profile_handler, get_profile_handler, update_profile_handler}; +use super::middlewares::eth_auth_layer; + +pub async fn create_app(pool: sqlx::PgPool) -> Router { + let auth_service = EthereumAddressVerificationService::new(); + let profile_repository = PostgresProfileRepository::new(pool); + + let state: AppState = AppState { + profile_repository: Arc::from(profile_repository), + auth_service: Arc::from(auth_service), + }; + + let protected = Router::new() + .route("/profiles/:address", post(create_profile_handler)) + .route("/profiles/:address", get(get_profile_handler)) + .route("/profiles/:address", put(update_profile_handler)); + + Router::new() + .nest("/", protected) + .with_state(state.clone()) + .layer( + ServiceBuilder::new() + .layer(TraceLayer::new_for_http()) + .layer( + CorsLayer::new() + .allow_origin(Any) + .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE]) + .allow_headers(Any), + ) + .layer(DefaultBodyLimit::max(1024 * 1024)) + .layer(from_fn_with_state(state, eth_auth_layer)), + ) +} + +#[derive(Clone)] +pub struct AppState { + pub profile_repository: Arc, + pub auth_service: Arc, +} diff --git a/backend/src/presentation/handlers.rs b/backend/src/presentation/handlers.rs new file mode 100644 index 0000000..68760bf --- /dev/null +++ b/backend/src/presentation/handlers.rs @@ -0,0 +1,55 @@ +use axum::{extract::State, http::StatusCode, Extension, Json}; + +use crate::{ + application::{ + commands::{ + create_profile::create_profile, get_profile::get_profile, + update_profile::update_profile, + }, + dtos::{CreateProfileRequest, ProfileResponse, UpdateProfileRequest}, + }, + domain::value_objects::WalletAddress, +}; + +use super::{api::AppState, middlewares::VerifiedWallet}; + +pub async fn create_profile_handler( + State(state): State, + Extension(VerifiedWallet(wallet)): Extension, + Json(payload): Json, +) -> StatusCode { + create_profile(state.profile_repository, wallet, payload) + .await + .unwrap(); + StatusCode::CREATED +} + +pub async fn get_profile_handler( + State(state): State, + Extension(VerifiedWallet(wallet)): Extension, +) -> Json { + Json(get_profile(state.profile_repository, wallet).await.unwrap()) +} + +pub async fn update_profile_handler( + State(state): State, + Extension(VerifiedWallet(wallet)): Extension, + Json(payload): Json, +) -> StatusCode { + update_profile(state.profile_repository, wallet, payload) + .await + .unwrap(); + StatusCode::CREATED +} + +pub async fn delete_profile_handler( + State(state): State, + Extension(VerifiedWallet(wallet)): Extension, +) -> StatusCode { + state + .profile_repository + .delete(&WalletAddress(wallet)) + .await + .unwrap(); + StatusCode::ACCEPTED +} diff --git a/backend/src/presentation/middlewares.rs b/backend/src/presentation/middlewares.rs new file mode 100644 index 0000000..ae97bd9 --- /dev/null +++ b/backend/src/presentation/middlewares.rs @@ -0,0 +1,54 @@ +use axum::{ + body::Body, + extract::State, + http::{Request, StatusCode}, + middleware::Next, + response::Response, +}; + +use crate::domain::services::auth_service::AuthChallenge; + +use super::api::AppState; + +#[derive(Clone, Debug)] +pub struct VerifiedWallet(pub String); + +pub async fn eth_auth_layer( + State(state): State, + mut req: Request, + next: Next, +) -> Result { + let headers = req.headers(); + + let address = headers + .get("x-eth-address") + .and_then(|v| v.to_str().ok()) + .map(str::to_owned) + .ok_or(StatusCode::UNAUTHORIZED)?; + + let signature = headers + .get("x-eth-signature") + .and_then(|v| v.to_str().ok()) + .map(str::to_owned) + .ok_or(StatusCode::UNAUTHORIZED)?; + + let nonce = "NONCE"; + + state + .auth_service + .verify_signature( + &AuthChallenge { + address: address.clone().to_string(), + nonce: nonce.to_string(), + }, + &signature, + ) // define the signature you like + .await + .map_err(|_| StatusCode::UNAUTHORIZED)?; + + // Inject identity for handlers: + req.extensions_mut() + .insert(VerifiedWallet(address.to_string())); + + Ok(next.run(req).await) +} diff --git a/backend/src/presentation/mod.rs b/backend/src/presentation/mod.rs new file mode 100644 index 0000000..490859f --- /dev/null +++ b/backend/src/presentation/mod.rs @@ -0,0 +1,3 @@ +pub mod api; +pub mod handlers; +pub mod middlewares;