From 9617f4415cd0f0b5c922ab24b3375be4143c9ddf Mon Sep 17 00:00:00 2001 From: MDietermann Date: Thu, 13 Aug 2026 13:08:13 +0200 Subject: [PATCH] feat: initial commit Axum/Postgres server with auth, migrations, and Docker setup. --- .dockerignore | 2 + .env.example | 10 + .gitignore | 2 + Cargo.lock | 2148 +++++++++++++++++ Cargo.toml | 17 + Dockerfile | 18 + compose.yaml | 31 + docs/api.md | 230 ++ .../20260812114905_create_user_table.down.sql | 3 + .../20260812114905_create_user_table.up.sql | 28 + .../20260812120000_create_workspaces.down.sql | 4 + .../20260812120000_create_workspaces.up.sql | 34 + ...260812130000_create_conversations.down.sql | 3 + ...20260812130000_create_conversations.up.sql | 23 + .../20260812140000_create_messages.down.sql | 3 + .../20260812140000_create_messages.up.sql | 31 + src/handlers/attachment.rs | 59 + src/handlers/auth.rs | 161 ++ src/handlers/auth_token.rs | 60 + src/handlers/conversation.rs | 102 + src/handlers/conversation_member.rs | 59 + src/handlers/message.rs | 89 + src/handlers/mod.rs | 17 + src/handlers/reaction.rs | 56 + src/handlers/user.rs | 90 + src/handlers/workspace.rs | 84 + src/handlers/workspace_invitation.rs | 61 + src/handlers/workspace_member.rs | 82 + src/main.rs | 51 + src/middleware.rs | 33 + src/model.rs | 173 ++ src/route.rs | 107 + src/schema.rs | 103 + 33 files changed, 3974 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Dockerfile create mode 100644 compose.yaml create mode 100644 docs/api.md create mode 100644 migrations/20260812114905_create_user_table.down.sql create mode 100644 migrations/20260812114905_create_user_table.up.sql create mode 100644 migrations/20260812120000_create_workspaces.down.sql create mode 100644 migrations/20260812120000_create_workspaces.up.sql create mode 100644 migrations/20260812130000_create_conversations.down.sql create mode 100644 migrations/20260812130000_create_conversations.up.sql create mode 100644 migrations/20260812140000_create_messages.down.sql create mode 100644 migrations/20260812140000_create_messages.up.sql create mode 100644 src/handlers/attachment.rs create mode 100644 src/handlers/auth.rs create mode 100644 src/handlers/auth_token.rs create mode 100644 src/handlers/conversation.rs create mode 100644 src/handlers/conversation_member.rs create mode 100644 src/handlers/message.rs create mode 100644 src/handlers/mod.rs create mode 100644 src/handlers/reaction.rs create mode 100644 src/handlers/user.rs create mode 100644 src/handlers/workspace.rs create mode 100644 src/handlers/workspace_invitation.rs create mode 100644 src/handlers/workspace_member.rs create mode 100644 src/main.rs create mode 100644 src/middleware.rs create mode 100644 src/model.rs create mode 100644 src/route.rs create mode 100644 src/schema.rs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..14ee500 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +target/ +.env diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c29fc54 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +POSTGRES_DB=rustification +POSTGRES_USER=admin +POSTGRES_PASSWORD=password + +DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB} + +API_URL=0.0.0.0 +API_PORT=3000 + +JWT_SECRET=XXXX diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fedaa2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.env diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..afe0c53 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2148 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +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 0.2.17", + "password-hash", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[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 = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.1", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "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 = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +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 = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustification-server" +version = "0.1.0" +dependencies = [ + "argon2", + "axum", + "chrono", + "dotenv", + "jsonwebtoken", + "rand_core 0.6.4", + "serde", + "serde_json", + "sqlx", + "tokio", + "uuid", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64", + "bytes", + "cfg-if", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap", + "log", + "memchr", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.119", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.119", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest 0.11.3", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array", + "log", + "percent-encoding", + "serde", + "sha1", + "sha2 0.11.0", + "sqlx-core", + "thiserror", + "tracing", + "uuid", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "rand", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +dependencies = [ + "atoi", + "chrono", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[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.119", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[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.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "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 = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.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 = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d0e183d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rustification-server" +version = "0.1.0" +edition = "2024" + +[dependencies] +axum = "0.8.9" +dotenv = "0.15.0" +serde_json = "1.0.151" +tokio = { version = "1", features = ["full"] } +sqlx = { version = "0.9", features = ["runtime-tokio", "tls-rustls-ring-webpki", "postgres", "chrono", "uuid"] } +serde = { version = "1.0.219", features = ["derive"] } +chrono = { version = "0.4.41", features = ["serde"] } +uuid = { version = "1.17.0", features = ["serde", "v4"] } +argon2 = "0.5" +jsonwebtoken = "9" +rand_core = { version = "0.6", features = ["getrandom"] } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..98b46d8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM rust:1-slim-bookworm AS builder +WORKDIR /app + +# Cache dependencies before copying source +COPY Cargo.toml Cargo.lock ./ +RUN mkdir src && echo "fn main() {}" > src/main.rs \ + && cargo build --release \ + && rm -rf src + +COPY src ./src +# Touch main.rs so cargo knows the real source changed +RUN touch src/main.rs && cargo build --release + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* +COPY --from=builder /app/target/release/rustification-server /usr/local/bin/server +EXPOSE 3000 +CMD ["server"] diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..115e78f --- /dev/null +++ b/compose.yaml @@ -0,0 +1,31 @@ +services: + db: + image: postgres:17-alpine + container_name: rustification-postgres + env_file: + - ./.env + ports: + - '5432:5432' + volumes: + - postgresDb:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + + server: + build: . + container_name: rustification-server + env_file: + - ./.env + environment: + DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} + ports: + - '3000:3000' + depends_on: + db: + condition: service_healthy + +volumes: + postgresDb: diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..1848b81 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,230 @@ +# API Reference + +Base URL: `http://localhost:3000/api/v1` + +--- + +## Users + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/users` | List all users | +| POST | `/users` | Create a user | +| GET | `/users/{id}` | Get a user | +| PUT | `/users/{id}` | Update a user (display_name, avatar_url, bio) | +| DELETE | `/users/{id}` | Soft-delete a user | + +**POST /users — body** +```json +{ + "email": "user@example.com", + "password_hash": "...", + "username": "jane", + "display_name": "Jane", + "avatar_url": null, + "bio": null, + "email_verified": false +} +``` + +--- + +## Auth Tokens + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/users/{id}/auth-tokens` | List tokens for a user | +| POST | `/auth-tokens` | Create a token | +| DELETE | `/auth-tokens/{id}` | Revoke a token | + +**POST /auth-tokens — body** +```json +{ + "user_id": "uuid", + "kind": "refresh_token", + "expires_at": "2026-09-01T00:00:00Z" +} +``` +`kind`: `password_reset` | `email_verification` | `refresh_token` + +**POST /auth-tokens — response** includes `token` (raw value) and `record`. + +--- + +## Workspaces + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/workspaces` | List all workspaces | +| POST | `/workspaces` | Create a workspace | +| GET | `/workspaces/{id}` | Get a workspace | +| PUT | `/workspaces/{id}` | Update a workspace (name, slug) | +| DELETE | `/workspaces/{id}` | Soft-delete a workspace | + +**POST /workspaces — body** +```json +{ + "name": "Acme", + "slug": "acme", + "owner_id": "uuid" +} +``` + +--- + +## Workspace Members + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/workspaces/{id}/members` | List members | +| POST | `/workspaces/{id}/members` | Add a member | +| PUT | `/workspaces/{workspace_id}/members/{user_id}` | Update role / display name | +| DELETE | `/workspaces/{workspace_id}/members/{user_id}` | Remove a member | + +**POST body** +```json +{ + "user_id": "uuid", + "role": "member", + "display_name": null, + "avatar_url": null +} +``` +`role`: `owner` | `admin` | `member` + +--- + +## Workspace Invitations + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/workspaces/{id}/invitations` | List invitations | +| POST | `/workspaces/{id}/invitations` | Create an invitation | +| DELETE | `/invitations/{id}` | Delete an invitation | + +**POST body** +```json +{ + "created_by": "uuid", + "max_uses": 10, + "expires_at": "2026-09-01T00:00:00Z" +} +``` +`max_uses` and `expires_at` are optional. The invite `token` is generated server-side. + +--- + +## Conversations + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/conversations` | List conversations (`?workspace_id=uuid`) | +| POST | `/conversations` | Create a conversation | +| GET | `/conversations/{id}` | Get a conversation | +| PUT | `/conversations/{id}` | Update a conversation (name, is_public) | +| DELETE | `/conversations/{id}` | Soft-delete a conversation | + +**POST body** +```json +{ + "workspace_id": "uuid", + "kind": "channel", + "name": "general", + "is_public": true, + "created_by": "uuid" +} +``` +`kind`: `dm` | `group` | `channel` +`workspace_id` is optional (null for DMs). + +--- + +## Conversation Members + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/conversations/{id}/members` | List members | +| POST | `/conversations/{id}/members` | Add a member | +| DELETE | `/conversations/{conversation_id}/members/{user_id}` | Remove a member | + +**POST body** +```json +{ + "user_id": "uuid" +} +``` + +--- + +## Messages + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/conversations/{id}/messages` | List messages (chronological) | +| POST | `/messages` | Send a message | +| GET | `/messages/{id}` | Get a message | +| PUT | `/messages/{id}` | Edit a message (content) | +| DELETE | `/messages/{id}` | Soft-delete a message | + +**POST body** +```json +{ + "conversation_id": "uuid", + "user_id": "uuid", + "content": "Hello!" +} +``` + +--- + +## Attachments + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/messages/{id}/attachments` | List attachments for a message | +| POST | `/attachments` | Create an attachment | +| DELETE | `/attachments/{id}` | Delete an attachment | + +**POST body** +```json +{ + "message_id": "uuid", + "url": "https://...", + "filename": "file.pdf", + "mime_type": "application/pdf", + "size_bytes": 102400 +} +``` + +--- + +## Reactions + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/messages/{id}/reactions` | List reactions for a message | +| POST | `/reactions` | Add a reaction | +| DELETE | `/reactions/{id}` | Remove a reaction | + +**POST body** +```json +{ + "message_id": "uuid", + "user_id": "uuid", + "emoji": "👍" +} +``` + +--- + +## Response conventions + +| Status | Meaning | +|--------|---------| +| 200 | OK | +| 201 | Created | +| 204 | Deleted (no body) | +| 404 | Not found | +| 500 | Internal server error (body: error string) | + +Soft-deleted resources (`users`, `workspaces`, `conversations`, `messages`) are excluded from all list/get responses. diff --git a/migrations/20260812114905_create_user_table.down.sql b/migrations/20260812114905_create_user_table.down.sql new file mode 100644 index 0000000..3a24408 --- /dev/null +++ b/migrations/20260812114905_create_user_table.down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS auth_tokens; +DROP TYPE IF EXISTS auth_token_kind; +DROP TABLE IF EXISTS users; diff --git a/migrations/20260812114905_create_user_table.up.sql b/migrations/20260812114905_create_user_table.up.sql new file mode 100644 index 0000000..186e6c2 --- /dev/null +++ b/migrations/20260812114905_create_user_table.up.sql @@ -0,0 +1,28 @@ +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + username TEXT NOT NULL UNIQUE, + display_name TEXT NOT NULL, + avatar_url TEXT, + bio TEXT, + email_verified BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE TYPE auth_token_kind AS ENUM ('password_reset', 'email_verification', 'refresh_token'); + +CREATE TABLE auth_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash TEXT NOT NULL, + kind auth_token_kind NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + used_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX auth_tokens_user_id_idx ON auth_tokens(user_id); +CREATE INDEX auth_tokens_token_hash_idx ON auth_tokens(token_hash); diff --git a/migrations/20260812120000_create_workspaces.down.sql b/migrations/20260812120000_create_workspaces.down.sql new file mode 100644 index 0000000..99848b7 --- /dev/null +++ b/migrations/20260812120000_create_workspaces.down.sql @@ -0,0 +1,4 @@ +DROP TABLE IF EXISTS workspace_invitations; +DROP TABLE IF EXISTS workspace_members; +DROP TABLE IF EXISTS workspaces; +DROP TYPE IF EXISTS workspace_role; diff --git a/migrations/20260812120000_create_workspaces.up.sql b/migrations/20260812120000_create_workspaces.up.sql new file mode 100644 index 0000000..94a04a2 --- /dev/null +++ b/migrations/20260812120000_create_workspaces.up.sql @@ -0,0 +1,34 @@ +CREATE TYPE workspace_role AS ENUM ('owner', 'admin', 'member'); + +CREATE TABLE workspaces ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + slug TEXT NOT NULL UNIQUE, + owner_id UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE TABLE workspace_members ( + workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role workspace_role NOT NULL DEFAULT 'member', + display_name TEXT, + avatar_url TEXT, + joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (workspace_id, user_id) +); + +CREATE TABLE workspace_invitations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + created_by UUID NOT NULL REFERENCES users(id), + token TEXT NOT NULL UNIQUE, + max_uses INT, + use_count INT NOT NULL DEFAULT 0, + expires_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX workspace_members_user_id_idx ON workspace_members(user_id); diff --git a/migrations/20260812130000_create_conversations.down.sql b/migrations/20260812130000_create_conversations.down.sql new file mode 100644 index 0000000..aea66b8 --- /dev/null +++ b/migrations/20260812130000_create_conversations.down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS conversation_members; +DROP TABLE IF EXISTS conversations; +DROP TYPE IF EXISTS conversation_kind; diff --git a/migrations/20260812130000_create_conversations.up.sql b/migrations/20260812130000_create_conversations.up.sql new file mode 100644 index 0000000..d1c9472 --- /dev/null +++ b/migrations/20260812130000_create_conversations.up.sql @@ -0,0 +1,23 @@ +CREATE TYPE conversation_kind AS ENUM ('dm', 'group', 'channel'); + +CREATE TABLE conversations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + workspace_id UUID REFERENCES workspaces(id) ON DELETE CASCADE, + kind conversation_kind NOT NULL, + name TEXT, + is_public BOOLEAN NOT NULL DEFAULT true, + created_by UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE TABLE conversation_members ( + conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (conversation_id, user_id) +); + +CREATE INDEX conversations_workspace_id_idx ON conversations(workspace_id); +CREATE INDEX conversation_members_user_id_idx ON conversation_members(user_id); diff --git a/migrations/20260812140000_create_messages.down.sql b/migrations/20260812140000_create_messages.down.sql new file mode 100644 index 0000000..ac08dd5 --- /dev/null +++ b/migrations/20260812140000_create_messages.down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS reactions; +DROP TABLE IF EXISTS attachments; +DROP TABLE IF EXISTS messages; diff --git a/migrations/20260812140000_create_messages.up.sql b/migrations/20260812140000_create_messages.up.sql new file mode 100644 index 0000000..d2ba6ca --- /dev/null +++ b/migrations/20260812140000_create_messages.up.sql @@ -0,0 +1,31 @@ +CREATE TABLE messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id), + content TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE TABLE attachments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, + url TEXT NOT NULL, + filename TEXT NOT NULL, + mime_type TEXT NOT NULL, + size_bytes BIGINT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE reactions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + emoji TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (message_id, user_id, emoji) +); + +CREATE INDEX messages_conversation_id_created_at_idx ON messages(conversation_id, created_at DESC); +CREATE INDEX reactions_message_id_idx ON reactions(message_id); diff --git a/src/handlers/attachment.rs b/src/handlers/attachment.rs new file mode 100644 index 0000000..0bb4d4c --- /dev/null +++ b/src/handlers/attachment.rs @@ -0,0 +1,59 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use std::sync::Arc; +use uuid::Uuid; +use crate::{model::Attachment, schema::AttachmentSchema, AppState}; +use super::db_err; + +pub async fn list_attachments( + State(state): State>, + Path(message_id): Path, +) -> Result { + let attachments = sqlx::query_as::<_, Attachment>( + "SELECT * FROM attachments WHERE message_id = $1", + ) + .bind(message_id) + .fetch_all(&state.db) + .await + .map_err(db_err)?; + Ok(Json(attachments)) +} + +pub async fn create_attachment( + State(state): State>, + Json(body): Json, +) -> Result { + let attachment = sqlx::query_as::<_, Attachment>( + "INSERT INTO attachments (message_id, url, filename, mime_type, size_bytes) + VALUES ($1, $2, $3, $4, $5) RETURNING *", + ) + .bind(body.message_id) + .bind(&body.url) + .bind(&body.filename) + .bind(&body.mime_type) + .bind(body.size_bytes) + .fetch_one(&state.db) + .await + .map_err(db_err)?; + Ok((StatusCode::CREATED, Json(attachment))) +} + +pub async fn delete_attachment( + State(state): State>, + Path(id): Path, +) -> Result { + let res = sqlx::query("DELETE FROM attachments WHERE id = $1") + .bind(id) + .execute(&state.db) + .await + .map_err(db_err)?; + if res.rows_affected() > 0 { + Ok(StatusCode::NO_CONTENT.into_response()) + } else { + Err((StatusCode::NOT_FOUND, "Attachment not found".into())) + } +} diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs new file mode 100644 index 0000000..a511c76 --- /dev/null +++ b/src/handlers/auth.rs @@ -0,0 +1,161 @@ +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Argon2, +}; +use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; +use chrono::{Duration, Utc}; +use jsonwebtoken::{encode, EncodingKey, Header}; +use std::sync::Arc; +use uuid::Uuid; + +use crate::{ + model::{AuthToken, AuthTokenKind, Claims, User, UserResponse}, + schema::{LoginSchema, RefreshSchema, RegisterSchema}, + AppState, +}; + +use super::db_err; + +fn make_access_token(user_id: &str, secret: &str) -> Result { + let claims = Claims { + sub: user_id.to_string(), + exp: (Utc::now() + Duration::minutes(15)).timestamp() as usize, + }; + encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes())) + .map_err(|e| e.to_string()) +} + +async fn insert_refresh_token(state: &AppState, user_id: Uuid) -> Result { + // ponytail: raw UUID stored in token_hash — add SHA-256 if compliance requires + let raw = Uuid::new_v4().to_string(); + let expires = Utc::now() + Duration::days(30); + sqlx::query( + "INSERT INTO auth_tokens (user_id, token_hash, kind, expires_at) + VALUES ($1, $2, $3, $4)", + ) + .bind(user_id) + .bind(&raw) + .bind(AuthTokenKind::RefreshToken) + .bind(expires) + .execute(&state.db) + .await + .map_err(db_err)?; + Ok(raw) +} + +pub async fn register( + State(state): State>, + Json(body): Json, +) -> Result { + let salt = SaltString::generate(&mut OsRng); + let hash = Argon2::default() + .hash_password(body.password.as_bytes(), &salt) + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? + .to_string(); + + let user = sqlx::query_as::<_, User>( + "INSERT INTO users (email, password_hash, username, display_name, email_verified) + VALUES ($1, $2, $3, $4, false) RETURNING *", + ) + .bind(&body.email) + .bind(&hash) + .bind(&body.username) + .bind(&body.display_name) + .fetch_one(&state.db) + .await + .map_err(db_err)?; + + let refresh_token = insert_refresh_token(&state, user.id).await?; + let access_token = make_access_token(&user.id.to_string(), &state.jwt_secret) + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?; + + Ok(( + StatusCode::CREATED, + Json(serde_json::json!({ + "user": UserResponse::from(user), + "access_token": access_token, + "refresh_token": refresh_token, + })), + )) +} + +pub async fn login( + State(state): State>, + Json(body): Json, +) -> Result { + let user = sqlx::query_as::<_, User>( + "SELECT * FROM users WHERE email = $1 AND deleted_at IS NULL", + ) + .bind(&body.email) + .fetch_optional(&state.db) + .await + .map_err(db_err)? + .ok_or((StatusCode::UNAUTHORIZED, "Invalid credentials".into()))?; + + let parsed = PasswordHash::new(&user.password_hash) + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Hash parse error".into()))?; + Argon2::default() + .verify_password(body.password.as_bytes(), &parsed) + .map_err(|_| (StatusCode::UNAUTHORIZED, "Invalid credentials".into()))?; + + let refresh_token = insert_refresh_token(&state, user.id).await?; + let access_token = make_access_token(&user.id.to_string(), &state.jwt_secret) + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?; + + Ok(Json(serde_json::json!({ + "user": UserResponse::from(user), + "access_token": access_token, + "refresh_token": refresh_token, + }))) +} + +pub async fn refresh( + State(state): State>, + Json(body): Json, +) -> Result { + let record = sqlx::query_as::<_, AuthToken>( + "SELECT * FROM auth_tokens + WHERE token_hash = $1 AND kind = $2 AND expires_at > NOW()", + ) + .bind(&body.refresh_token) + .bind(AuthTokenKind::RefreshToken) + .fetch_optional(&state.db) + .await + .map_err(db_err)? + .ok_or((StatusCode::UNAUTHORIZED, "Invalid or expired refresh token".into()))?; + + sqlx::query("UPDATE auth_tokens SET used_at = NOW() WHERE id = $1") + .bind(record.id) + .execute(&state.db) + .await + .map_err(db_err)?; + + let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1 AND deleted_at IS NULL") + .bind(record.user_id) + .fetch_one(&state.db) + .await + .map_err(db_err)?; + + let access_token = make_access_token(&user.id.to_string(), &state.jwt_secret) + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?; + + Ok(Json(serde_json::json!({ + "access_token": access_token, + "user": UserResponse::from(user), + }))) +} + +pub async fn logout( + State(state): State>, + Json(body): Json, +) -> Result { + sqlx::query( + "DELETE FROM auth_tokens WHERE token_hash = $1 AND kind = $2", + ) + .bind(&body.refresh_token) + .bind(AuthTokenKind::RefreshToken) + .execute(&state.db) + .await + .map_err(db_err)?; + Ok(StatusCode::NO_CONTENT) +} diff --git a/src/handlers/auth_token.rs b/src/handlers/auth_token.rs new file mode 100644 index 0000000..bed8756 --- /dev/null +++ b/src/handlers/auth_token.rs @@ -0,0 +1,60 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use std::sync::Arc; +use uuid::Uuid; +use crate::{model::AuthToken, schema::AuthTokenSchema, AppState}; +use super::db_err; + +pub async fn list_user_tokens( + State(state): State>, + Path(user_id): Path, +) -> Result { + let tokens = sqlx::query_as::<_, AuthToken>( + "SELECT * FROM auth_tokens WHERE user_id = $1 ORDER BY created_at DESC", + ) + .bind(user_id) + .fetch_all(&state.db) + .await + .map_err(db_err)?; + Ok(Json(tokens)) +} + +pub async fn create_token( + State(state): State>, + Json(body): Json, +) -> Result { + // ponytail: raw uuid used as token, hash with argon2/bcrypt when hardening auth + let raw_token = Uuid::new_v4().to_string(); + let record = sqlx::query_as::<_, AuthToken>( + "INSERT INTO auth_tokens (user_id, token_hash, kind, expires_at) + VALUES ($1, $2, $3, $4) RETURNING *", + ) + .bind(body.user_id) + .bind(&raw_token) + .bind(&body.kind) + .bind(body.expires_at) + .fetch_one(&state.db) + .await + .map_err(db_err)?; + Ok((StatusCode::CREATED, Json(serde_json::json!({ "token": raw_token, "record": record })))) +} + +pub async fn revoke_token( + State(state): State>, + Path(id): Path, +) -> Result { + let res = sqlx::query("DELETE FROM auth_tokens WHERE id = $1") + .bind(id) + .execute(&state.db) + .await + .map_err(db_err)?; + if res.rows_affected() > 0 { + Ok(StatusCode::NO_CONTENT.into_response()) + } else { + Err((StatusCode::NOT_FOUND, "Token not found".into())) + } +} diff --git a/src/handlers/conversation.rs b/src/handlers/conversation.rs new file mode 100644 index 0000000..706c596 --- /dev/null +++ b/src/handlers/conversation.rs @@ -0,0 +1,102 @@ +use axum::{ + extract::{Path, Query, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use serde::Deserialize; +use std::sync::Arc; +use uuid::Uuid; +use crate::{model::Conversation, schema::ConversationSchema, AppState}; +use super::db_err; + +#[derive(Deserialize)] +pub struct ConversationFilter { + workspace_id: Option, +} + +pub async fn list_conversations( + State(state): State>, + Query(filter): Query, +) -> Result { + let conversations = sqlx::query_as::<_, Conversation>( + "SELECT * FROM conversations WHERE deleted_at IS NULL + AND ($1::uuid IS NULL OR workspace_id = $1)", + ) + .bind(filter.workspace_id) + .fetch_all(&state.db) + .await + .map_err(db_err)?; + Ok(Json(conversations)) +} + +pub async fn get_conversation( + State(state): State>, + Path(id): Path, +) -> Result { + sqlx::query_as::<_, Conversation>( + "SELECT * FROM conversations WHERE id = $1 AND deleted_at IS NULL", + ) + .bind(id) + .fetch_optional(&state.db) + .await + .map_err(db_err)? + .map(|c| Json(c).into_response()) + .ok_or((StatusCode::NOT_FOUND, "Conversation not found".into())) +} + +pub async fn create_conversation( + State(state): State>, + Json(body): Json, +) -> Result { + let conversation = sqlx::query_as::<_, Conversation>( + "INSERT INTO conversations (workspace_id, kind, name, is_public, created_by) + VALUES ($1, $2, $3, $4, $5) RETURNING *", + ) + .bind(body.workspace_id) + .bind(&body.kind) + .bind(&body.name) + .bind(body.is_public) + .bind(body.created_by) + .fetch_one(&state.db) + .await + .map_err(db_err)?; + Ok((StatusCode::CREATED, Json(conversation))) +} + +pub async fn update_conversation( + State(state): State>, + Path(id): Path, + Json(body): Json, +) -> Result { + sqlx::query_as::<_, Conversation>( + "UPDATE conversations SET name = $1, is_public = $2, updated_at = NOW() + WHERE id = $3 AND deleted_at IS NULL RETURNING *", + ) + .bind(&body.name) + .bind(body.is_public) + .bind(id) + .fetch_optional(&state.db) + .await + .map_err(db_err)? + .map(|c| Json(c).into_response()) + .ok_or((StatusCode::NOT_FOUND, "Conversation not found".into())) +} + +pub async fn delete_conversation( + State(state): State>, + Path(id): Path, +) -> Result { + let res = sqlx::query( + "UPDATE conversations SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL", + ) + .bind(id) + .execute(&state.db) + .await + .map_err(db_err)?; + if res.rows_affected() > 0 { + Ok(StatusCode::NO_CONTENT.into_response()) + } else { + Err((StatusCode::NOT_FOUND, "Conversation not found".into())) + } +} diff --git a/src/handlers/conversation_member.rs b/src/handlers/conversation_member.rs new file mode 100644 index 0000000..d524e92 --- /dev/null +++ b/src/handlers/conversation_member.rs @@ -0,0 +1,59 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use std::sync::Arc; +use uuid::Uuid; +use crate::{model::ConversationMember, schema::ConversationMemberSchema, AppState}; +use super::db_err; + +pub async fn list_members( + State(state): State>, + Path(conversation_id): Path, +) -> Result { + let members = sqlx::query_as::<_, ConversationMember>( + "SELECT * FROM conversation_members WHERE conversation_id = $1", + ) + .bind(conversation_id) + .fetch_all(&state.db) + .await + .map_err(db_err)?; + Ok(Json(members)) +} + +pub async fn add_member( + State(state): State>, + Path(conversation_id): Path, + Json(body): Json, +) -> Result { + let member = sqlx::query_as::<_, ConversationMember>( + "INSERT INTO conversation_members (conversation_id, user_id) VALUES ($1, $2) RETURNING *", + ) + .bind(conversation_id) + .bind(body.user_id) + .fetch_one(&state.db) + .await + .map_err(db_err)?; + Ok((StatusCode::CREATED, Json(member))) +} + +pub async fn remove_member( + State(state): State>, + Path((conversation_id, user_id)): Path<(Uuid, Uuid)>, +) -> Result { + let res = sqlx::query( + "DELETE FROM conversation_members WHERE conversation_id = $1 AND user_id = $2", + ) + .bind(conversation_id) + .bind(user_id) + .execute(&state.db) + .await + .map_err(db_err)?; + if res.rows_affected() > 0 { + Ok(StatusCode::NO_CONTENT.into_response()) + } else { + Err((StatusCode::NOT_FOUND, "Member not found".into())) + } +} diff --git a/src/handlers/message.rs b/src/handlers/message.rs new file mode 100644 index 0000000..b293d31 --- /dev/null +++ b/src/handlers/message.rs @@ -0,0 +1,89 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use std::sync::Arc; +use uuid::Uuid; +use crate::{model::Message, schema::MessageSchema, AppState}; +use super::db_err; + +pub async fn list_messages( + State(state): State>, + Path(conversation_id): Path, +) -> Result { + let messages = sqlx::query_as::<_, Message>( + "SELECT * FROM messages WHERE conversation_id = $1 AND deleted_at IS NULL ORDER BY created_at ASC", + ) + .bind(conversation_id) + .fetch_all(&state.db) + .await + .map_err(db_err)?; + Ok(Json(messages)) +} + +pub async fn get_message( + State(state): State>, + Path(id): Path, +) -> Result { + sqlx::query_as::<_, Message>("SELECT * FROM messages WHERE id = $1 AND deleted_at IS NULL") + .bind(id) + .fetch_optional(&state.db) + .await + .map_err(db_err)? + .map(|m| Json(m).into_response()) + .ok_or((StatusCode::NOT_FOUND, "Message not found".into())) +} + +pub async fn create_message( + State(state): State>, + Json(body): Json, +) -> Result { + let message = sqlx::query_as::<_, Message>( + "INSERT INTO messages (conversation_id, user_id, content) VALUES ($1, $2, $3) RETURNING *", + ) + .bind(body.conversation_id) + .bind(body.user_id) + .bind(&body.content) + .fetch_one(&state.db) + .await + .map_err(db_err)?; + Ok((StatusCode::CREATED, Json(message))) +} + +pub async fn update_message( + State(state): State>, + Path(id): Path, + Json(body): Json, +) -> Result { + sqlx::query_as::<_, Message>( + "UPDATE messages SET content = $1, updated_at = NOW() + WHERE id = $2 AND deleted_at IS NULL RETURNING *", + ) + .bind(&body.content) + .bind(id) + .fetch_optional(&state.db) + .await + .map_err(db_err)? + .map(|m| Json(m).into_response()) + .ok_or((StatusCode::NOT_FOUND, "Message not found".into())) +} + +pub async fn delete_message( + State(state): State>, + Path(id): Path, +) -> Result { + let res = sqlx::query( + "UPDATE messages SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL", + ) + .bind(id) + .execute(&state.db) + .await + .map_err(db_err)?; + if res.rows_affected() > 0 { + Ok(StatusCode::NO_CONTENT.into_response()) + } else { + Err((StatusCode::NOT_FOUND, "Message not found".into())) + } +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..f92511a --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,17 @@ +use axum::http::StatusCode; + +pub mod attachment; +pub mod auth; +pub mod auth_token; +pub mod conversation; +pub mod conversation_member; +pub mod message; +pub mod reaction; +pub mod user; +pub mod workspace; +pub mod workspace_invitation; +pub mod workspace_member; + +pub(crate) fn db_err(e: sqlx::Error) -> (StatusCode, String) { + (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()) +} diff --git a/src/handlers/reaction.rs b/src/handlers/reaction.rs new file mode 100644 index 0000000..04ff708 --- /dev/null +++ b/src/handlers/reaction.rs @@ -0,0 +1,56 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use std::sync::Arc; +use uuid::Uuid; +use crate::{model::Reaction, schema::ReactionSchema, AppState}; +use super::db_err; + +pub async fn list_reactions( + State(state): State>, + Path(message_id): Path, +) -> Result { + let reactions = sqlx::query_as::<_, Reaction>( + "SELECT * FROM reactions WHERE message_id = $1", + ) + .bind(message_id) + .fetch_all(&state.db) + .await + .map_err(db_err)?; + Ok(Json(reactions)) +} + +pub async fn add_reaction( + State(state): State>, + Json(body): Json, +) -> Result { + let reaction = sqlx::query_as::<_, Reaction>( + "INSERT INTO reactions (message_id, user_id, emoji) VALUES ($1, $2, $3) RETURNING *", + ) + .bind(body.message_id) + .bind(body.user_id) + .bind(&body.emoji) + .fetch_one(&state.db) + .await + .map_err(db_err)?; + Ok((StatusCode::CREATED, Json(reaction))) +} + +pub async fn remove_reaction( + State(state): State>, + Path(id): Path, +) -> Result { + let res = sqlx::query("DELETE FROM reactions WHERE id = $1") + .bind(id) + .execute(&state.db) + .await + .map_err(db_err)?; + if res.rows_affected() > 0 { + Ok(StatusCode::NO_CONTENT.into_response()) + } else { + Err((StatusCode::NOT_FOUND, "Reaction not found".into())) + } +} diff --git a/src/handlers/user.rs b/src/handlers/user.rs new file mode 100644 index 0000000..2b1405a --- /dev/null +++ b/src/handlers/user.rs @@ -0,0 +1,90 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use std::sync::Arc; +use uuid::Uuid; +use crate::{model::{User, UserResponse}, schema::UserSchema, AppState}; +use super::db_err; + +pub async fn list_users( + State(state): State>, +) -> Result { + let users = sqlx::query_as::<_, User>("SELECT * FROM users WHERE deleted_at IS NULL") + .fetch_all(&state.db) + .await + .map_err(db_err)?; + Ok(Json(users.into_iter().map(UserResponse::from).collect::>())) +} + +pub async fn get_user( + State(state): State>, + Path(id): Path, +) -> Result { + sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1 AND deleted_at IS NULL") + .bind(id) + .fetch_optional(&state.db) + .await + .map_err(db_err)? + .map(|u| Json(UserResponse::from(u)).into_response()) + .ok_or((StatusCode::NOT_FOUND, "User not found".into())) +} + +pub async fn create_user( + State(state): State>, + Json(body): Json, +) -> Result { + let user = sqlx::query_as::<_, User>( + "INSERT INTO users (email, password_hash, username, display_name, avatar_url, bio, email_verified) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *", + ) + .bind(&body.email) + .bind(&body.password_hash) + .bind(&body.username) + .bind(&body.display_name) + .bind(&body.avatar_url) + .bind(&body.bio) + .bind(body.email_verified) + .fetch_one(&state.db) + .await + .map_err(db_err)?; + Ok((StatusCode::CREATED, Json(UserResponse::from(user)))) +} + +pub async fn update_user( + State(state): State>, + Path(id): Path, + Json(body): Json, +) -> Result { + sqlx::query_as::<_, User>( + "UPDATE users SET display_name = $1, avatar_url = $2, bio = $3, updated_at = NOW() + WHERE id = $4 AND deleted_at IS NULL RETURNING *", + ) + .bind(&body.display_name) + .bind(&body.avatar_url) + .bind(&body.bio) + .bind(id) + .fetch_optional(&state.db) + .await + .map_err(db_err)? + .map(|u| Json(UserResponse::from(u)).into_response()) + .ok_or((StatusCode::NOT_FOUND, "User not found".into())) +} + +pub async fn delete_user( + State(state): State>, + Path(id): Path, +) -> Result { + let res = sqlx::query("UPDATE users SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL") + .bind(id) + .execute(&state.db) + .await + .map_err(db_err)?; + if res.rows_affected() > 0 { + Ok(StatusCode::NO_CONTENT.into_response()) + } else { + Err((StatusCode::NOT_FOUND, "User not found".into())) + } +} diff --git a/src/handlers/workspace.rs b/src/handlers/workspace.rs new file mode 100644 index 0000000..6465af5 --- /dev/null +++ b/src/handlers/workspace.rs @@ -0,0 +1,84 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use std::sync::Arc; +use uuid::Uuid; +use crate::{model::Workspace, schema::WorkspaceSchema, AppState}; +use super::db_err; + +pub async fn list_workspaces( + State(state): State>, +) -> Result { + let workspaces = sqlx::query_as::<_, Workspace>("SELECT * FROM workspaces WHERE deleted_at IS NULL") + .fetch_all(&state.db) + .await + .map_err(db_err)?; + Ok(Json(workspaces)) +} + +pub async fn get_workspace( + State(state): State>, + Path(id): Path, +) -> Result { + sqlx::query_as::<_, Workspace>("SELECT * FROM workspaces WHERE id = $1 AND deleted_at IS NULL") + .bind(id) + .fetch_optional(&state.db) + .await + .map_err(db_err)? + .map(|w| Json(w).into_response()) + .ok_or((StatusCode::NOT_FOUND, "Workspace not found".into())) +} + +pub async fn create_workspace( + State(state): State>, + Json(body): Json, +) -> Result { + let workspace = sqlx::query_as::<_, Workspace>( + "INSERT INTO workspaces (name, slug, owner_id) VALUES ($1, $2, $3) RETURNING *", + ) + .bind(&body.name) + .bind(&body.slug) + .bind(body.owner_id) + .fetch_one(&state.db) + .await + .map_err(db_err)?; + Ok((StatusCode::CREATED, Json(workspace))) +} + +pub async fn update_workspace( + State(state): State>, + Path(id): Path, + Json(body): Json, +) -> Result { + sqlx::query_as::<_, Workspace>( + "UPDATE workspaces SET name = $1, slug = $2, updated_at = NOW() + WHERE id = $3 AND deleted_at IS NULL RETURNING *", + ) + .bind(&body.name) + .bind(&body.slug) + .bind(id) + .fetch_optional(&state.db) + .await + .map_err(db_err)? + .map(|w| Json(w).into_response()) + .ok_or((StatusCode::NOT_FOUND, "Workspace not found".into())) +} + +pub async fn delete_workspace( + State(state): State>, + Path(id): Path, +) -> Result { + let res = sqlx::query("UPDATE workspaces SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL") + .bind(id) + .execute(&state.db) + .await + .map_err(db_err)?; + if res.rows_affected() > 0 { + Ok(StatusCode::NO_CONTENT.into_response()) + } else { + Err((StatusCode::NOT_FOUND, "Workspace not found".into())) + } +} diff --git a/src/handlers/workspace_invitation.rs b/src/handlers/workspace_invitation.rs new file mode 100644 index 0000000..4299ebf --- /dev/null +++ b/src/handlers/workspace_invitation.rs @@ -0,0 +1,61 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use std::sync::Arc; +use uuid::Uuid; +use crate::{model::WorkspaceInvitation, schema::WorkspaceInvitationSchema, AppState}; +use super::db_err; + +pub async fn list_invitations( + State(state): State>, + Path(workspace_id): Path, +) -> Result { + let invitations = sqlx::query_as::<_, WorkspaceInvitation>( + "SELECT * FROM workspace_invitations WHERE workspace_id = $1", + ) + .bind(workspace_id) + .fetch_all(&state.db) + .await + .map_err(db_err)?; + Ok(Json(invitations)) +} + +pub async fn create_invitation( + State(state): State>, + Path(workspace_id): Path, + Json(body): Json, +) -> Result { + let token = Uuid::new_v4().to_string(); + let invitation = sqlx::query_as::<_, WorkspaceInvitation>( + "INSERT INTO workspace_invitations (workspace_id, created_by, token, max_uses, expires_at) + VALUES ($1, $2, $3, $4, $5) RETURNING *", + ) + .bind(workspace_id) + .bind(body.created_by) + .bind(&token) + .bind(body.max_uses) + .bind(body.expires_at) + .fetch_one(&state.db) + .await + .map_err(db_err)?; + Ok((StatusCode::CREATED, Json(invitation))) +} + +pub async fn delete_invitation( + State(state): State>, + Path(id): Path, +) -> Result { + let res = sqlx::query("DELETE FROM workspace_invitations WHERE id = $1") + .bind(id) + .execute(&state.db) + .await + .map_err(db_err)?; + if res.rows_affected() > 0 { + Ok(StatusCode::NO_CONTENT.into_response()) + } else { + Err((StatusCode::NOT_FOUND, "Invitation not found".into())) + } +} diff --git a/src/handlers/workspace_member.rs b/src/handlers/workspace_member.rs new file mode 100644 index 0000000..adf3d17 --- /dev/null +++ b/src/handlers/workspace_member.rs @@ -0,0 +1,82 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use std::sync::Arc; +use uuid::Uuid; +use crate::{model::WorkspaceMember, schema::WorkspaceMemberSchema, AppState}; +use super::db_err; + +pub async fn list_members( + State(state): State>, + Path(workspace_id): Path, +) -> Result { + let members = sqlx::query_as::<_, WorkspaceMember>( + "SELECT * FROM workspace_members WHERE workspace_id = $1", + ) + .bind(workspace_id) + .fetch_all(&state.db) + .await + .map_err(db_err)?; + Ok(Json(members)) +} + +pub async fn add_member( + State(state): State>, + Path(workspace_id): Path, + Json(body): Json, +) -> Result { + let member = sqlx::query_as::<_, WorkspaceMember>( + "INSERT INTO workspace_members (workspace_id, user_id, role, display_name, avatar_url) + VALUES ($1, $2, $3, $4, $5) RETURNING *", + ) + .bind(workspace_id) + .bind(body.user_id) + .bind(&body.role) + .bind(&body.display_name) + .bind(&body.avatar_url) + .fetch_one(&state.db) + .await + .map_err(db_err)?; + Ok((StatusCode::CREATED, Json(member))) +} + +pub async fn update_member( + State(state): State>, + Path((workspace_id, user_id)): Path<(Uuid, Uuid)>, + Json(body): Json, +) -> Result { + sqlx::query_as::<_, WorkspaceMember>( + "UPDATE workspace_members SET role = $1, display_name = $2, avatar_url = $3 + WHERE workspace_id = $4 AND user_id = $5 RETURNING *", + ) + .bind(&body.role) + .bind(&body.display_name) + .bind(&body.avatar_url) + .bind(workspace_id) + .bind(user_id) + .fetch_optional(&state.db) + .await + .map_err(db_err)? + .map(|m| Json(m).into_response()) + .ok_or((StatusCode::NOT_FOUND, "Member not found".into())) +} + +pub async fn remove_member( + State(state): State>, + Path((workspace_id, user_id)): Path<(Uuid, Uuid)>, +) -> Result { + let res = sqlx::query("DELETE FROM workspace_members WHERE workspace_id = $1 AND user_id = $2") + .bind(workspace_id) + .bind(user_id) + .execute(&state.db) + .await + .map_err(db_err)?; + if res.rows_affected() > 0 { + Ok(StatusCode::NO_CONTENT.into_response()) + } else { + Err((StatusCode::NOT_FOUND, "Member not found".into())) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..df2acc3 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,51 @@ +use std::sync::Arc; + +use axum::Router; + +mod handlers; +mod middleware; +mod model; +mod route; +mod schema; + +use dotenv::dotenv; +use sqlx::{PgPool, postgres::PgPoolOptions}; + +pub struct AppState { + pub db: PgPool, + pub jwt_secret: String, +} + +#[tokio::main] +async fn main() { + dotenv().ok(); + let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set!"); + let pool = match PgPoolOptions::new() + .max_connections(10) + .connect(&db_url) + .await + { + Ok(pool) => { + println!("Connected to DB successfully!"); + pool + } + Err(err) => { + println!("Failed to connect to DB: {}", err); + std::process::exit(1); + } + }; + + let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set!"); + let ip_addr: String = std::env::var("API_URL").expect("API_URL must be set!"); + let port: String = std::env::var("API_PORT").expect("API_PORT must be set!"); + let addr: String = format!("{}:{}", ip_addr, port); + + let state = Arc::new(AppState { db: pool, jwt_secret }); + let app: Router = Router::new() + .nest("/api/v1", route::router(state.clone())) + .with_state(state); + + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + println!("Server started successfully at {}", addr); + axum::serve(listener, app).await.unwrap(); +} diff --git a/src/middleware.rs b/src/middleware.rs new file mode 100644 index 0000000..98de49d --- /dev/null +++ b/src/middleware.rs @@ -0,0 +1,33 @@ +use axum::{ + extract::{Request, State}, + http::StatusCode, + middleware::Next, + response::Response, +}; +use jsonwebtoken::{decode, DecodingKey, Validation}; +use std::sync::Arc; + +use crate::{model::Claims, AppState}; + +pub async fn require_auth( + State(state): State>, + mut req: Request, + next: Next, +) -> Result { + let token = req + .headers() + .get("Authorization") + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.strip_prefix("Bearer ")) + .ok_or((StatusCode::UNAUTHORIZED, "Missing or invalid Authorization header".into()))?; + + let data = decode::( + token, + &DecodingKey::from_secret(state.jwt_secret.as_bytes()), + &Validation::default(), + ) + .map_err(|_| (StatusCode::UNAUTHORIZED, "Invalid or expired token".into()))?; + + req.extensions_mut().insert(data.claims); + Ok(next.run(req).await) +} diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..45b7016 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,173 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub exp: usize, +} + +// ── Enums ──────────────────────────────────────────────────────────────────── + +#[derive(Debug, Deserialize, Serialize, sqlx::Type)] +#[sqlx(type_name = "auth_token_kind", rename_all = "snake_case")] +pub enum AuthTokenKind { + PasswordReset, + EmailVerification, + RefreshToken, +} + +#[derive(Debug, Deserialize, Serialize, sqlx::Type)] +#[sqlx(type_name = "workspace_role", rename_all = "snake_case")] +pub enum WorkspaceRole { + Owner, + Admin, + Member, +} + +#[derive(Debug, Deserialize, Serialize, sqlx::Type)] +#[sqlx(type_name = "conversation_kind", rename_all = "snake_case")] +pub enum ConversationKind { + Dm, + Group, + Channel, +} + +// ── Tables ─────────────────────────────────────────────────────────────────── + +#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] +pub struct User { + pub id: Uuid, + pub email: String, + pub password_hash: String, + pub username: String, + pub display_name: String, + pub avatar_url: Option, + pub bio: Option, + pub email_verified: bool, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub deleted_at: Option>, +} + +#[derive(Debug, Serialize)] +pub struct UserResponse { + pub id: Uuid, + pub email: String, + pub username: String, + pub display_name: String, + pub avatar_url: Option, + pub bio: Option, + pub email_verified: bool, + pub created_at: chrono::DateTime, +} + +impl From for UserResponse { + fn from(u: User) -> Self { + Self { + id: u.id, + email: u.email, + username: u.username, + display_name: u.display_name, + avatar_url: u.avatar_url, + bio: u.bio, + email_verified: u.email_verified, + created_at: u.created_at, + } + } +} + +#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] +pub struct AuthToken { + pub id: Uuid, + pub user_id: Uuid, + pub token_hash: String, + pub kind: AuthTokenKind, + pub expires_at: chrono::DateTime, + pub used_at: Option>, + pub created_at: chrono::DateTime, +} + +#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] +pub struct Workspace { + pub id: Uuid, + pub name: String, + pub slug: String, + pub owner_id: Uuid, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub deleted_at: Option>, +} + +#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] +pub struct WorkspaceMember { + pub workspace_id: Uuid, + pub user_id: Uuid, + pub role: WorkspaceRole, + pub display_name: Option, + pub avatar_url: Option, + pub joined_at: chrono::DateTime, +} + +#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] +pub struct WorkspaceInvitation { + pub id: Uuid, + pub workspace_id: Uuid, + pub created_by: Uuid, + pub token: String, + pub max_uses: Option, + pub use_count: i32, + pub expires_at: Option>, + pub created_at: chrono::DateTime, +} + +#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] +pub struct Conversation { + pub id: Uuid, + pub workspace_id: Option, + pub kind: ConversationKind, + pub name: Option, + pub is_public: bool, + pub created_by: Uuid, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub deleted_at: Option>, +} + +#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] +pub struct ConversationMember { + pub conversation_id: Uuid, + pub user_id: Uuid, + pub joined_at: chrono::DateTime, +} + +#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] +pub struct Message { + pub id: Uuid, + pub conversation_id: Uuid, + pub user_id: Uuid, + pub content: String, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub deleted_at: Option>, +} + +#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] +pub struct Attachment { + pub id: Uuid, + pub message_id: Uuid, + pub url: String, + pub filename: String, + pub mime_type: String, + pub size_bytes: i64, + pub created_at: chrono::DateTime, +} + +#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] +pub struct Reaction { + pub id: Uuid, + pub message_id: Uuid, + pub user_id: Uuid, + pub emoji: String, + pub created_at: chrono::DateTime, +} diff --git a/src/route.rs b/src/route.rs new file mode 100644 index 0000000..9d431dc --- /dev/null +++ b/src/route.rs @@ -0,0 +1,107 @@ +use crate::handlers::{ + attachment, auth, auth_token, conversation, conversation_member, message, reaction, user, + workspace, workspace_invitation, workspace_member, +}; +use crate::middleware::require_auth; +use crate::AppState; +use axum::{ + middleware, + routing::{delete, get, post, put}, + Router, +}; +use std::sync::Arc; + +pub fn router(state: Arc) -> Router> { + let public = Router::new() + .route("/auth/register", post(auth::register)) + .route("/auth/login", post(auth::login)) + .route("/auth/refresh", post(auth::refresh)) + .route("/auth/logout", post(auth::logout)); + + let protected = Router::new() + // Users + .route("/users", get(user::list_users).post(user::create_user)) + .route( + "/users/{id}", + get(user::get_user) + .put(user::update_user) + .delete(user::delete_user), + ) + // Auth tokens (admin) + .route("/users/{id}/auth-tokens", get(auth_token::list_user_tokens)) + .route("/auth-tokens", post(auth_token::create_token)) + .route("/auth-tokens/{id}", delete(auth_token::revoke_token)) + // Workspaces + .route( + "/workspaces", + get(workspace::list_workspaces).post(workspace::create_workspace), + ) + .route( + "/workspaces/{id}", + get(workspace::get_workspace) + .put(workspace::update_workspace) + .delete(workspace::delete_workspace), + ) + // Workspace members + .route( + "/workspaces/{id}/members", + get(workspace_member::list_members).post(workspace_member::add_member), + ) + .route( + "/workspaces/{workspace_id}/members/{user_id}", + put(workspace_member::update_member).delete(workspace_member::remove_member), + ) + // Workspace invitations + .route( + "/workspaces/{id}/invitations", + get(workspace_invitation::list_invitations) + .post(workspace_invitation::create_invitation), + ) + .route( + "/invitations/{id}", + delete(workspace_invitation::delete_invitation), + ) + // Conversations + .route( + "/conversations", + get(conversation::list_conversations).post(conversation::create_conversation), + ) + .route( + "/conversations/{id}", + get(conversation::get_conversation) + .put(conversation::update_conversation) + .delete(conversation::delete_conversation), + ) + // Conversation members + .route( + "/conversations/{id}/members", + get(conversation_member::list_members).post(conversation_member::add_member), + ) + .route( + "/conversations/{conversation_id}/members/{user_id}", + delete(conversation_member::remove_member), + ) + // Messages + .route("/conversations/{id}/messages", get(message::list_messages)) + .route("/messages", post(message::create_message)) + .route( + "/messages/{id}", + get(message::get_message) + .put(message::update_message) + .delete(message::delete_message), + ) + // Attachments + .route( + "/messages/{id}/attachments", + get(attachment::list_attachments), + ) + .route("/attachments", post(attachment::create_attachment)) + .route("/attachments/{id}", delete(attachment::delete_attachment)) + // Reactions + .route("/messages/{id}/reactions", get(reaction::list_reactions)) + .route("/reactions", post(reaction::add_reaction)) + .route("/reactions/{id}", delete(reaction::remove_reaction)) + .layer(middleware::from_fn_with_state(state, require_auth)); + + Router::new().merge(public).merge(protected) +} diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..0fd17d8 --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,103 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::model::{AuthTokenKind, ConversationKind, WorkspaceRole}; + +#[derive(Deserialize)] +pub struct RegisterSchema { + pub email: String, + pub username: String, + pub display_name: String, + pub password: String, +} + +#[derive(Deserialize)] +pub struct LoginSchema { + pub email: String, + pub password: String, +} + +#[derive(Deserialize)] +pub struct RefreshSchema { + pub refresh_token: String, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct UserSchema { + pub email: String, + pub password_hash: String, + pub username: String, + pub display_name: String, + pub avatar_url: Option, + pub bio: Option, + pub email_verified: bool, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct AuthTokenSchema { + pub user_id: Uuid, + pub kind: AuthTokenKind, + pub expires_at: chrono::DateTime, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct WorkspaceSchema { + pub name: String, + pub slug: String, + pub owner_id: Uuid, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct WorkspaceMemberSchema { + pub workspace_id: Uuid, + pub user_id: Uuid, + pub role: WorkspaceRole, + pub display_name: Option, + pub avatar_url: Option, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct WorkspaceInvitationSchema { + pub workspace_id: Uuid, + pub created_by: Uuid, + pub max_uses: Option, + pub expires_at: Option>, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct ConversationSchema { + pub workspace_id: Option, + pub kind: ConversationKind, + pub name: Option, + pub is_public: bool, + pub created_by: Uuid, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct ConversationMemberSchema { + pub conversation_id: Uuid, + pub user_id: Uuid, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct MessageSchema { + pub conversation_id: Uuid, + pub user_id: Uuid, + pub content: String, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct AttachmentSchema { + pub message_id: Uuid, + pub url: String, + pub filename: String, + pub mime_type: String, + pub size_bytes: i64, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct ReactionSchema { + pub message_id: Uuid, + pub user_id: Uuid, + pub emoji: String, +}