From 1a2e0c58b671ff5e871c083d12afd3b2f948a576 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Mon, 21 Jul 2025 21:29:04 +0700 Subject: [PATCH] Add comprehensive tests for mentor repository and authentication - Implemented tests for creating, retrieving, updating, and deleting mentors in `mentor_repository_test.rs`. - Added tests for user authentication, including successful login, invalid email formats, and inactive users in `auth_login_tests.rs`. - Created a mock test environment setup in `mock_test.rs` to facilitate database operations during tests. - Updated module structure to include new test files for mentors and authentication. - Ensured cleanup of the database after tests to maintain isolation and prevent side effects. --- .env.example | 32 +- Cargo.lock | 1305 +++++++++-------- Cargo.toml | 37 +- Dockerfile | 2 +- imphnen-backend/Cargo.toml | 19 +- imphnen-backend/src/bin/api.rs | 1 - imphnen-backend/src/bin/clear_db_test.rs | 169 +++ imphnen-backend/src/bin/seed_events.rs | 31 +- imphnen-backend/src/bin/seed_gacha_rolls.rs | 55 + imphnen-backend/src/bin/seed_mentor_user.rs | 94 ++ imphnen-backend/src/bin/seed_permissions.rs | 11 +- imphnen-backend/src/bin/seed_roles.rs | 15 +- .../src/bin/seed_roles_permissions.rs | 158 +- imphnen-backend/src/bin/seed_users.rs | 8 +- imphnen-backend/src/bin/seeder.rs | 3 +- imphnen-backend/src/main.rs | 2 + imphnen-cms/Cargo.toml | 13 +- .../src/v1/landing/events/events_dto.rs | 25 +- .../v1/landing/events/events_repository.rs | 50 +- .../src/v1/landing/events/events_schema.rs | 8 +- .../src/v1/landing/events/events_service.rs | 165 ++- .../testimonials/testimonials_controller.rs | 1 - .../landing/testimonials/testimonials_dto.rs | 8 +- .../testimonials/testimonials_repository.rs | 52 +- .../testimonials/testimonials_schema.rs | 4 +- imphnen-dimentorin/Cargo.toml | 12 +- imphnen-dimentorin/src/lib.rs | 16 +- .../src/v1/mentors/mentors_controller.rs | 356 +++++ .../src/v1/mentors/mentors_dto.rs | 479 ++++++ .../src/v1/mentors/mentors_repository.rs | 299 ++++ .../src/v1/mentors/mentors_schema.rs | 250 ++++ .../src/v1/mentors/mentors_service.rs | 386 +++++ imphnen-dimentorin/src/v1/mentors/mod.rs | 30 + imphnen-dimentorin/src/v1/mod.rs | 7 + imphnen-dimentorin/src/v2/mod.rs | 1 + imphnen-entities/src/common_dto.rs | 6 +- imphnen-entities/src/error_dto.rs | 17 +- imphnen-gacha/Cargo.toml | 12 +- imphnen-gacha/src/lib.rs | 4 - .../gacha_claims/gacha_claims_repository.rs | 18 + .../gacha_credits/gacha_credits_repository.rs | 52 +- .../v1/gacha_items/gacha_items_repository.rs | 57 +- .../src/v1/gacha_items/gacha_items_service.rs | 7 +- .../v1/gacha_rolls/gacha_rolls_controller.rs | 29 + .../src/v1/gacha_rolls/gacha_rolls_dto.rs | 2 +- .../v1/gacha_rolls/gacha_rolls_repository.rs | 72 +- .../src/v1/gacha_rolls/gacha_rolls_service.rs | 8 + imphnen-gateway/Cargo.toml | 15 +- imphnen-gateway/src/docs.rs | 102 +- imphnen-gateway/src/lib.rs | 2 + imphnen-iam/Cargo.toml | 16 +- imphnen-iam/src/lib.rs | 28 - imphnen-iam/src/v1/auth/auth_controller.rs | 22 +- imphnen-iam/src/v1/auth/auth_dto.rs | 7 +- imphnen-iam/src/v1/auth/auth_repository.rs | 77 +- imphnen-iam/src/v1/auth/auth_service.rs | 259 +++- imphnen-iam/src/v1/auth/mod.rs | 1 + .../src/v1/permissions/permissions_enum.rs | 40 +- .../src/v1/permissions/permissions_guard.rs | 20 +- .../v1/permissions/permissions_repository.rs | 63 +- .../src/v1/permissions/permissions_service.rs | 6 +- imphnen-iam/src/v1/roles/roles_dto.rs | 13 + imphnen-iam/src/v1/roles/roles_enum.rs | 4 +- imphnen-iam/src/v1/roles/roles_repository.rs | 78 +- imphnen-iam/src/v1/users/users_dto.rs | 34 +- imphnen-iam/src/v1/users/users_repository.rs | 86 +- imphnen-iam/src/v1/users/users_schema.rs | 34 +- imphnen-iam/src/v1/users/users_service.rs | 46 +- imphnen-libs/Cargo.toml | 3 +- imphnen-libs/src/argon/mod.rs | 8 +- imphnen-libs/src/axum/mod.rs | 13 +- imphnen-libs/src/enviroment/mod.rs | 126 +- imphnen-libs/src/jsonwebtoken/mod.rs | 6 +- imphnen-libs/src/lettre/mod.rs | 10 +- imphnen-libs/src/surrealdb/mod.rs | 5 +- imphnen-libs/src/surrealdb/resource.rs | 4 +- imphnen-middleware/Cargo.toml | 8 +- imphnen-utils/Cargo.toml | 8 +- imphnen-utils/src/bind_filter.rs | 4 +- imphnen-utils/src/extract_email.rs | 17 +- imphnen-utils/src/generate_otp.rs | 2 +- imphnen-utils/src/get_id.rs | 3 +- imphnen-utils/src/lib.rs | 10 +- imphnen-utils/src/make_thing.rs | 2 +- imphnen-utils/src/mock_test.rs | 51 - imphnen-utils/src/query_builder.rs | 113 +- imphnen-utils/src/query_list.rs | 219 +-- imphnen-utils/src/response_format.rs | 2 +- imphnen-utils/src/serde_helpers.rs | 130 ++ test.sh | 905 ++++++++++++ tests/Cargo.toml | 33 +- .../mentors/mentor_registration_tests.rs | 758 ++++++++++ .../mentors/mentor_repository_test.rs | 349 +++++ tests/src/dimentorin/mentors/mod.rs | 4 + tests/src/dimentorin/mod.rs | 1 + tests/src/iam/auth/auth_login_tests.rs | 379 +++++ tests/src/iam/auth/auth_repository_test.rs | 146 +- tests/src/iam/auth/mod.rs | 2 + .../permissions_repository_test.rs | 60 +- tests/src/iam/roles/roles_repository_test.rs | 228 ++- tests/src/iam/users/users_repository_test.rs | 162 +- tests/src/lib.rs | 51 +- tests/src/mock_test.rs | 197 +++ 103 files changed, 7851 insertions(+), 1509 deletions(-) create mode 100644 imphnen-backend/src/bin/clear_db_test.rs create mode 100644 imphnen-backend/src/bin/seed_gacha_rolls.rs create mode 100644 imphnen-backend/src/bin/seed_mentor_user.rs create mode 100644 imphnen-dimentorin/src/v1/mentors/mentors_controller.rs create mode 100644 imphnen-dimentorin/src/v1/mentors/mentors_dto.rs create mode 100644 imphnen-dimentorin/src/v1/mentors/mentors_repository.rs create mode 100644 imphnen-dimentorin/src/v1/mentors/mentors_schema.rs create mode 100644 imphnen-dimentorin/src/v1/mentors/mentors_service.rs create mode 100644 imphnen-dimentorin/src/v1/mentors/mod.rs create mode 100644 imphnen-dimentorin/src/v1/mod.rs create mode 100644 imphnen-dimentorin/src/v2/mod.rs create mode 100644 imphnen-utils/src/serde_helpers.rs create mode 100644 test.sh create mode 100644 tests/src/dimentorin/mentors/mentor_registration_tests.rs create mode 100644 tests/src/dimentorin/mentors/mentor_repository_test.rs create mode 100644 tests/src/dimentorin/mentors/mod.rs create mode 100644 tests/src/dimentorin/mod.rs create mode 100644 tests/src/iam/auth/auth_login_tests.rs create mode 100644 tests/src/mock_test.rs diff --git a/.env.example b/.env.example index f5c2b84..1dce100 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,24 @@ -PORT= -SURREALDB_URL= -SURREALDB_USERNAME= -SURREALDB_PASSWORD= -SURREALDB_NAMESPACE= -SURREALDB_DBNAME= -ACCESS_TOKEN_SECRET= -REFRESH_TOKEN_SECRET= +RUST_ENV=development +PORT=4099 +SURREALDB_URL=ws://localhost:8000/rpc +SURREALDB_USERNAME=root +SURREALDB_PASSWORD=root +SURREALDB_NAMESPACE=test +SURREALDB_DBNAME=test +ACCESS_TOKEN_SECRET=your-access-token-secret-key-here +REFRESH_TOKEN_SECRET=your-refresh-token-secret-key-here +SMTP_EMAIL=your-email@example.com +SMTP_PASSWORD=your-smtp-password +SMTP_NAME="Your App Name" +SMTP_HOST=smtp.gmail.com +REDISDB_URL=localhost +FE_URL=http://localhost +MINIO_ENDPOINT=http://localhost:9000 +MINIO_BUCKET_NAME=default_bucket +MINIO_ACCESS_KEY=your-minio-access-key +MINIO_SECRET_KEY=your-minio-secret-key +MAIL_USER=your-email@example.com +MAIL_PASSWORD=your-smtp-password +MAIL_HOST=smtp.gmail.com +MAIL_PORT=465 +MAIL_SECURE=true diff --git a/Cargo.lock b/Cargo.lock index 94ec1f3..0e56b5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,9 +32,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "affinitypool" @@ -54,22 +54,22 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.2.15", + "getrandom 0.3.3", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -89,13 +89,13 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "ammonia" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab99eae5ee58501ab236beb6f20f6ca39be615267b014899c89b2f0bc18a459" +checksum = "d6b346764dd0814805de8abf899fe03065bcee69bb1a4771c785817e39f3978f" dependencies = [ + "cssparser", "html5ever", "maplit", - "once_cell", "tendril", "url", ] @@ -117,9 +117,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -132,44 +132,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] [[package]] name = "any_ascii" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea50b14b7a4b9343f8c627a7a53c52076482bd4bdad0a24fd3ec533ed616cc2c" +checksum = "90c6333e01ba7235575b6ab53e5af10f1c327927fd97c36462917e289557ea64" [[package]] name = "anyhow" @@ -249,9 +249,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -261,22 +261,23 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", + "pin-project-lite", "slab", ] [[package]] name = "async-graphql" -version = "7.0.16" +version = "7.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3ee559e72d983e7e04001ba3bf32e6b71c1d670595780723727fd8a29d36e87" +checksum = "036618f842229ba0b89652ffe425f96c7c16a49f7e3cb23b56fca7f61fd74980" dependencies = [ "async-graphql-derive", "async-graphql-parser", @@ -289,7 +290,7 @@ dependencies = [ "futures-timer", "futures-util", "http", - "indexmap 2.9.0", + "indexmap 2.10.0", "mime", "multer", "num-traits", @@ -304,9 +305,9 @@ dependencies = [ [[package]] name = "async-graphql-derive" -version = "7.0.16" +version = "7.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29db05b624fb6352fc11bfe30c54ab1b16a1fe937d7c05a783f4e88ef1292b3b" +checksum = "fd45deb3dbe5da5cdb8d6a670a7736d735ba65b455328440f236dfb113727a3d" dependencies = [ "Inflector", "async-graphql-parser", @@ -314,16 +315,16 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "strum", - "syn 2.0.100", + "strum 0.26.3", + "syn 2.0.104", "thiserror 1.0.69", ] [[package]] name = "async-graphql-parser" -version = "7.0.16" +version = "7.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4904895044116aab098ca82c6cec831ec43ed99efd04db9b70a390419bc88c5b" +checksum = "60b7607e59424a35dadbc085b0d513aa54ec28160ee640cf79ec3b634eba66d3" dependencies = [ "async-graphql-value", "pest", @@ -333,12 +334,12 @@ dependencies = [ [[package]] name = "async-graphql-value" -version = "7.0.16" +version = "7.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0cde74de18e3a00c5dd5cfa002ab6f532e1a06c2a79ee6671e2fc353b400b92" +checksum = "34ecdaff7c9cffa3614a9f9999bf9ee4c3078fe3ce4d6a6e161736b56febf2de" dependencies = [ "bytes", - "indexmap 2.9.0", + "indexmap 2.10.0", "serde", "serde_json", ] @@ -362,7 +363,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -379,7 +380,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -401,9 +402,9 @@ checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" @@ -492,9 +493,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -519,9 +520,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bcrypt" @@ -531,7 +532,7 @@ checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7" dependencies = [ "base64 0.22.1", "blowfish", - "getrandom 0.2.15", + "getrandom 0.2.16", "subtle", "zeroize", ] @@ -562,9 +563,9 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "bitvec" @@ -589,9 +590,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389a099b34312839e16420d499a9cad9650541715937ffbdd40d36f49e77eeb3" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" dependencies = [ "arrayref", "arrayvec", @@ -639,14 +640,14 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytecheck" @@ -672,9 +673,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" [[package]] name = "byteorder" @@ -699,18 +700,18 @@ checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba" [[package]] name = "castaway" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" dependencies = [ "rustversion", ] [[package]] name = "cc" -version = "1.2.18" +version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ "shlex", ] @@ -774,9 +775,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" @@ -848,9 +849,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "concurrent-queue" @@ -904,9 +905,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -938,9 +939,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" @@ -952,6 +953,29 @@ dependencies = [ "typenum", ] +[[package]] +name = "cssparser" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.104", +] + [[package]] name = "darling" version = "0.20.11" @@ -973,7 +997,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -984,7 +1008,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -1002,9 +1026,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "deranged" @@ -1024,14 +1048,14 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] name = "deunicode" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc55fe0d1f6c107595572ec8b107c0999bb1a2e0b75e37429a4fb0d6474a0e7d" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" [[package]] name = "diff" @@ -1079,15 +1103,16 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] name = "dmp" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a742337ba8f9745310573b60196a507e54e8c0267741763eb0ed89d87bd29ae" +checksum = "bb2dfc7a18dffd3ef60a442b72a827126f1557d914620f8fc4d1049916da43c1" dependencies = [ + "trice", "urlencoding", ] @@ -1103,6 +1128,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0d05e1c0dbad51b52c38bda7adceef61b9efc2baf04acfe8726a8c4630a6f57" +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dyn-clone" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" + [[package]] name = "earcutr" version = "0.4.3" @@ -1121,9 +1167,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "email-encoding" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20b9cde6a71f9f758440470f3de16db6c09a02c443ce66850d87f5410548fb8e" +checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" dependencies = [ "base64 0.22.1", "memchr", @@ -1190,12 +1236,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1221,9 +1267,9 @@ dependencies = [ [[package]] name = "ext-sort" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf73e44617eab501beba39234441a194cf138629d3b6447f81f573e1c3d0a13" +checksum = "cf5d3b056bcc471d38082b8c453acb6670f7327fd44219b3c411e40834883569" dependencies = [ "log", "rayon", @@ -1246,11 +1292,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -1387,7 +1434,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -1477,31 +1524,31 @@ dependencies = [ [[package]] name = "geographiclib-rs" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e5ed84f8089c70234b0a8e0aedb6dc733671612ddc0d37c6066052f9781960" +checksum = "f611040a2bb37eaa29a78a128d1e92a378a03e0b6e66ae27398d42b1ba9a7841" dependencies = [ "libm", ] [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "js-sys", @@ -1519,9 +1566,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "half" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", @@ -1551,15 +1598,15 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", "allocator-api2", ] [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", @@ -1584,9 +1631,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -1605,27 +1652,24 @@ dependencies = [ [[package]] name = "hostname" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" dependencies = [ "cfg-if", "libc", - "windows 0.52.0", + "windows-link", ] [[package]] name = "html5ever" -version = "0.27.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" dependencies = [ "log", - "mac", "markup5ever", - "proc-macro2", - "quote", - "syn 2.0.100", + "match_token", ] [[package]] @@ -1702,11 +1746,10 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", "http", "hyper", "hyper-util", @@ -1715,22 +1758,26 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 1.0.2", ] [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", "tokio", @@ -1750,7 +1797,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.0", + "windows-core 0.61.2", ] [[package]] @@ -1764,21 +1811,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -1787,31 +1835,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -1819,84 +1847,60 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "1.0.3" @@ -1910,20 +1914,14 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", ] -[[package]] -name = "if_chain" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" - [[package]] name = "imphnen-backend" version = "0.1.0" @@ -1933,7 +1931,9 @@ dependencies = [ "axum-test", "chrono", "env_logger", + "futures-util", "imphnen-cms", + "imphnen-dimentorin", "imphnen-entities", "imphnen-gateway", "imphnen-iam", @@ -1946,9 +1946,14 @@ dependencies = [ "serde_json", "surrealdb", "tokio", + "tokio-tungstenite", "tower-http", + "tracing", + "tracing-subscriber", + "url", "utoipa", "utoipa-swagger-ui", + "uuid", "validator", ] @@ -1965,6 +1970,7 @@ dependencies = [ "imphnen-libs", "imphnen-utils", "lazy_static", + "log", "rand 0.9.1", "regex", "serde", @@ -1972,6 +1978,7 @@ dependencies = [ "surrealdb", "tokio", "tower-http", + "tracing", "utoipa", "utoipa-swagger-ui", "validator", @@ -1985,7 +1992,10 @@ dependencies = [ "axum", "axum-test", "chrono", + "dotenvy", + "http-body-util", "imphnen-entities", + "imphnen-iam", "imphnen-libs", "imphnen-utils", "lazy_static", @@ -1996,6 +2006,7 @@ dependencies = [ "surrealdb", "tokio", "tower-http", + "tracing", "utoipa", "utoipa-swagger-ui", "validator", @@ -2025,6 +2036,7 @@ dependencies = [ "imphnen-libs", "imphnen-utils", "lazy_static", + "log", "rand 0.9.1", "rand_distr", "regex", @@ -2033,6 +2045,7 @@ dependencies = [ "surrealdb", "tokio", "tower-http", + "tracing", "utoipa", "utoipa-swagger-ui", "validator", @@ -2047,6 +2060,7 @@ dependencies = [ "axum-test", "chrono", "imphnen-cms", + "imphnen-dimentorin", "imphnen-entities", "imphnen-gacha", "imphnen-iam", @@ -2074,19 +2088,27 @@ dependencies = [ "axum", "axum-test", "chrono", + "dotenvy", + "http-body-util", "imphnen-entities", "imphnen-libs", "imphnen-utils", "lazy_static", + "log", + "once_cell", "rand 0.9.1", "regex", "serde", "serde_json", + "strum 0.27.1", + "strum_macros 0.27.1", "surrealdb", "tokio", "tower-http", + "tracing", "utoipa", "utoipa-swagger-ui", + "uuid", "validator", ] @@ -2147,7 +2169,11 @@ dependencies = [ "rand 0.9.1", "serde", "serde_json", + "strum 0.27.1", + "strum_macros 0.27.1", "surrealdb", + "tracing", + "uuid", "validator", ] @@ -2164,12 +2190,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "serde", ] @@ -2182,12 +2208,33 @@ dependencies = [ "generic-array", ] +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -2229,9 +2276,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "log", @@ -2242,13 +2289,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -2315,9 +2362,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lettre" -version = "0.11.16" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ffd14fa289730e3ad68edefdc31f603d56fe716ec38f2076bb7410e09147c2" +checksum = "cb2a0354e9ece2fcdcf9fa53417f6de587230c0c248068eb058fa26c4a753179" dependencies = [ "async-trait", "base64 0.22.1", @@ -2329,7 +2376,7 @@ dependencies = [ "futures-util", "hostname", "httpdate", - "idna 1.0.3", + "idna", "mime", "native-tls", "nom", @@ -2352,26 +2399,35 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.171" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" dependencies = [ "bitflags", "libc", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linfa-linalg" version = "0.1.0" @@ -2386,32 +2442,26 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", ] -[[package]] -name = "lockfree-object-pool" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" - [[package]] name = "log" version = "0.4.27" @@ -2424,9 +2474,15 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.15.4", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "mac" version = "0.1.1" @@ -2441,23 +2497,25 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" -version = "0.12.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" dependencies = [ "log", - "phf", - "phf_codegen", - "string_cache", - "string_cache_codegen", "tendril", + "web_atoms", ] [[package]] -name = "matches" -version = "0.1.10" +name = "match_token" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] [[package]] name = "matchit" @@ -2467,9 +2525,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "matrixmultiply" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" dependencies = [ "autocfg", "rawpointer", @@ -2487,9 +2545,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "miette" @@ -2511,7 +2569,7 @@ checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -2532,22 +2590,22 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.7" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] @@ -2664,6 +2722,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -2710,9 +2778,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ "hermit-abi", "libc", @@ -2729,9 +2797,9 @@ dependencies = [ [[package]] name = "object_store" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d94ac16b433c0ccf75326388c893d2835ab7457ea35ab8ba5d745c053ef5fa16" +checksum = "efc4f07659e11cd45a341cd24d71e683e3be65d9ff1f8150061678fe60437496" dependencies = [ "async-trait", "bytes", @@ -2758,10 +2826,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "openssl" -version = "0.10.72" +name = "once_cell_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ "bitflags", "cfg-if", @@ -2780,7 +2854,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -2791,9 +2865,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.107" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", @@ -2801,6 +2875,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking" version = "2.2.1" @@ -2809,9 +2889,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -2819,9 +2899,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -2883,9 +2963,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", "thiserror 2.0.12", @@ -2899,7 +2979,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.9.0", + "indexmap 2.10.0", ] [[package]] @@ -2952,7 +3032,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", "unicase", ] @@ -2992,9 +3072,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" @@ -3005,6 +3085,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3017,7 +3106,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.24", + "zerocopy", ] [[package]] @@ -3046,34 +3135,32 @@ dependencies = [ ] [[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ - "proc-macro-error-attr", "proc-macro2", "quote", - "syn 1.0.109", - "version_check", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "proc-macro-error2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" dependencies = [ + "proc-macro-error-attr2", "proc-macro2", "quote", - "version_check", + "syn 2.0.104", ] [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -3086,9 +3173,9 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" [[package]] name = "psm" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58e5423e24c18cc840e1c98370b3993c6649cd1678b4d24318bcf0a083cbe88" +checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" dependencies = [ "cc", ] @@ -3119,7 +3206,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb55a1aa7668676bb93926cd4e9cdfe60f03bb866553bcca9112554911b6d3dc" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", "equivalent", "hashbrown 0.14.5", "parking_lot", @@ -3127,21 +3214,21 @@ dependencies = [ [[package]] name = "quick_cache" -version = "0.6.12" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f8ed0655cbaf18a26966142ad23b95d8ab47221c50c4f73a1db7d0d2d6e3da8" +checksum = "6b450dad8382b1b95061d5ca1eb792081fb082adf48c678791fe917509596d5f" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "parking_lot", ] [[package]] name = "quinn" -version = "0.11.7" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", "cfg_aliases", @@ -3159,12 +3246,13 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.10" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" dependencies = [ "bytes", - "getrandom 0.3.2", + "getrandom 0.3.3", + "lru-slab", "rand 0.9.1", "ring", "rustc-hash", @@ -3179,9 +3267,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.11" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ "cfg_aliases", "libc", @@ -3208,9 +3296,9 @@ checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radium" @@ -3276,7 +3364,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -3285,7 +3373,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", ] [[package]] @@ -3326,15 +3414,15 @@ dependencies = [ [[package]] name = "reblessive" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffead9d0a0b45f3e0bc063a244b1779fd53a09d2c2f7282c186a016b1f10a778" +checksum = "bbc4a4ea2a66a41a1152c4b3d86e8954dc087bdf33af35446e6e176db4e73c8c" [[package]] name = "redox_syscall" -version = "0.5.11" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ "bitflags", ] @@ -3345,7 +3433,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "libredox", "thiserror 1.0.69", ] @@ -3367,7 +3455,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -3410,9 +3498,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64 0.22.1", "bytes", @@ -3424,17 +3512,13 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", - "ipnet", "js-sys", "log", - "mime", "mime_guess", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", "rustls", - "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", @@ -3444,21 +3528,21 @@ dependencies = [ "tokio-rustls", "tokio-util", "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", - "windows-registry", + "webpki-roots 1.0.2", ] [[package]] name = "reserve-port" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3747658ee2585ecf5607fa9887c92eff61b362ff5253dbf797dfeb73d33d78" +checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356" dependencies = [ "thiserror 2.0.12", ] @@ -3495,7 +3579,7 @@ checksum = "5f0ec466e5d8dca9965eb6871879677bef5590cf7525ad96cae14376efb75073" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -3506,7 +3590,7 @@ checksum = "d3415e1bc838c36f9a0a2ac60c0fa0851c72297685e66592c44870d82834dfa2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -3517,7 +3601,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -3597,9 +3681,9 @@ dependencies = [ [[package]] name = "robust" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf4a6aa5f6d6888f39e980649f3ad6b666acdce1d78e95b8a2cb076e687ae30" +checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" [[package]] name = "rstar" @@ -3614,9 +3698,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.6.0" +version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f" +checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -3625,22 +3709,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.6.0" +version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae" +checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.100", + "syn 2.0.104", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.6.0" +version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a" +checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" dependencies = [ "sha2", "walkdir", @@ -3673,9 +3757,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.37.1" +version = "1.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50" +checksum = "b203a6425500a03e0919c42d3c47caca51e79f1132046626d2c8871c5092035d" dependencies = [ "arrayvec", "borsh", @@ -3689,9 +3773,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustc-hash" @@ -3719,22 +3803,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "rustls" -version = "0.23.25" +version = "0.23.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" dependencies = [ "log", "once_cell", @@ -3745,29 +3829,21 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", + "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.1" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "ring", "rustls-pki-types", @@ -3776,9 +3852,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" @@ -3813,6 +3889,30 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3901,7 +4001,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -3910,7 +4010,7 @@ version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "itoa", "memchr", "ryu", @@ -3941,15 +4041,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.9.0", + "indexmap 2.10.0", + "schemars 0.9.0", + "schemars 1.0.4", "serde", "serde_derive", "serde_json", @@ -3959,14 +4061,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -3982,21 +4084,39 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -4029,18 +4149,15 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smol_str" @@ -4059,9 +4176,9 @@ checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -4069,11 +4186,11 @@ dependencies = [ [[package]] name = "spade" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ece03ff43cd2a9b57ebf776ea5e78bd30b3b4185a619f041079f4109f385034" +checksum = "a14e31a007e9f85c32784b04f89e6e194bb252a4d41b4a8ccd9e77245d901c8c" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.15.4", "num-traits", "robust", "smallvec", @@ -4093,9 +4210,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stacker" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601f9201feb9b09c00266478bf459952b9ef9a6b94edb2f21eba14ab681a60a9" +checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" dependencies = [ "cc", "cfg-if", @@ -4159,7 +4276,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +dependencies = [ + "strum_macros 0.27.1", ] [[package]] @@ -4172,7 +4298,20 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.100", + "syn 2.0.104", +] + +[[package]] +name = "strum_macros" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.104", ] [[package]] @@ -4183,9 +4322,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "surrealdb" -version = "2.3.2" +version = "2.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12a5a2d3f0d40d8b0bc2e3a327663c0cdffe7d35508ccabc810dab8281cf251d" +checksum = "5545940eb21920f4eb3fbdd4a805c68c9917e9ee95b805d7702c0a6cf61ed4d0" dependencies = [ "arrayvec", "async-channel", @@ -4194,8 +4333,8 @@ dependencies = [ "dmp", "futures", "geo", - "getrandom 0.3.2", - "indexmap 2.9.0", + "getrandom 0.3.3", + "indexmap 2.10.0", "path-clean", "pharos", "reblessive", @@ -4225,13 +4364,13 @@ dependencies = [ [[package]] name = "surrealdb-core" -version = "2.3.2" +version = "2.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "032f20c01cf3a5569f47c857da280abb7b6ff1e443bf58b993c3c28d2d802859" +checksum = "04c0c3d3e8e8156a1f15e3f146b8e40d7a0197e89abea6c4aa0178d06d11e6d1" dependencies = [ "addr", "affinitypool", - "ahash 0.8.11", + "ahash 0.8.12", "ammonia", "any_ascii", "argon2", @@ -4256,7 +4395,7 @@ dependencies = [ "fuzzy-matcher", "geo", "geo-types", - "getrandom 0.3.2", + "getrandom 0.3.3", "hex", "http", "ipnet", @@ -4321,15 +4460,15 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43d55edab1e65c7704486016f98e9eac61c97474921dbac094af2cd16e16c3" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", "bytes", "chrono", "crc32fast", "double-ended-peekable", - "getrandom 0.2.15", + "getrandom 0.2.16", "lru", "parking_lot", - "quick_cache 0.6.12", + "quick_cache 0.6.14", "revision 0.10.0", "vart 0.9.2", ] @@ -4347,9 +4486,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -4367,13 +4506,13 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -4387,7 +4526,7 @@ dependencies = [ "memchr", "ntapi", "rayon", - "windows 0.57.0", + "windows", ] [[package]] @@ -4398,12 +4537,12 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.19.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", @@ -4435,16 +4574,27 @@ dependencies = [ name = "tests" version = "0.1.0" dependencies = [ + "async-trait", "axum", - "axum-test", "chrono", + "dotenvy", + "http-body-util", + "hyper", + "hyper-util", + "imphnen-dimentorin", "imphnen-entities", "imphnen-iam", "imphnen-libs", "imphnen-utils", + "rand 0.9.1", + "serde", "serde_json", + "strum 0.27.1", "surrealdb", "tokio", + "tower", + "tracing", + "uuid", ] [[package]] @@ -4473,7 +4623,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -4484,17 +4634,16 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -4539,9 +4688,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -4564,15 +4713,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.0" +version = "1.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", + "slab", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -4586,7 +4739,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -4622,14 +4775,14 @@ dependencies = [ "tokio", "tokio-rustls", "tungstenite", - "webpki-roots", + "webpki-roots 0.26.11", ] [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -4641,17 +4794,17 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "toml_datetime", "winnow", ] @@ -4674,16 +4827,21 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.4" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "bitflags", "bytes", + "futures-util", "http", + "http-body", + "iri-string", "pin-project-lite", + "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -4712,22 +4870,48 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -4797,12 +4981,6 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" -[[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.18" @@ -4859,7 +5037,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 1.0.3", + "idna", "percent-encoding", ] @@ -4875,12 +5053,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -4895,11 +5067,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utoipa" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" +checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "serde", "serde_json", "utoipa-gen", @@ -4907,21 +5079,21 @@ dependencies = [ [[package]] name = "utoipa-gen" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" +checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] name = "utoipa-swagger-ui" -version = "9.0.1" +version = "9.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29519b3c485df6b13f4478ac909a491387e9ef70204487c3b64b53749aec0be" +checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55" dependencies = [ "axum", "base64 0.22.1", @@ -4937,54 +5109,52 @@ dependencies = [ [[package]] name = "uuid" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", "js-sys", + "rand 0.9.1", "serde", "wasm-bindgen", ] [[package]] name = "validator" -version = "0.12.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d6937c33ec6039d8071bcf72933146b5bbe378d645d8fa59bdadabfc2a249" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" dependencies = [ - "idna 0.2.3", - "lazy_static", + "idna", + "once_cell", "regex", "serde", "serde_derive", "serde_json", "url", "validator_derive", - "validator_types", ] [[package]] name = "validator_derive" -version = "0.12.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286b4497f270f59276a89ae0ad109d5f8f18c69b613e3fb22b61201aadb0c4d" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" dependencies = [ - "if_chain", - "lazy_static", - "proc-macro-error", + "darling", + "once_cell", + "proc-macro-error2", "proc-macro2", "quote", - "regex", - "syn 1.0.109", - "validator_types", + "syn 2.0.104", ] [[package]] -name = "validator_types" -version = "0.12.0" +name = "valuable" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9680608df133af2c1ddd5eaf1ddce91d60d61b6bc51494ef326458365a470a" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vart" @@ -5031,9 +5201,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -5066,7 +5236,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -5101,7 +5271,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5162,10 +5332,31 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "0.26.8" +name = "web_atoms" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.2", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] @@ -5201,16 +5392,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core 0.52.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.57.0" @@ -5221,15 +5402,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.57.0" @@ -5244,15 +5416,15 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", "windows-link", - "windows-result 0.3.2", - "windows-strings 0.4.0", + "windows-result 0.3.4", + "windows-strings", ] [[package]] @@ -5263,7 +5435,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -5274,7 +5446,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -5285,7 +5457,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -5296,25 +5468,14 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" - -[[package]] -name = "windows-registry" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" -dependencies = [ - "windows-result 0.3.2", - "windows-strings 0.3.1", - "windows-targets 0.53.0", -] +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-result" @@ -5327,27 +5488,18 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] @@ -5370,6 +5522,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -5388,9 +5549,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", @@ -5500,9 +5661,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] @@ -5516,23 +5677,17 @@ dependencies = [ "bitflags", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "ws_stream_wasm" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7999f5f4217fe3818726b66257a4475f71e74ffd190776ad053fa159e50737f5" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" dependencies = [ "async_io_stream", "futures", @@ -5541,7 +5696,7 @@ dependencies = [ "pharos", "rustc_version", "send_wrapper", - "thiserror 1.0.69", + "thiserror 2.0.12", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -5564,9 +5719,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -5576,54 +5731,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" -dependencies = [ - "zerocopy-derive 0.8.24", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] @@ -5643,7 +5778,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", "synstructure", ] @@ -5654,10 +5789,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] -name = "zerovec" -version = "0.10.4" +name = "zerotrie" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -5666,40 +5812,43 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.104", ] [[package]] name = "zip" -version = "2.6.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744" +checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" dependencies = [ "arbitrary", "crc32fast", - "crossbeam-utils", "flate2", - "indexmap 2.9.0", + "indexmap 2.10.0", "memchr", "zopfli", ] [[package]] -name = "zopfli" -version = "0.8.1" +name = "zlib-rs" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" dependencies = [ "bumpalo", "crc32fast", - "lockfree-object-pool", "log", - "once_cell", "simd-adler32", ] diff --git a/Cargo.toml b/Cargo.toml index 7dc3345..8507b4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,28 +20,55 @@ axum = { version = "0.8.4", features = ["multipart"] } log = "0.4.25" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.138" -tokio = { version = "1.45.0" } +tokio = { version = "1.45.0", features = ["full"] } argon2 = { version = "0.5.3", features = ["password-hash"] } jsonwebtoken = "9.3.1" chrono = "0.4.41" utoipa = { version = "5.3.1", features = ["axum_extras"] } utoipa-swagger-ui = { version = "9.0.0", features = ["axum"] } lettre = { version = "0.11.16", features = ["tokio1-native-tls"] } -surrealdb = { version = "2.3.2", features = ["kv-mem"] } +surrealdb = { version = "2.3.7", features = ["kv-mem", "kv-fdb"] } thiserror = "2.0.12" anyhow = "1.0.98" rand = { version = "0.9.1", features = ["std", "alloc"] } rand_distr = "0.5.1" -tower-http = { version = "0.6.4", features = ["cors"] } -validator = { version = "0.12", features = ["derive"] } +tower-http = { version = "0.6.4", features = ["cors", "trace"] } +http-body-util = "0.1.1" + +validator = { version = "0.20.0", features = ["derive"] } lazy_static = "1.4.0" regex = "1.11.1" axum-test = "17.2.0" -fancy-regex = "0.14.0" +fancy-regex = "0.15.0" futures = "0.3.31" tower = "0.5.2" env_logger = "0.11.8" dotenvy = "0.15.7" +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["fmt"] } +once_cell = "1.21.3" +uuid = { version = "1.8.0", features = ["v4", "fast-rng", "serde"] } +strum = { version = "0.27.1", features = ["derive"] } +strum_macros = "0.27.1" +hyper = "1.6.0" +hyper-util = "0.1.0" +async-trait = "0.1.75" +tokio-tungstenite = "0.23" +url = "2.5" +futures-util = "0.3" + +tests = { path = "./tests" } +imphnen-iam = { path = "./imphnen-iam" } +imphnen-cms = { path = "./imphnen-cms" } +imphnen-libs = { path = "./imphnen-libs" } +imphnen-utils = { path = "./imphnen-utils" } +imphnen-gacha = { path = "./imphnen-gacha" } +imphnen-gateway = { path = "./imphnen-gateway" } +imphnen-backend = { path = "./imphnen-backend" } +imphnen-entities = { path = "./imphnen-entities" } +imphnen-dimentorin = { path = "./imphnen-dimentorin" } +imphnen-middleware = { path = "./imphnen-middleware" } + [profile.release] lto = "fat" codegen-units = 1 diff --git a/Dockerfile b/Dockerfile index 528e944..897b227 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.85-alpine AS builder +FROM rust:1.86-alpine AS builder RUN apk add --no-cache \ curl \ diff --git a/imphnen-backend/Cargo.toml b/imphnen-backend/Cargo.toml index d9def1f..aa97468 100644 --- a/imphnen-backend/Cargo.toml +++ b/imphnen-backend/Cargo.toml @@ -4,12 +4,13 @@ version = "0.1.0" edition = "2021" [dependencies] -imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } -imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } -imphnen-gateway = { version = "0.1.0", path = "../imphnen-gateway" } -imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } -imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" } -imphnen-cms = { version = "0.1.0", path = "../imphnen-cms" } +imphnen-libs.workspace = true +imphnen-utils.workspace = true +imphnen-gateway.workspace = true +imphnen-entities.workspace = true +imphnen-iam.workspace = true +imphnen-cms.workspace = true +imphnen-dimentorin.workspace = true axum.workspace = true serde.workspace = true serde_json.workspace = true @@ -26,3 +27,9 @@ anyhow.workspace = true tower-http.workspace = true utoipa-swagger-ui.workspace = true env_logger.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +uuid.workspace=true +tokio-tungstenite.workspace = true +url.workspace = true +futures-util.workspace = true diff --git a/imphnen-backend/src/bin/api.rs b/imphnen-backend/src/bin/api.rs index b6b3c3a..1e0f00d 100644 --- a/imphnen-backend/src/bin/api.rs +++ b/imphnen-backend/src/bin/api.rs @@ -1,4 +1,3 @@ -use env_logger; use imphnen_gateway::gateway_service; use imphnen_libs::axum_init; diff --git a/imphnen-backend/src/bin/clear_db_test.rs b/imphnen-backend/src/bin/clear_db_test.rs new file mode 100644 index 0000000..6fe825c --- /dev/null +++ b/imphnen-backend/src/bin/clear_db_test.rs @@ -0,0 +1,169 @@ +use tokio_tungstenite::{connect_async, tungstenite::protocol::Message}; +use url::Url; +use futures_util::{StreamExt, SinkExt}; +use serde_json::json; + +// Menentukan kredensial dan detail koneksi secara langsung sebagai string statis +static SURREALDB_URL_WS: &str = "ws://localhost:8000/rpc"; +static SURREALDB_USERNAME: &str = "root"; +static SURREALDB_PASSWORD: &str = "root"; +static SURREALDB_NAMESPACE: &str = "test"; +static SURREALDB_DBNAME: &str = "test"; + +// Daftar tabel sebagai variabel static yang tidak dapat diubah +static TABLES_TO_CLEAR: &[&str] = &[ + "app_events", "users", "roles", "permissions", "gacha_rolls", + "mentor_users", "gacha_claims", "gacha_credits", "gacha_items", + "mentor_profiles", "roles_permissions", "testimonials", +]; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Tidak perlu memuat env lagi, karena kita menggunakan nilai hardcoded + // imphnen_libs::enviroment::load_env(); // Baris ini tidak lagi dibutuhkan + // let env = Env::new(); // Baris ini tidak lagi dibutuhkan + + println!("DEBUG: URL WS: {}", SURREALDB_URL_WS); + println!("DEBUG: Username: {}", SURREALDB_USERNAME); + println!("DEBUG: Namespace: {}", SURREALDB_NAMESPACE); + println!("DEBUG: Database: {}", SURREALDB_DBNAME); + + let url = Url::parse(SURREALDB_URL_WS)?; // Menggunakan SURREALDB_URL_WS statis + + let (ws_stream, _) = connect_async(url).await?; + let (mut write, mut read) = ws_stream.split(); + + // Authenticate (signin) + let signin_query = json!({ + "method": "signin", + "params": [{ + "user": SURREALDB_USERNAME, // Menggunakan SURREALDB_USERNAME statis + "pass": SURREALDB_PASSWORD, // Menggunakan SURREALDB_PASSWORD statis + }], + "id": 1 + }).to_string(); + println!("DEBUG: Sending signin query: {}", signin_query); + write.send(Message::Text(signin_query)).await?; + + let signin_response = read.next().await.ok_or("Failed to read signin response")?; + let signin_response_msg = signin_response?; + let signin_response_str = signin_response_msg.to_text()?; + println!("DEBUG: Signin response: {}", signin_response_str); + if signin_response_str.contains("\"error\":") { + return Err(format!("Signin failed: {}", signin_response_str).into()); + } + + // Use namespace and database + let use_query = json!({ + "method": "use", + "params": [SURREALDB_NAMESPACE, SURREALDB_DBNAME], // Menggunakan NS & DB statis + "id": 2 + }).to_string(); + println!("DEBUG: Sending use query: {}", use_query); + write.send(Message::Text(use_query)).await?; + + let use_response = read.next().await.ok_or("Failed to read use response")?; + let use_response_msg = use_response?; + let use_response_str = use_response_msg.to_text()?; + println!("DEBUG: Use response: {}", use_response_str); + if use_response_str.contains("\"error\":") { + return Err(format!("USE command failed: {}", use_response_str).into()); + } + + println!("INFO: Attempting to clear database tables via WebSocket..."); + + let mut all_clear = true; + for (i, table) in TABLES_TO_CLEAR.iter().enumerate() { + let remove_query = format!("REMOVE TABLE {};", table); + let query_json = json!({ + "method": "query", + "params": [remove_query], + "id": i + 3 + }).to_string(); + + println!("DEBUG: Attempting REMOVE TABLE {}: {}", table, query_json); + write.send(Message::Text(query_json)).await?; + let response_result = read.next().await.ok_or("Stream ended unexpectedly")?; + + + + match response_result { + Ok(msg) => { + let response_str = msg.to_text()?; + if response_str.contains("\"error\":") { + println!("WARN: Failed to REMOVE TABLE {}: {}. Attempting DELETE type::{}.", table, response_str, table); + let delete_all_query = format!("DELETE FROM {};", table); + let delete_all_json = json!({ + "method": "query", + "params": [delete_all_query], + "id": i + 300 + }).to_string(); + + println!("DEBUG: Attempting DELETE {}: {}", table, delete_all_json); + write.send(Message::Text(delete_all_json)).await?; + let delete_response_result = read.next().await.ok_or("Stream ended unexpectedly during DELETE type::")?; + + match delete_response_result { + Ok(delete_msg) => { + let delete_response_str = delete_msg.to_text()?; + if delete_response_str.contains("\"error\":") { + println!("ERROR: Failed to DELETE type::{} : {}", table, delete_response_str); + all_clear = false; + } else { + println!("INFO: Successfully DELETED type:: table: {}", table); + + } + }, + Err(delete_e) => { + println!("ERROR: Error receiving response for DELETE type:: table {}: {}", table, delete_e); + all_clear = false; + } + } + } else { + println!("INFO: Successfully REMOVED TABLE: {}", table); + + } + }, + Err(e) => { + println!("ERROR: Error receiving response for REMOVE TABLE {}: {}", table, e); + all_clear = false; + } + } + + // Check if table is empty after deletion attempt + let select_query = format!("SELECT * FROM {} LIMIT 1;", table); + let select_json = json!({ + "method": "query", + "params": [select_query], + "id": i + 1000 + }).to_string(); + write.send(Message::Text(select_json)).await?; + let select_response_result = read.next().await.ok_or("Stream ended unexpectedly during SELECT check")?; + match select_response_result { + Ok(select_msg) => { + let select_response_str = select_msg.to_text()?; + if select_response_str.contains("does not exist") { + println!("CHECK: Table '{}' does not exist after clear attempt (success).", table); + } else if select_response_str.contains("\"result\":[]") || select_response_str.contains("\"result\":[[]]") { + println!("CHECK: Table '{}' is empty after clear attempt.", table); + } else { + println!("WARNING: Table '{}' is NOT empty after clear attempt! Response: {}", table, select_response_str); + all_clear = false; + } + }, + Err(e) => { + println!("ERROR: Error receiving response for SELECT check on table {}: {}", table, e); + all_clear = false; + } + } + } + + println!("INFO: Database clearing complete."); + + if !all_clear { + eprintln!("ERROR: One or more tables could not be cleared. Check logs for details."); + return Err("Database clearing failed for one or more tables.".into()); + } + + Ok(()) +} \ No newline at end of file diff --git a/imphnen-backend/src/bin/seed_events.rs b/imphnen-backend/src/bin/seed_events.rs index 028243c..ef2667a 100644 --- a/imphnen-backend/src/bin/seed_events.rs +++ b/imphnen-backend/src/bin/seed_events.rs @@ -1,9 +1,9 @@ use imphnen_cms::v1::landing::events::events_schema::EventsSchema; +use imphnen_libs::enviroment::load_env; use imphnen_utils::{get_iso_date, Env}; use std::error::Error; use surrealdb::engine::any; -use surrealdb::{opt::auth::Root, sql::Thing}; -use imphnen_libs::enviroment::load_env; +use surrealdb::{opt::auth::Root, sql::Thing, Uuid}; // Added Uuid #[tokio::main] async fn main() -> Result<(), Box> { load_env(); @@ -20,7 +20,6 @@ async fn main() -> Result<(), Box> { let events = vec![ ( - "e1a2b3c4-5d6e-7f8g-9h0i-1j2k3l4m5n6o", "Tech Conference 2025", "Annual technology conference featuring the latest innovations in software development, AI, and cloud computing.", "https://techconf2025.example.com", @@ -31,7 +30,6 @@ async fn main() -> Result<(), Box> { "2025-06-17T18:00:00Z", ), ( - "f2b3c4d5-6e7f-8g9h-0i1j-2k3l4m5n6o7p", "Online Web Development Workshop", "Comprehensive workshop covering modern web development frameworks including React, Vue, and Angular.", "https://webdev-workshop.example.com", @@ -42,7 +40,6 @@ async fn main() -> Result<(), Box> { "2025-07-10T17:00:00Z", ), ( - "g3c4d5e6-7f8g-9h0i-1j2k-3l4m5n6o7p8q", "Startup Pitch Competition", "Exciting competition where emerging startups present their innovative ideas to a panel of expert judges and investors.", "https://startup-pitch.example.com", @@ -53,7 +50,6 @@ async fn main() -> Result<(), Box> { "2025-08-05T16:00:00Z", ), ( - "h4d5e6f7-8g9h-0i1j-2k3l-4m5n6o7p8q9r", "Digital Marketing Masterclass", "Learn advanced digital marketing strategies, social media optimization, and data-driven marketing techniques.", "https://digital-marketing.example.com", @@ -65,9 +61,20 @@ async fn main() -> Result<(), Box> { ), ]; - for (id, name, description, detail_link, price, location, is_online, start_date, end_date) in events { + for ( + name, + description, + detail_link, + price, + location, + is_online, + start_date, + end_date, + ) in events + { + let uuid = Uuid::new_v4().to_string(); // Generate new UUID let event = EventsSchema { - id: Thing::from(("app_events", id)), + id: Thing::from(("app_events", uuid.as_str())), // Use generated UUID name: name.into(), description: description.into(), detail_link: detail_link.into(), @@ -81,11 +88,15 @@ async fn main() -> Result<(), Box> { updated_at: get_iso_date(), }; - db.create::>(("app_events", id)) + db.create::>(("app_events", uuid.as_str())) // Use generated UUID .content(event) .await?; - println!("✅ Inserted event: {} ({})", name, if is_online { "Online" } else { "In-person" }); + println!( + "✅ Inserted event: {} ({})", + name, + if is_online { "Online" } else { "In-person" } + ); } println!("✅ All Events seeded"); diff --git a/imphnen-backend/src/bin/seed_gacha_rolls.rs b/imphnen-backend/src/bin/seed_gacha_rolls.rs new file mode 100644 index 0000000..839ddb6 --- /dev/null +++ b/imphnen-backend/src/bin/seed_gacha_rolls.rs @@ -0,0 +1,55 @@ +use imphnen_libs::enviroment::load_env; +use imphnen_utils::{get_iso_date, Env}; + +use std::error::Error; +use surrealdb::opt::auth::Root; +use surrealdb::sql::Thing; + +#[tokio::main] +async fn main() -> Result<(), Box> { + load_env(); + let env = Env::new(); + use surrealdb::engine::any; + let db = any::connect(&env.surrealdb_url).await?; + db.signin(Root { + username: &env.surrealdb_username, + password: &env.surrealdb_password, + }) + .await?; + db.use_ns(env.surrealdb_namespace) + .use_db(env.surrealdb_dbname) + .await?; + + db.query("DELETE type::thing('app_gacha_items', $id)") + .bind(("id", "gacha_item_test_id")) + .await?; + db.query("DELETE type::thing('app_gacha_rolls', $id)") + .bind(("id", "gacha_roll_test_id")) + .await?; + + let gacha_item_id = "gacha_item_test_id"; + db.query("CREATE type::thing('app_gacha_items', $id) SET name = $name, image_url = $image_url, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at") + .bind(("id", gacha_item_id)) + .bind(("name", "Test Gacha Item")) + .bind(("image_url", "https://example.com/gacha_item.png")) + .bind(("is_deleted", false)) + .bind(("created_at", get_iso_date())) + .bind(("updated_at", get_iso_date())) + .await?; + println!("Gacha Item seeded successfully!"); + + let gacha_roll_id = "gacha_roll_test_id"; + db.query("CREATE type::thing('app_gacha_rolls', $id) SET item = $item, quantity = $quantity, weight = $weight, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at") + .bind(("id", gacha_roll_id)) + .bind(("item", Thing::from(("app_gacha_items", gacha_item_id)))) + .bind(("quantity", 10)) + .bind(("weight", 1.0)) + .bind(("is_deleted", false)) + .bind(("created_at", get_iso_date())) + .bind(("updated_at", get_iso_date())) + .await?; + println!("Gacha Roll seeded successfully!"); + + println!("✅ Gacha items and rolls seeded."); + Ok(()) +} diff --git a/imphnen-backend/src/bin/seed_mentor_user.rs b/imphnen-backend/src/bin/seed_mentor_user.rs new file mode 100644 index 0000000..63cea4d --- /dev/null +++ b/imphnen-backend/src/bin/seed_mentor_user.rs @@ -0,0 +1,94 @@ +use imphnen_libs::enviroment::load_env; +use imphnen_utils::{get_iso_date, hash_password, Env}; +use serde_json::json; +use std::error::Error; +use surrealdb::opt::auth::Root; + +#[tokio::main] +async fn main() -> Result<(), Box> { + load_env(); + let env = Env::new(); + use surrealdb::engine::any; + let db = any::connect(&env.surrealdb_url).await?; + db.signin(Root { + username: &env.surrealdb_username, + password: &env.surrealdb_password, + }) + .await?; + db.use_ns(env.surrealdb_namespace) + .use_db(env.surrealdb_dbname) + .await?; + + db.query("DELETE type::thing('app_mentors', $id)") + .bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901")) + .await?; + + db.query("DELETE type::thing('app_users', $id)") + .bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901")) + .await?; + + use surrealdb::sql::Thing; + db.query("CREATE type::thing('app_users', $id) SET fullname = $fullname, email = $email, password = $password, avatar = $avatar, phone_number = $phone_number, is_active = $is_active, is_deleted = $is_deleted, mentor_id = $mentor_id, gender = $gender, birthdate = $birthdate, role = $role, created_at = $created_at, updated_at = $updated_at") + .bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901")) + .bind(("fullname", "Mentor User")) + .bind(("email", "mentor@example.com")) + .bind(("password", hash_password("password").unwrap())) + .bind(("avatar", Option::::None)) + .bind(("phone_number", "081234567890")) + .bind(("is_active", true)) + .bind(("is_deleted", false)) + .bind(("mentor_id", Option::::None)) + .bind(("gender", "male")) + .bind(("birthdate", "1990-05-15")) + .bind(("role", Thing::from(("app_roles", "3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a")))) + .bind(("created_at", get_iso_date())) + .bind(("updated_at", get_iso_date())) + .await?; + + db.query("CREATE type::thing('app_mentors', $id) SET user_id = $user_id, legal_name = $legal_name, identity_document_url = $identity_document_url, phone_for_verification = $phone_for_verification, bio = $bio, linkedin_url = $linkedin_url, github_url = $github_url, cv_url = $cv_url, industries = $industries, expertise = $expertise, languages = $languages, current_company = $current_company, current_role = $current_role, years_of_experience = $years_of_experience, topics_of_interest = $topics_of_interest, preferred_mentee_level = $preferred_mentee_level, preferred_mentoring_formats = $preferred_mentoring_formats, availability_commitment = $availability_commitment, mentoring_rate = $mentoring_rate, status = $status, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at, email = $email") + .bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901")) + .bind(("user_id", Thing::from(("app_users", "e6f78d23-83bf-5c2b-bcd4-001345678901")))) + .bind(("legal_name", "Mentor User")) + .bind(("identity_document_url", "https://example.com/ktp.jpg")) + .bind(("phone_for_verification", "081234567890")) + .bind(("bio", "Saya adalah mentor backend Rust dengan pengalaman 5 tahun dalam pengembangan aplikasi backend yang scalable dan performant.")) + .bind(("linkedin_url", "https://linkedin.com/in/mentor")) + .bind(("github_url", "https://github.com/mentor")) + .bind(("cv_url", Option::::None)) + .bind(("industries", vec!["Software", "Education"])) + .bind(("expertise", vec!["Rust", "Microservices"])) + .bind(("languages", vec!["Indonesian", "English"])) + .bind(("current_company", "PT Contoh")) + .bind(("current_role", "Senior Backend Engineer")) + .bind(("years_of_experience", 5)) + .bind(("topics_of_interest", vec!["Rust Programming", "Backend Development"])) + .bind(("preferred_mentee_level", vec!["beginner", "intermediate"])) + .bind(("preferred_mentoring_formats", vec!["online", "offline"])) + .bind(("availability_commitment", "2 jam per minggu untuk mentoring online dan offline")) + .bind(("mentoring_rate", json!({ + "amount": 100000, + "currency": "IDR", + "per_duration": "hour" + }))) + .bind(("status", "verified")) + .bind(("is_deleted", false)) + .bind(("created_at", get_iso_date())) + .bind(("updated_at", get_iso_date())) + .bind(("email", "mentor@example.com")) + .await?; + println!("Mentor created successfully!"); + println!("Updating user with mentor_id..."); + + db.query("UPDATE type::thing('app_users', $id) SET mentor_id = $mentor_id") + .bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901")) + .bind(( + "mentor_id", + Thing::from(("app_mentors", "e6f78d23-83bf-5c2b-bcd4-001345678901")), + )) + .await?; + println!("User updated with mentor_id successfully!"); + + println!("✅ Inserted mentor user: mentor@example.com"); + println!("✅ Mentor user seeded"); + Ok(()) +} diff --git a/imphnen-backend/src/bin/seed_permissions.rs b/imphnen-backend/src/bin/seed_permissions.rs index 33fbd9c..2859662 100644 --- a/imphnen-backend/src/bin/seed_permissions.rs +++ b/imphnen-backend/src/bin/seed_permissions.rs @@ -48,6 +48,14 @@ async fn main() -> Result<(), Box> { PermissionsEnum::ReadDetailGachaRolls, PermissionsEnum::CreateGachaRolls, PermissionsEnum::ExecuteGachaRolls, + PermissionsEnum::ReadListMentors, + PermissionsEnum::ReadDetailMentors, + PermissionsEnum::RegisterMentors, + PermissionsEnum::ReadOwnMentorProfile, + PermissionsEnum::UpdateOwnMentorProfile, + PermissionsEnum::ReadOwnMentorStatus, + PermissionsEnum::VerifyMentors, + PermissionsEnum::DeleteMentors, ] { db.query("CREATE type::thing('app_permissions', $id) CONTENT $data") .bind(("id", permission.id())) @@ -61,9 +69,10 @@ async fn main() -> Result<(), Box> { }), )) .await?; - println!("✅ Inserted: {}", permission.to_string()); + println!("✅ Inserted: {permission}"); } println!("✅ All Permissions seeded"); + Ok(()) } diff --git a/imphnen-backend/src/bin/seed_roles.rs b/imphnen-backend/src/bin/seed_roles.rs index f9da385..e4862ca 100644 --- a/imphnen-backend/src/bin/seed_roles.rs +++ b/imphnen-backend/src/bin/seed_roles.rs @@ -1,9 +1,9 @@ +use imphnen_libs::enviroment::load_env; use imphnen_utils::{get_iso_date, Env}; use serde_json::json; use std::error::Error; -use surrealdb::opt::auth::Root; -use imphnen_libs::enviroment::load_env; use surrealdb::engine::any; +use surrealdb::opt::auth::Root; #[tokio::main] async fn main() -> Result<(), Box> { load_env(); @@ -49,9 +49,18 @@ async fn main() -> Result<(), Box> { None, Some("2025-02-22T15:38:39.868306+00"), ), + ( + "3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a", + "Mentor", + None, + Some("2025-07-06T10:00:00.000000+00"), + ), ]; for (id, name, _created_at, _updated_at) in roles { + db.query("DELETE type::thing('app_roles', $id)") + .bind(("id", id)) + .await?; db.query("CREATE type::thing('app_roles', $id) CONTENT $data") .bind(("id", id)) .bind(( @@ -65,7 +74,7 @@ async fn main() -> Result<(), Box> { }), )) .await?; - println!("✅ Inserted role: {}", name); + println!("✅ Inserted role: {name}"); } println!("✅ All Roles seeded"); Ok(()) diff --git a/imphnen-backend/src/bin/seed_roles_permissions.rs b/imphnen-backend/src/bin/seed_roles_permissions.rs index a989b90..57b8904 100644 --- a/imphnen-backend/src/bin/seed_roles_permissions.rs +++ b/imphnen-backend/src/bin/seed_roles_permissions.rs @@ -1,8 +1,9 @@ use imphnen_iam::{get_iso_date, make_thing, Env, PermissionsEnum}; -use std::error::Error; -use surrealdb::opt::auth::Root; -use surrealdb::engine::any; use imphnen_libs::enviroment::load_env; +use std::error::Error; +use surrealdb::engine::any; +use surrealdb::opt::auth::Root; + #[tokio::main] async fn main() -> Result<(), Box> { load_env(); @@ -16,43 +17,118 @@ async fn main() -> Result<(), Box> { db.use_ns(env.surrealdb_namespace) .use_db(env.surrealdb_dbname) .await?; - let permission_refs_admin: Vec<_> = [ - PermissionsEnum::ReadListUsers, - PermissionsEnum::ReadDetailUsers, - PermissionsEnum::CreateUsers, - PermissionsEnum::DeleteUsers, - PermissionsEnum::UpdateUsers, - PermissionsEnum::ActivateUsers, - PermissionsEnum::ReadListRoles, - PermissionsEnum::ReadDetailRoles, - PermissionsEnum::CreateRoles, - PermissionsEnum::DeleteRoles, - PermissionsEnum::UpdateRoles, - PermissionsEnum::ReadListPermissions, - PermissionsEnum::ReadDetailPermissions, - PermissionsEnum::CreatePermissions, - PermissionsEnum::DeletePermissions, - PermissionsEnum::UpdatePermissions, - PermissionsEnum::CreateGachaClaims, - PermissionsEnum::ReadDetailGachaClaims, - PermissionsEnum::ReadListGachaItems, - PermissionsEnum::ReadDetailGachaItems, - PermissionsEnum::CreateGachaItems, - PermissionsEnum::DeleteGachaItems, - PermissionsEnum::UpdateGachaItems, - PermissionsEnum::ReadDetailGachaRolls, - PermissionsEnum::CreateGachaRolls, - PermissionsEnum::ExecuteGachaRolls, - ] - .iter() - .map(|perm| make_thing("app_permissions", perm.id())) - .collect(); - let admin_role_id = "f6b03f25-e416-4893-ac88-caaa690afb07"; - db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false") - .bind(("role_id", admin_role_id)) - .bind(("permissions", permission_refs_admin)) - .bind(("updated_at", get_iso_date())) - .await?; - println!("✅ All permissions successfully added to Admin role"); + + let roles_permissions = vec![ + ( + "f6b03f25-e416-4893-ac88-caaa690afb07", + vec![ + PermissionsEnum::ReadListUsers, + PermissionsEnum::ReadDetailUsers, + PermissionsEnum::CreateUsers, + PermissionsEnum::DeleteUsers, + PermissionsEnum::UpdateUsers, + PermissionsEnum::ActivateUsers, + PermissionsEnum::ReadListRoles, + PermissionsEnum::ReadDetailRoles, + PermissionsEnum::CreateRoles, + PermissionsEnum::DeleteRoles, + PermissionsEnum::UpdateRoles, + PermissionsEnum::ReadListPermissions, + PermissionsEnum::ReadDetailPermissions, + PermissionsEnum::CreatePermissions, + PermissionsEnum::DeletePermissions, + PermissionsEnum::UpdatePermissions, + PermissionsEnum::CreateGachaClaims, + PermissionsEnum::ReadDetailGachaClaims, + PermissionsEnum::ReadListGachaItems, + PermissionsEnum::ReadDetailGachaItems, + PermissionsEnum::CreateGachaItems, + PermissionsEnum::DeleteGachaItems, + PermissionsEnum::UpdateGachaItems, + PermissionsEnum::ReadDetailGachaRolls, + PermissionsEnum::CreateGachaRolls, + PermissionsEnum::ExecuteGachaRolls, + PermissionsEnum::ReadListMentors, + PermissionsEnum::ReadDetailMentors, + PermissionsEnum::RegisterMentors, + PermissionsEnum::UpdateMentors, + PermissionsEnum::VerifyMentors, + PermissionsEnum::DeleteMentors, + ], + ), + ( + "3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a", + vec![ + PermissionsEnum::ReadOwnMentorProfile, + PermissionsEnum::UpdateOwnMentorProfile, + PermissionsEnum::ReadOwnMentorStatus, + PermissionsEnum::ReadListMentors, + PermissionsEnum::ReadDetailMentors, + PermissionsEnum::ReadListGachaItems, + PermissionsEnum::ReadDetailGachaItems, + PermissionsEnum::ReadDetailGachaRolls, + PermissionsEnum::CreateGachaRolls, + PermissionsEnum::ExecuteGachaRolls, + ], + ), + ( + "5713cb37-dc02-4e87-8048-d7a41d352059", + vec![ + PermissionsEnum::ReadListGachaItems, + PermissionsEnum::ReadDetailGachaItems, + PermissionsEnum::CreateGachaClaims, + PermissionsEnum::ReadDetailGachaClaims, + PermissionsEnum::ReadDetailGachaRolls, + PermissionsEnum::CreateGachaRolls, + PermissionsEnum::ExecuteGachaRolls, + PermissionsEnum::RegisterMentors, + PermissionsEnum::ReadListMentors, + PermissionsEnum::ReadDetailMentors, + PermissionsEnum::ReadOwnMentorProfile, + PermissionsEnum::ReadOwnMentorStatus, + ], + ), + ( + "50133429-f4b1-4249-9f97-7b86e6ee9d86", + vec![ + PermissionsEnum::ReadListUsers, + PermissionsEnum::ReadListMentors, + PermissionsEnum::ReadDetailUsers, + PermissionsEnum::ActivateUsers, + PermissionsEnum::ReadListRoles, + PermissionsEnum::ReadDetailRoles, + PermissionsEnum::ReadListPermissions, + PermissionsEnum::ReadDetailPermissions, + PermissionsEnum::ReadListGachaItems, + PermissionsEnum::ReadDetailGachaItems, + PermissionsEnum::ReadListMentors, + PermissionsEnum::ReadDetailMentors, + PermissionsEnum::ReadDetailGachaRolls, + PermissionsEnum::CreateGachaRolls, + PermissionsEnum::ExecuteGachaRolls, + ], + ), + ( + "60f1aeb7-dad2-4e06-bcb5-be1ba510c906", + vec![PermissionsEnum::ActivateUsers], + ), + ("6d4fea5d-4a08-4b8a-9782-f2ab2183dcf0", vec![]), + ]; + + for (role_id, permissions) in roles_permissions { + let permission_refs: Vec<_> = permissions + .iter() + .map(|perm| make_thing("app_permissions", perm.id())) + .collect(); + + db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false") + .bind(("role_id", role_id)) + .bind(("permissions", permission_refs)) + .bind(("updated_at", get_iso_date())) + .await?; + println!("✅ Permissions updated for role: {role_id}"); + } + + println!("✅ All roles permissions updated!"); Ok(()) } diff --git a/imphnen-backend/src/bin/seed_users.rs b/imphnen-backend/src/bin/seed_users.rs index 23ee9b9..05468ec 100644 --- a/imphnen-backend/src/bin/seed_users.rs +++ b/imphnen-backend/src/bin/seed_users.rs @@ -2,6 +2,7 @@ use imphnen_iam::UsersSchema; use imphnen_libs::enviroment::load_env; use imphnen_utils::{get_iso_date, hash_password, Env}; use std::error::Error; + use surrealdb::{opt::auth::Root, sql::Thing}; #[tokio::main] async fn main() -> Result<(), Box> { @@ -40,6 +41,10 @@ async fn main() -> Result<(), Box> { ]; for (id, email, fullname, role_id) in users { + db.query("DELETE type::thing('app_users', $id)") + .bind(("id", id)) + .await?; + let user = UsersSchema { id: Thing::from(("app_users", id)), fullname: fullname.into(), @@ -49,6 +54,7 @@ async fn main() -> Result<(), Box> { phone_number: "081234567890".into(), is_active: true, is_deleted: false, + mentor_id: None, gender: None, birthdate: None, role: Thing::from(("app_roles", role_id)), @@ -60,7 +66,7 @@ async fn main() -> Result<(), Box> { .content(user) .await?; - println!("✅ Inserted user: {} ({})", fullname, email); + println!("✅ Inserted user: {fullname} ({email})"); } println!("✅ All Users seeded"); diff --git a/imphnen-backend/src/bin/seeder.rs b/imphnen-backend/src/bin/seeder.rs index 116db22..5562271 100644 --- a/imphnen-backend/src/bin/seeder.rs +++ b/imphnen-backend/src/bin/seeder.rs @@ -20,7 +20,8 @@ fn main() -> Result<(), Box> { run_seed("seed_roles_permissions")?; run_seed("seed_users")?; run_seed("seed_events")?; - + run_seed("seed_gacha_rolls")?; + run_seed("seed_mentor_user")?; println!("\n✅ All seeding completed successfully."); Ok(()) } diff --git a/imphnen-backend/src/main.rs b/imphnen-backend/src/main.rs index a261ef9..09d07a9 100644 --- a/imphnen-backend/src/main.rs +++ b/imphnen-backend/src/main.rs @@ -3,6 +3,8 @@ use imphnen_libs::axum_init; #[tokio::main] async fn main() { + tracing_subscriber::fmt::init(); + axum_init(|surrealdb_ws, surrealdb_mem| async { gateway_service(surrealdb_ws, surrealdb_mem).await }) diff --git a/imphnen-cms/Cargo.toml b/imphnen-cms/Cargo.toml index 21d91c9..bdb4122 100644 --- a/imphnen-cms/Cargo.toml +++ b/imphnen-cms/Cargo.toml @@ -4,10 +4,10 @@ version = "0.1.0" edition = "2024" [dependencies] -imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" } -imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } -imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } -imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } +imphnen-iam.workspace = true +imphnen-libs.workspace = true +imphnen-utils.workspace = true +imphnen-entities.workspace = true axum.workspace = true serde.workspace = true serde_json.workspace = true @@ -23,3 +23,8 @@ chrono.workspace = true anyhow.workspace = true tower-http.workspace = true utoipa-swagger-ui.workspace = true +log.workspace = true +tracing.workspace = true + +[package.metadata.validator.regex] +VALID_URL_REGEX = "^https?://" diff --git a/imphnen-cms/src/v1/landing/events/events_dto.rs b/imphnen-cms/src/v1/landing/events/events_dto.rs index d39e88e..c2f9e3d 100644 --- a/imphnen-cms/src/v1/landing/events/events_dto.rs +++ b/imphnen-cms/src/v1/landing/events/events_dto.rs @@ -1,16 +1,14 @@ -use chrono::{DateTime, Utc}; use lazy_static::lazy_static; -use regex::Regex; +lazy_static! { + pub static ref VALID_URL_REGEX: regex::Regex = + regex::Regex::new(r"^https?://").unwrap(); +} +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use surrealdb::sql::Thing; use utoipa::ToSchema; use validator::Validate; -// Lazy static regex for URL validation -lazy_static! { - static ref VALID_URL_REGEX: Regex = Regex::new(r"^https?://").unwrap(); -} - #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct EventsCreateRequestDto { #[validate(length(min = 1, message = "Name is required"))] @@ -19,13 +17,10 @@ pub struct EventsCreateRequestDto { #[validate(length(min = 1, message = "Description is required"))] pub description: String, - #[validate(regex( - path = "VALID_URL_REGEX", - message = "Detail link must be a valid URL" - ))] + #[validate(url(message = "Detail link must be a valid URL"))] pub detail_link: String, - #[validate(range(min = 0, message = "Price cannot be negative"))] + #[validate(range(min = 0.0, message = "Price cannot be negative"))] pub price: f64, #[schema(example = "2025-09-20T13:00:00Z", value_type = String)] @@ -42,16 +37,18 @@ pub struct EventsCreateRequestDto { pub struct EventsUpdateRequestDto { #[validate(length(min = 1, message = "Name is required"))] pub name: String, - + #[schema(example = "2025-09-20T13:00:00Z", value_type = String)] pub end_date: DateTime, #[schema(example = "2025-09-20T13:00:00Z", value_type = String)] pub start_date: DateTime, - + + #[validate(range(min = 0.0, message = "Price cannot be negative"))] pub price: f64, pub is_online: bool, pub description: String, + #[validate(url(message = "Detail link must be a valid URL"))] pub detail_link: String, pub location: Option, } diff --git a/imphnen-cms/src/v1/landing/events/events_repository.rs b/imphnen-cms/src/v1/landing/events/events_repository.rs index 6746a59..c4ed1f2 100644 --- a/imphnen-cms/src/v1/landing/events/events_repository.rs +++ b/imphnen-cms/src/v1/landing/events/events_repository.rs @@ -2,6 +2,8 @@ use super::{events_dto::EventsQueryDto, events_schema::EventsSchema}; use anyhow::{Result, bail}; use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto}; use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date}; +use std::time::Instant; +use tracing::instrument; pub struct EventsRepository<'a> { state: &'a AppState, @@ -12,17 +14,25 @@ impl<'a> EventsRepository<'a> { Self { state } } + #[instrument(skip(self, meta), err)] pub async fn query_event_list( &self, meta: MetaRequestDto, ) -> Result>> { - let query = ListQueryBuilder::new(&ResourceEnum::Events.to_string()) + let now = Instant::now(); + let query = ListQueryBuilder::new(ResourceEnum::Events.to_string()) .with_select_fields(vec!["*"]) .with_pagination(meta.page, Some(10)) .with_sorting(meta.sort_by.as_deref(), meta.order.as_deref()) .build(); let res: Vec = self.state.surrealdb_ws.query(query).await?.take(0)?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_event_list' took: {elapsed:.2?}"); + } let data = ResponseListSuccessDto { data: res, meta: None, @@ -30,8 +40,9 @@ impl<'a> EventsRepository<'a> { Ok(data) } - // Get event by ID + #[instrument(skip(self, id), err)] pub async fn query_event_by_id(&self, id: String) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let builder = DetailQueryBuilder::new(ResourceEnum::Events.to_string()) .with_id(&id) @@ -39,6 +50,12 @@ impl<'a> EventsRepository<'a> { let sql = builder.build(); let result: Option = builder.apply_bindings(db.query(sql)).await?.take(0)?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_event_by_id' took: {elapsed:.2?}"); + } match result { Some(event) => { @@ -51,13 +68,20 @@ impl<'a> EventsRepository<'a> { } } - // Create new event + #[instrument(skip(self, data), err)] pub async fn query_create_event(&self, data: EventsSchema) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let record: Option = db .create(ResourceEnum::Events.to_string()) .content(data) .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_create_event' took: {elapsed:.2?}"); + } match record { Some(_) => Ok("Success create event".into()), @@ -65,17 +89,16 @@ impl<'a> EventsRepository<'a> { } } - // Update existing event + #[instrument(skip(self, data), err)] pub async fn query_update_event(&self, data: EventsSchema) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; - // Cek apakah event ada let existing = self.query_event_by_id(data.id.id.to_raw()).await?; if existing.is_deleted { bail!("Event already deleted"); } - // Merge field tertentu jika diperlukan let merged = EventsSchema { created_at: existing.created_at, updated_at: get_iso_date(), @@ -84,6 +107,12 @@ impl<'a> EventsRepository<'a> { let record_key = get_id(&merged.id)?; let record: Option = db.update(record_key).merge(merged).await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_update_event' took: {elapsed:.2?}"); + } match record { Some(_) => Ok("Success update event".into()), @@ -91,8 +120,9 @@ impl<'a> EventsRepository<'a> { } } - // Soft delete event (mark is_deleted = true) + #[instrument(skip(self, id), err)] pub async fn query_delete_event(&self, id: String) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let event = self.query_event_by_id(id).await?; if event.is_deleted { @@ -104,6 +134,12 @@ impl<'a> EventsRepository<'a> { .update(record_key) .merge(serde_json::json!({ "is_deleted": true })) .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_delete_event' took: {elapsed:.2?}"); + } match record { Some(_) => Ok("Success delete event".into()), diff --git a/imphnen-cms/src/v1/landing/events/events_schema.rs b/imphnen-cms/src/v1/landing/events/events_schema.rs index 3f69b9d..43a0a0d 100644 --- a/imphnen-cms/src/v1/landing/events/events_schema.rs +++ b/imphnen-cms/src/v1/landing/events/events_schema.rs @@ -1,10 +1,12 @@ use imphnen_libs::ResourceEnum; +use imphnen_utils::{get_iso_date, make_thing}; +use serde::{Deserialize, Serialize}; use surrealdb::Uuid; use surrealdb::sql::Thing; -use serde::{Deserialize, Serialize}; -use imphnen_utils::{get_iso_date, make_thing}; -use super::events_dto::{EventsCreateRequestDto, EventsQueryDto, EventsUpdateRequestDto}; +use super::events_dto::{ + EventsCreateRequestDto, EventsQueryDto, EventsUpdateRequestDto, +}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct EventsSchema { diff --git a/imphnen-cms/src/v1/landing/events/events_service.rs b/imphnen-cms/src/v1/landing/events/events_service.rs index 71af4e8..a46291b 100644 --- a/imphnen-cms/src/v1/landing/events/events_service.rs +++ b/imphnen-cms/src/v1/landing/events/events_service.rs @@ -1,86 +1,101 @@ use super::{ - events_dto::{EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsQueryDto, EventsUpdateRequestDto}, - events_repository::EventsRepository, - events_schema::EventsSchema, + events_dto::{ + EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsQueryDto, + EventsUpdateRequestDto, + }, + events_repository::EventsRepository, + events_schema::EventsSchema, }; -use imphnen_libs::{AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto}; -use imphnen_utils::{common_response, success_list_response, success_response, validate_request}; use axum::{http::StatusCode, response::Response}; +use imphnen_libs::{ + AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto, +}; +use imphnen_utils::{ + common_response, success_list_response, success_response, validate_request, +}; pub struct EventsService; impl EventsService { - pub async fn get_event_list(state: &AppState, meta: MetaRequestDto) -> Response { - let repo = EventsRepository::new(state); - match repo.query_event_list(meta).await { - Ok(data) => { - let items: Vec = data.data - .into_iter() - .filter(|e| !e.is_deleted) - .map(EventsQueryDto::from) - .collect(); - let response = ResponseListSuccessDto { - data: items, - meta: data.meta, - }; - success_list_response(response) - } - Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), - } - } + pub async fn get_event_list(state: &AppState, meta: MetaRequestDto) -> Response { + let repo = EventsRepository::new(state); + match repo.query_event_list(meta).await { + Ok(data) => { + let items: Vec = data + .data + .into_iter() + .filter(|e| !e.is_deleted) + .map(EventsQueryDto::from) + .collect(); + let response = ResponseListSuccessDto { + data: items, + meta: data.meta, + }; + success_list_response(response) + } + Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), + } + } - pub async fn get_event_by_id(state: &AppState, id: String) -> Response { - let repo = EventsRepository::new(state); - match repo.query_event_by_id(id).await { - Ok(event) if !event.is_deleted => success_response(ResponseSuccessDto { - data: EventsDetailItemDto { - id: event.id.id.to_raw(), - name: event.name, - description: event.description, - detail_link: event.detail_link, - price: event.price, - is_online: event.is_online, - start_date: event.start_date, - end_date: event.end_date, - created_at: event.created_at, - updated_at: event.updated_at, - location: event.location, - }, - }), - Ok(_) => common_response(StatusCode::NOT_FOUND, "Event not found"), - Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()), - } - } + pub async fn get_event_by_id(state: &AppState, id: String) -> Response { + let repo = EventsRepository::new(state); + match repo.query_event_by_id(id).await { + Ok(event) if !event.is_deleted => success_response(ResponseSuccessDto { + data: EventsDetailItemDto { + id: event.id.id.to_raw(), + name: event.name, + description: event.description, + detail_link: event.detail_link, + price: event.price, + is_online: event.is_online, + start_date: event.start_date, + end_date: event.end_date, + created_at: event.created_at, + updated_at: event.updated_at, + location: event.location, + }, + }), + Ok(_) => common_response(StatusCode::NOT_FOUND, "Event not found"), + Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()), + } + } - pub async fn create_event(state: &AppState, payload: EventsCreateRequestDto) -> Response { - if let Err((status, message)) = validate_request(&payload) { - return common_response(status, &message); - } - let repo = EventsRepository::new(state); - let schema = EventsSchema::create(payload); - match repo.query_create_event(schema).await { - Ok(msg) => common_response(StatusCode::CREATED, &msg), - Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), - } - } + pub async fn create_event( + state: &AppState, + payload: EventsCreateRequestDto, + ) -> Response { + if let Err((status, message)) = validate_request(&payload) { + return common_response(status, &message); + } + let repo = EventsRepository::new(state); + let schema = EventsSchema::create(payload); + match repo.query_create_event(schema).await { + Ok(msg) => common_response(StatusCode::CREATED, &msg), + Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), + } + } - pub async fn update_event(state: &AppState, id: String, payload: EventsUpdateRequestDto) -> Response { - if let Err((status, message)) = validate_request(&payload) { - return common_response(status, &message); - } - let repo = EventsRepository::new(state); - let schema = EventsSchema::update(payload, id); - match repo.query_update_event(schema).await { - Ok(msg) => common_response(StatusCode::OK, &msg), - Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), - } - } + pub async fn update_event( + state: &AppState, + id: String, + payload: EventsUpdateRequestDto, + ) -> Response { + if let Err((status, message)) = validate_request(&payload) { + return common_response(status, &message); + } + let repo = EventsRepository::new(state); + let schema = EventsSchema::update(payload, id); + match repo.query_update_event(schema).await { + Ok(msg) => common_response(StatusCode::OK, &msg), + Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), + } + } - pub async fn delete_event(state: &AppState, id: String) -> Response { - let repo = EventsRepository::new(state); - match repo.query_delete_event(id).await { - Ok(msg) => common_response(StatusCode::OK, &msg), - Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), - } - } -} \ No newline at end of file + pub async fn delete_event(state: &AppState, id: String) -> Response { + let repo = EventsRepository::new(state); + match repo.query_delete_event(id).await { + Ok(msg) => common_response(StatusCode::OK, &msg), + Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), + } + } +} diff --git a/imphnen-cms/src/v1/landing/testimonials/testimonials_controller.rs b/imphnen-cms/src/v1/landing/testimonials/testimonials_controller.rs index 0429b7d..2746095 100644 --- a/imphnen-cms/src/v1/landing/testimonials/testimonials_controller.rs +++ b/imphnen-cms/src/v1/landing/testimonials/testimonials_controller.rs @@ -73,7 +73,6 @@ pub async fn post_create_testimonial( Extension(authenticated_user): Extension, Json(payload): Json, ) -> impl IntoResponse { - println!("Authenticated User Now: {:?}", authenticated_user); TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await } diff --git a/imphnen-cms/src/v1/landing/testimonials/testimonials_dto.rs b/imphnen-cms/src/v1/landing/testimonials/testimonials_dto.rs index 393139e..1c4557c 100644 --- a/imphnen-cms/src/v1/landing/testimonials/testimonials_dto.rs +++ b/imphnen-cms/src/v1/landing/testimonials/testimonials_dto.rs @@ -34,7 +34,7 @@ pub struct TestimonialsUpdateRequestDto { pub struct TestimonialsListItemDto { pub id: String, pub user_id: String, - pub user_fullname: String, // Assuming we'll fetch user's full name + pub user_fullname: String, pub role: String, pub content: String, pub created_at: String, @@ -45,7 +45,7 @@ pub struct TestimonialsListItemDto { pub struct TestimonialsDetailItemDto { pub id: String, pub user_id: String, - pub user_fullname: String, // Assuming we'll fetch user's full name + pub user_fullname: String, pub role: String, pub content: String, pub created_at: String, @@ -55,7 +55,7 @@ pub struct TestimonialsDetailItemDto { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct TestimonialsQueryDto { pub id: Thing, - pub user: UsersSchema, // Change from Thing to UsersSchema + pub user: UsersSchema, pub role: String, pub content: String, pub is_deleted: bool, @@ -68,7 +68,7 @@ impl TestimonialsQueryDto { TestimonialsListItemDto { id: self.id.id.to_raw(), user_id: self.user.id.id.to_raw(), - user_fullname: self.user.fullname, // Extract fullname from UsersSchema + user_fullname: self.user.fullname, role: self.role, content: self.content, created_at: self.created_at, diff --git a/imphnen-cms/src/v1/landing/testimonials/testimonials_repository.rs b/imphnen-cms/src/v1/landing/testimonials/testimonials_repository.rs index ae4c41c..cdb81ac 100644 --- a/imphnen-cms/src/v1/landing/testimonials/testimonials_repository.rs +++ b/imphnen-cms/src/v1/landing/testimonials/testimonials_repository.rs @@ -4,6 +4,9 @@ use super::{ use anyhow::{Result, bail}; use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto}; use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date}; +use serde_json; +use std::time::Instant; +use tracing::instrument; pub struct TestimonialsRepository<'a> { state: &'a AppState, @@ -14,17 +17,25 @@ impl<'a> TestimonialsRepository<'a> { Self { state } } + #[instrument(skip(self, meta), err)] pub async fn query_testimonial_list( &self, meta: MetaRequestDto, ) -> Result>> { - let query = ListQueryBuilder::new(&ResourceEnum::Testimonials.to_string()) - .with_select_fields(vec!["*", "user.* as user"]) // Select user details + let now = Instant::now(); + let query = ListQueryBuilder::new(ResourceEnum::Testimonials.to_string()) + .with_select_fields(vec!["*", "user.* as user"]) .with_pagination(meta.page, Some(10)) .with_sorting(meta.sort_by.as_deref(), meta.order.as_deref()) .build(); let res: Vec = self.state.surrealdb_ws.query(query).await?.take(0)?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_testimonial_list' took: {elapsed:.2?}"); + } let data = ResponseListSuccessDto { data: res, meta: None, @@ -32,17 +43,26 @@ impl<'a> TestimonialsRepository<'a> { Ok(data) } + #[instrument(skip(self, id), err)] pub async fn query_testimonial_by_id( &self, id: String, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let builder = DetailQueryBuilder::new(ResourceEnum::Testimonials.to_string()) .with_id(&id) - .with_select_fields(vec!["*", "user.* as user"]); // Select user details + .with_condition("is_deleted = false") + .with_select_fields(vec!["*", "user.* as user"]); let sql = builder.build(); let result: Option = builder.apply_bindings(db.query(sql)).await?.take(0)?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_testimonial_by_id' took: {elapsed:.2?}"); + } match result { Some(testimonial) => { @@ -55,15 +75,23 @@ impl<'a> TestimonialsRepository<'a> { } } + #[instrument(skip(self, data), err)] pub async fn query_create_testimonial( &self, data: TestimonialsSchema, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let record: Option = db .create(ResourceEnum::Testimonials.to_string()) .content(data) .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_create_testimonial' took: {elapsed:.2?}"); + } match record { Some(_) => Ok("Success create testimonial".into()), @@ -71,10 +99,12 @@ impl<'a> TestimonialsRepository<'a> { } } + #[instrument(skip(self, data), err)] pub async fn query_update_testimonial( &self, data: TestimonialsSchema, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let existing = self.query_testimonial_by_id(data.id.id.to_raw()).await?; @@ -85,13 +115,19 @@ impl<'a> TestimonialsRepository<'a> { let merged = TestimonialsSchema { created_at: existing.created_at, updated_at: get_iso_date(), - user: existing.user.id, // Preserve user ID + user: existing.user.id, ..data }; let record_key = get_id(&merged.id)?; let record: Option = db.update(record_key).merge(merged).await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_update_testimonial' took: {elapsed:.2?}"); + } match record { Some(_) => Ok("Success update testimonial".into()), @@ -99,7 +135,9 @@ impl<'a> TestimonialsRepository<'a> { } } + #[instrument(skip(self, id), err)] pub async fn query_delete_testimonial(&self, id: String) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let testimonial = self.query_testimonial_by_id(id).await?; if testimonial.is_deleted { @@ -111,6 +149,12 @@ impl<'a> TestimonialsRepository<'a> { .update(record_key) .merge(serde_json::json!({ "is_deleted": true })) .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_delete_testimonial' took: {elapsed:.2?}"); + } match record { Some(_) => Ok("Success delete testimonial".into()), diff --git a/imphnen-cms/src/v1/landing/testimonials/testimonials_schema.rs b/imphnen-cms/src/v1/landing/testimonials/testimonials_schema.rs index 6b8a4a1..39fdca4 100644 --- a/imphnen-cms/src/v1/landing/testimonials/testimonials_schema.rs +++ b/imphnen-cms/src/v1/landing/testimonials/testimonials_schema.rs @@ -11,7 +11,7 @@ use super::testimonials_dto::{ #[derive(Clone, Debug, Serialize, Deserialize)] pub struct TestimonialsSchema { pub id: Thing, - pub user: Thing, // Link to app_users table + pub user: Thing, pub role: String, pub content: String, pub is_deleted: bool, @@ -28,7 +28,7 @@ impl Default for TestimonialsSchema { ), user: make_thing( &ResourceEnum::Users.to_string(), - &Uuid::new_v4().to_string(), // Placeholder, will be replaced by actual user ID + &Uuid::new_v4().to_string(), ), role: String::new(), content: String::new(), diff --git a/imphnen-dimentorin/Cargo.toml b/imphnen-dimentorin/Cargo.toml index 6b4bce3..78605a1 100644 --- a/imphnen-dimentorin/Cargo.toml +++ b/imphnen-dimentorin/Cargo.toml @@ -4,9 +4,10 @@ version = "0.1.0" edition = "2024" [dependencies] -imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } -imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } -imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } +imphnen-libs.workspace = true +imphnen-utils.workspace = true +imphnen-entities.workspace = true +imphnen-iam.workspace = true axum.workspace = true serde.workspace = true serde_json.workspace = true @@ -22,3 +23,8 @@ chrono.workspace = true anyhow.workspace = true tower-http.workspace = true utoipa-swagger-ui.workspace = true +tracing.workspace = true + +[dev-dependencies] +dotenvy.workspace = true +http-body-util.workspace = true diff --git a/imphnen-dimentorin/src/lib.rs b/imphnen-dimentorin/src/lib.rs index 1303f3d..3789969 100644 --- a/imphnen-dimentorin/src/lib.rs +++ b/imphnen-dimentorin/src/lib.rs @@ -1,14 +1,2 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub mod v1; +pub use v1::*; diff --git a/imphnen-dimentorin/src/v1/mentors/mentors_controller.rs b/imphnen-dimentorin/src/v1/mentors/mentors_controller.rs new file mode 100644 index 0000000..a3b83a4 --- /dev/null +++ b/imphnen-dimentorin/src/v1/mentors/mentors_controller.rs @@ -0,0 +1,356 @@ +use super::{ + MentorDetailResponseDto, MentorListResponseDto, MentorUpdateRequestDto, + MentorUserRegisterRequestDto, MentorVerifyRequestDto, MentorsService, +}; +use crate::v1::mentors::mentors_dto::MentorRegisterResponseDto; +use ::axum::{ + extract::{Extension, Json, Path, Query}, + http::HeaderMap, + response::{IntoResponse, Response}, +}; +use imphnen_entities::*; +use imphnen_iam::{PermissionsEnum, permissions_guard}; +use imphnen_utils::extract_email; +use serde_json::json; + +#[utoipa::path( + post, + path = "/v1/mentors/register", + request_body = MentorUserRegisterRequestDto, + responses( + (status = 200, description = "Mentor registered successfully", body = MentorRegisterResponseDto), + (status = 400, description = "Bad request - validation error"), + (status = 409, description = "Conflict - user already has mentor profile"), + (status = 500, description = "Internal server error") + ), + tag = "Mentors" +)] +pub async fn post_register_mentor( + Extension(app_state): Extension, + Json(dto): Json, +) -> Response { + MentorsService::register_mentor(&app_state, dto).await +} + +#[utoipa::path( + get, + path = "/v1/mentors", + params( + ("page" = Option, Query, description = "Page number"), + ("per_page" = Option, Query, description = "Items per page"), + ("search" = Option, Query, description = "Search query"), + ("sort_by" = Option, Query, description = "Sort by field"), + ("order" = Option, Query, description = "Sort order (ASC/DESC)"), + ), + responses( + (status = 200, description = "Get list of mentors", body = Vec), + (status = 500, description = "Internal server error") + ), + tag = "Mentors", + security( + ("Bearer" = []) + ) +)] +pub async fn get_mentor_list( + headers: HeaderMap, + Extension(app_state): Extension, + Query(meta): Query, +) -> Response { + match permissions_guard( + &headers, + app_state.clone(), + vec![PermissionsEnum::ReadListMentors], + ) + .await + { + Ok(_) => MentorsService::get_mentor_list(&app_state, meta).await, + Err(response) => response, + } +} + +#[utoipa::path( + get, + path = "/v1/mentors/detail/{id}", + params( + ("id" = String, Path, description = "Mentor ID") + ), + responses( + (status = 200, description = "Get mentor by ID", body = MentorDetailResponseDto), + (status = 404, description = "Mentor not found"), + (status = 500, description = "Internal server error") + ), + tag = "Mentors", + security( + ("Bearer" = []) + ) +)] +pub async fn get_mentor_by_id( + headers: HeaderMap, + Extension(app_state): Extension, + Path(id): Path, +) -> Response { + match permissions_guard( + &headers, + app_state.clone(), + vec![PermissionsEnum::ReadDetailMentors], + ) + .await + { + Ok(_) => MentorsService::get_mentor_by_id(&app_state, &id).await, + Err(response) => response, + } +} + +#[utoipa::path( + put, + path = "/v1/mentors/update/{id}", + params( + ("id" = String, Path, description = "Mentor ID") + ), + request_body = MentorUpdateRequestDto, + responses( + (status = 200, description = "Mentor updated successfully", body = MentorDetailResponseDto), + (status = 400, description = "Bad request - validation error"), + (status = 404, description = "Mentor not found"), + (status = 500, description = "Internal server error") + ), + tag = "Mentors - Admin", + security( + ("Bearer" = []) + ) +)] +pub async fn put_update_mentor( + headers: HeaderMap, + Extension(app_state): Extension, + Path(id): Path, + Json(dto): Json, +) -> Response { + match permissions_guard( + &headers, + app_state.clone(), + vec![PermissionsEnum::UpdateMentors], + ) + .await + { + Ok(_) => MentorsService::update_mentor(&app_state, &id, dto).await, + Err(response) => response, + } +} + +#[utoipa::path( + delete, + path = "/v1/mentors/delete/{id}", + params( + ("id" = String, Path, description = "Mentor ID") + ), + responses( + (status = 200, description = "Mentor deleted successfully"), + (status = 404, description = "Mentor not found"), + (status = 500, description = "Internal server error") + ), + tag = "Mentors - Admin", + security( + ("Bearer" = []) + ) +)] +pub async fn delete_mentor( + headers: HeaderMap, + Extension(app_state): Extension, + Path(id): Path, +) -> Response { + match permissions_guard( + &headers, + app_state.clone(), + vec![PermissionsEnum::DeleteMentors], + ) + .await + { + Ok(_) => MentorsService::delete_mentor(&app_state, &id).await, + Err(response) => response, + } +} + +#[utoipa::path( + put, + path = "/v1/mentors/verify/{id}", + params( + ("id" = String, Path, description = "Mentor ID") + ), + request_body = MentorVerifyRequestDto, + responses( + (status = 200, description = "Mentor verified successfully", body = MentorDetailResponseDto), + (status = 400, description = "Bad request - validation error"), + (status = 404, description = "Mentor not found"), + (status = 500, description = "Internal server error") + ), + tag = "Mentors - Admin", + security( + ("Bearer" = []) + ) +)] +pub async fn put_verify_mentor( + headers: HeaderMap, + Extension(app_state): Extension, + Path(id): Path, + Json(dto): Json, +) -> Response { + match permissions_guard( + &headers, + app_state.clone(), + vec![PermissionsEnum::VerifyMentors], + ) + .await + { + Ok(_) => MentorsService::verify_mentor(&app_state, &id, dto).await, + Err(response) => response, + } +} + +#[utoipa::path( + get, + path = "/v1/mentors/me", + responses( + (status = 200, description = "Current user's mentor profile", body = MentorDetailResponseDto), + (status = 401, description = "Unauthorized - invalid token"), + (status = 403, description = "Mentor profile not found for current user"), + (status = 500, description = "Internal server error") + ), + tag = "Mentors", + security( + ("Bearer" = []) + ) +)] +pub async fn get_mentor_me( + Extension(app_state): Extension, + headers: HeaderMap, +) -> Response { + match permissions_guard( + &headers, + app_state.clone(), + vec![PermissionsEnum::ReadOwnMentorProfile], + ) + .await + { + Ok(_) => { + let email = match extract_email(&headers) { + Some(email) => email, + None => { + return ( + axum::http::StatusCode::UNAUTHORIZED, + Json(json!({ + "error": "Unauthorized", + "message": "Token tidak valid" + })), + ) + .into_response(); + } + }; + MentorsService::get_mentor_me(&app_state, &email).await + } + Err(response) => response, + } +} + +#[utoipa::path( + put, + path = "/v1/mentors/update/me", + request_body = MentorUpdateRequestDto, + responses( + (status = 200, description = "Mentor profile updated successfully", body = MentorDetailResponseDto), + (status = 400, description = "Bad request - validation error"), + (status = 401, description = "Unauthorized - invalid token"), + (status = 404, description = "Mentor profile not found"), + (status = 500, description = "Internal server error") + ), + tag = "Mentors", + security( + ("Bearer" = []) + ) +)] +pub async fn put_update_mentor_me( + Extension(app_state): Extension, + headers: HeaderMap, + Json(dto): Json, +) -> Response { + match permissions_guard( + &headers, + app_state.clone(), + vec![PermissionsEnum::UpdateOwnMentorProfile], + ) + .await + { + Ok(_) => { + let email = match extract_email(&headers) { + Some(email) => email, + None => { + return imphnen_utils::common_response( + axum::http::StatusCode::UNAUTHORIZED, + "Token tidak valid", + ); + } + }; + MentorsService::update_mentor_me(&app_state, &email, dto).await + } + Err(response) => response, + } +} +#[utoipa::path( + put, + path = "/v1/mentors/update", + request_body = MentorUpdateRequestDto, + responses( + (status = 400, description = "Bad request - Mentor ID is required for update"), + ), + tag = "Mentors - Admin" +)] +pub async fn put_update_mentor_no_id() -> Response { + imphnen_utils::common_response( + axum::http::StatusCode::BAD_REQUEST, + "Mentor ID is required for update", + ) +} + +#[utoipa::path( + get, + path = "/v1/mentors/status", + responses( + (status = 200, description = "Mentor application status", body = String), + (status = 401, description = "Unauthorized - invalid token"), + (status = 403, description = "No mentor application found for current user"), + (status = 500, description = "Internal server error") + ), + tag = "Mentors", + security( + ("Bearer" = []) + ) +)] +pub async fn get_mentor_status( + Extension(app_state): Extension, + headers: HeaderMap, +) -> Response { + match permissions_guard( + &headers, + app_state.clone(), + vec![PermissionsEnum::ReadOwnMentorStatus], + ) + .await + { + Ok(_) => { + let email = match extract_email(&headers) { + Some(email) => email, + None => { + return ( + axum::http::StatusCode::UNAUTHORIZED, + Json(json!({ + "error": "Unauthorized", + "message": "Token tidak valid" + })), + ) + .into_response(); + } + }; + MentorsService::get_mentor_status(&app_state, &email).await + } + Err(response) => response, + } +} diff --git a/imphnen-dimentorin/src/v1/mentors/mentors_dto.rs b/imphnen-dimentorin/src/v1/mentors/mentors_dto.rs new file mode 100644 index 0000000..a7ebbe3 --- /dev/null +++ b/imphnen-dimentorin/src/v1/mentors/mentors_dto.rs @@ -0,0 +1,479 @@ +use crate::v1::mentors::MentorSchema; +use imphnen_utils::extract_id; +use serde::{Deserialize, Serialize}; +use surrealdb::sql::Thing; +use utoipa::ToSchema; +use validator::Validate; + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct MentorListResponseDto { + pub id: String, + pub fullname: Option, + pub email: Option, + pub status: String, + pub created_at: String, + pub updated_at: String, +} +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MentorDetailWithUserDto { + pub id: Thing, + pub user_id: Thing, + pub fullname: Option, + pub email: Option, + pub legal_name: String, + pub identity_document_url: String, + pub phone_for_verification: String, + pub bio: String, + pub linkedin_url: Option, + pub github_url: Option, + pub cv_url: Option, + pub industries: Vec, + pub expertise: Vec, + pub languages: Vec, + pub current_company: String, + pub current_role: String, + pub years_of_experience: i32, + pub topics_of_interest: Vec, + pub preferred_mentee_level: Vec, + pub preferred_mentoring_formats: Vec, + pub availability_commitment: String, + pub mentoring_rate: MentoringRate, + pub status: String, + pub is_deleted: bool, + pub created_at: String, + pub updated_at: String, +} +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct MentorDetailResponseDto { + pub id: String, + pub user_id: String, + pub fullname: Option, + pub email: Option, + pub legal_name: String, + pub identity_document_url: String, + pub phone_for_verification: String, + pub bio: String, + pub linkedin_url: Option, + pub github_url: Option, + pub cv_url: Option, + pub industries: Vec, + pub expertise: Vec, + pub languages: Vec, + pub current_company: String, + pub current_role: String, + pub years_of_experience: i32, + pub topics_of_interest: Vec, + pub preferred_mentee_level: Vec, + pub preferred_mentoring_formats: Vec, + pub availability_commitment: String, + pub mentoring_rate: MentoringRate, + pub status: String, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] +pub struct MentorRegisterResponseDto { + pub id: String, + pub user_id: String, + pub email: Option, + pub status: String, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] +pub struct MentorUpdateRequestDto { + #[validate(length( + min = 3, + message = "Legal name must be at least 3 characters" + ))] + #[serde(skip_serializing_if = "Option::is_none")] + pub legal_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub gender: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub domicile: Option, + #[validate(url(message = "Invalid identity document URL"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub identity_document_url: Option, + #[validate(length( + min = 10, + max = 15, + message = "Phone must be 10-15 characters" + ))] + #[serde(skip_serializing_if = "Option::is_none")] + pub phone_for_verification: Option, + #[validate(length(min = 50, message = "Bio must be at least 50 characters"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub bio: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_education: Option, + #[validate(url(message = "Invalid LinkedIn URL"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub linkedin_url: Option, + #[validate(url(message = "Invalid GitHub URL"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub github_url: Option, + #[validate(url(message = "Invalid CV URL"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub cv_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub portfolio_url: Option, + #[validate(length(min = 1, message = "At least 1 industry required"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub industries: Option>, + #[validate(length(min = 1, message = "At least 1 expertise required"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub expertise: Option>, + #[validate(length(min = 1, message = "At least 1 language required"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub languages: Option>, + #[validate(length(min = 1, message = "Current company required"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub current_company: Option, + #[validate(length(min = 1, message = "Current role required"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub current_role: Option, + #[validate(range(min = 2, message = "At least 2 years of experience required"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub years_of_experience: Option, + #[validate(length(min = 1, message = "At least 1 topic required"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub topics_of_interest: Option>, + #[validate(length(min = 1, message = "At least 1 mentee level required"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub preferred_mentee_level: Option>, + #[validate(length(min = 1, message = "At least 1 mentoring format required"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub preferred_mentoring_formats: Option>, + #[validate(length( + min = 5, + message = "Availability commitment must be at least 5 characters" + ))] + #[serde(skip_serializing_if = "Option::is_none")] + pub availability_commitment: Option, + #[validate(range(min = 1, message = "Amount must be at least 1"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub mentoring_rate_amount: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] +pub struct MentorUserRegisterRequestDto { + #[validate( + length(min = 1, message = "Email cannot be empty"), + email(message = "Email not valid") + )] + pub email: String, + #[validate(length( + min = 8, + message = "Password must have at least 8 characters" + ))] + #[validate(custom( + function = "imphnen_iam::auth_dto::validate_password_complexity", + message = "Password must include uppercase, lowercase, number, and special character" + ))] + pub password: String, + #[validate(length(min = 2, message = "Fullname at least have 2 character"))] + pub fullname: String, + #[validate(length(min = 1, message = "Phone number is required"))] + pub phone_number: String, + #[validate(nested)] + pub identity_and_verification: IdentityAndVerification, + #[validate(nested)] + pub professional_profile: ProfessionalProfile, + #[validate(nested)] + pub mentoring_logistics: MentoringLogistics, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] +pub struct MentorRegisterFromTokenRequestDto { + #[validate(nested)] + pub identity_and_verification: IdentityAndVerification, + #[validate(nested)] + pub professional_profile: ProfessionalProfile, + #[validate(nested)] + pub mentoring_logistics: MentoringLogistics, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] +pub struct IdentityAndVerification { + #[validate(length( + min = 3, + message = "Legal name must be at least 3 characters" + ))] + pub legal_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub gender: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub domicile: Option, + #[validate(url(message = "Invalid identity document URL"))] + pub identity_document_url: String, + #[validate(length( + min = 10, + max = 15, + message = "Phone must be 10-15 characters" + ))] + pub phone_for_verification: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] +pub struct ProfessionalProfile { + #[validate(length(min = 50, message = "Bio must be at least 50 characters"))] + pub bio: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_education: Option, + #[validate(url(message = "Invalid LinkedIn URL"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub linkedin_url: Option, + #[validate(url(message = "Invalid GitHub URL"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub github_url: Option, + #[validate(url(message = "Invalid CV URL"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub cv_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub portfolio_url: Option, + #[validate(length(min = 1, message = "At least 1 industry required"))] + pub industries: Vec, + #[validate(length(min = 1, message = "At least 1 expertise required"))] + pub expertise: Vec, + #[validate(length(min = 1, message = "At least 1 language required"))] + pub languages: Vec, + #[validate(length(min = 1, message = "Current company required"))] + pub current_company: String, + #[validate(length(min = 1, message = "Current role required"))] + pub current_role: String, + #[validate(range(min = 2, message = "At least 2 years of experience required"))] + pub years_of_experience: i32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] +pub struct MentoringLogistics { + #[validate(length(min = 1, message = "At least 1 topic required"))] + pub topics_of_interest: Vec, + #[validate(length(min = 1, message = "At least 1 mentee level required"))] + pub preferred_mentee_level: Vec, + #[validate(length(min = 1, message = "At least 1 mentoring format required"))] + pub preferred_mentoring_formats: Vec, + #[validate(length( + min = 5, + message = "Availability commitment must be at least 5 characters" + ))] + pub availability_commitment: String, + #[validate(range(min = 1, message = "Amount must be at least 1"))] + pub mentoring_rate_amount: u64, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate, Default)] +pub struct MentoringRate { + #[validate(range(min = 1, message = "Amount must be at least 1"))] + pub amount: u64, + #[validate(length(min = 1, message = "Currency is required"))] + pub currency: String, + #[validate(length(min = 1, message = "Per duration is required"))] + pub per_duration: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MentorInsertDto { + pub id: Thing, + pub user_id: Option, + pub email: Option, + pub legal_name: String, + pub gender: Option, + pub domicile: Option, + pub identity_document_url: String, + pub phone_for_verification: String, + pub bio: String, + pub last_education: Option, + pub linkedin_url: Option, + pub github_url: Option, + pub cv_url: Option, + pub portfolio_url: Option, + pub industries: Vec, + pub expertise: Vec, + pub languages: Vec, + pub current_company: String, + pub current_role: String, + pub years_of_experience: i32, + pub topics_of_interest: Vec, + pub preferred_mentee_level: Vec, + pub preferred_mentoring_formats: Vec, + pub availability_commitment: String, + pub mentoring_rate: MentoringRate, + pub status: String, + pub is_deleted: bool, + pub created_at: String, + pub updated_at: String, +} + +impl From for MentorInsertDto { + fn from(schema: MentorSchema) -> Self { + MentorInsertDto { + id: schema.id, + user_id: schema.user_id, + email: schema.email, + legal_name: schema.legal_name, + gender: schema.gender, + domicile: schema.domicile, + identity_document_url: schema.identity_document_url, + phone_for_verification: schema.phone_for_verification, + bio: schema.bio, + last_education: schema.last_education, + linkedin_url: schema.linkedin_url, + github_url: schema.github_url, + cv_url: schema.cv_url, + portfolio_url: schema.portfolio_url, + industries: schema.industries, + expertise: schema.expertise, + languages: schema.languages, + current_company: schema.current_company, + current_role: schema.current_role, + years_of_experience: schema.years_of_experience, + topics_of_interest: schema.topics_of_interest, + preferred_mentee_level: schema.preferred_mentee_level, + preferred_mentoring_formats: schema.preferred_mentoring_formats, + availability_commitment: schema.availability_commitment, + mentoring_rate: schema.mentoring_rate, + status: schema.status, + is_deleted: schema.is_deleted, + created_at: schema.created_at, + updated_at: schema.updated_at, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] +pub struct MentorVerifyRequestDto { + #[validate(length(min = 1, message = "Status is required"))] + pub status: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MentorDetailQueryDto { + pub id: Thing, + pub user_id: Thing, + pub fullname: Option, + pub email: Option, + pub legal_name: String, + pub gender: Option, + pub domicile: Option, + pub identity_document_url: String, + pub phone_for_verification: String, + pub bio: String, + pub last_education: Option, + pub linkedin_url: Option, + pub github_url: Option, + pub cv_url: Option, + pub portfolio_url: Option, + pub industries: Vec, + pub expertise: Vec, + pub languages: Vec, + pub current_company: String, + pub current_role: String, + pub years_of_experience: i32, + pub topics_of_interest: Vec, + pub preferred_mentee_level: Vec, + pub preferred_mentoring_formats: Vec, + pub availability_commitment: String, + pub mentoring_rate: MentoringRate, + pub status: String, + pub is_deleted: bool, + pub created_at: String, + pub updated_at: String, +} + +impl From for MentorListResponseDto { + fn from(dto: MentorDetailQueryDto) -> Self { + Self { + id: extract_id(&dto.id), + fullname: dto.fullname, + email: dto.email, + status: dto.status, + created_at: dto.created_at, + updated_at: dto.updated_at, + } + } +} + +impl From for MentorDetailResponseDto { + fn from(dto: MentorDetailQueryDto) -> Self { + Self { + id: extract_id(&dto.id), + user_id: extract_id(&dto.user_id), + fullname: dto.fullname, + email: dto.email, + legal_name: dto.legal_name, + identity_document_url: dto.identity_document_url, + phone_for_verification: dto.phone_for_verification, + bio: dto.bio, + linkedin_url: dto.linkedin_url, + github_url: dto.github_url, + cv_url: dto.cv_url, + industries: dto.industries, + expertise: dto.expertise, + languages: dto.languages, + current_company: dto.current_company, + current_role: dto.current_role, + years_of_experience: dto.years_of_experience, + topics_of_interest: dto.topics_of_interest, + preferred_mentee_level: dto.preferred_mentee_level, + preferred_mentoring_formats: dto.preferred_mentoring_formats, + availability_commitment: dto.availability_commitment, + mentoring_rate: dto.mentoring_rate, + status: dto.status, + created_at: dto.created_at, + updated_at: dto.updated_at, + } + } +} +impl From for MentorRegisterResponseDto { + fn from(schema: MentorSchema) -> Self { + Self { + id: schema.id.to_string(), + user_id: schema.user_id.map(|id| extract_id(&id)).unwrap_or_default(), + email: schema.email, + status: schema.status, + created_at: schema.created_at, + updated_at: schema.updated_at, + } + } +} + +impl From for MentorDetailQueryDto { + fn from(dto: MentorDetailWithUserDto) -> Self { + MentorDetailQueryDto { + id: dto.id, + user_id: dto.user_id, + fullname: dto.fullname, + email: dto.email, + legal_name: dto.legal_name, + gender: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto + domicile: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto + identity_document_url: dto.identity_document_url, + phone_for_verification: dto.phone_for_verification, + bio: dto.bio, + last_education: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto + linkedin_url: dto.linkedin_url, + github_url: dto.github_url, + cv_url: dto.cv_url, + portfolio_url: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto + industries: dto.industries, + expertise: dto.expertise, + languages: dto.languages, + current_company: dto.current_company, + current_role: dto.current_role, + years_of_experience: dto.years_of_experience, + topics_of_interest: dto.topics_of_interest, + preferred_mentee_level: dto.preferred_mentee_level, + preferred_mentoring_formats: dto.preferred_mentoring_formats, + availability_commitment: dto.availability_commitment, + mentoring_rate: dto.mentoring_rate, + status: dto.status, + is_deleted: dto.is_deleted, + created_at: dto.created_at, + updated_at: dto.updated_at, + } + } +} diff --git a/imphnen-dimentorin/src/v1/mentors/mentors_repository.rs b/imphnen-dimentorin/src/v1/mentors/mentors_repository.rs new file mode 100644 index 0000000..fbe673e --- /dev/null +++ b/imphnen-dimentorin/src/v1/mentors/mentors_repository.rs @@ -0,0 +1,299 @@ +use anyhow::{Result, bail}; +use imphnen_iam::{get_id, make_thing}; +use surrealdb::sql::Thing; + +use crate::v1::mentors::{MentorDetailWithUserDto, MentorInsertDto, MentorSchema}; +use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto}; +use imphnen_utils::{DetailQueryBuilder, QueryListBuilder, get_iso_date}; +use serde_json::{Map, Value}; +use std::time::Instant; +use tracing::instrument; + +pub struct MentorsRepository<'a> { + pub state: &'a AppState, +} + +impl<'a> MentorsRepository<'a> { + pub fn new(state: &'a AppState) -> Self { + Self { state } + } + + #[instrument(skip(self, meta), err)] + pub async fn query_mentor_list( + &self, + meta: MetaRequestDto, + ) -> Result>> { + let now = Instant::now(); + let db = &self.state.surrealdb_ws; + let mentors_table = ResourceEnum::Mentors.to_string(); + let builder = QueryListBuilder::new(db, &mentors_table, &meta) + .search_field("legal_name") + .select_fields(vec![ + "id", + "user_id", + "user_id.fullname as fullname", + "email", + "legal_name", + "identity_document_url", + "phone_for_verification", + "bio", + "linkedin_url", + "github_url", + "cv_url", + "industries", + "expertise", + "languages", + "current_company", + "current_role", + "years_of_experience", + "topics_of_interest", + "preferred_mentee_level", + "preferred_mentoring_formats", + "availability_commitment", + "mentoring_rate", + "status", + "is_deleted", + "created_at", + "updated_at", + ]); + let result = builder.build().await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_mentor_list' took: {elapsed:.2?}"); + } + let data = result.data.into_iter().collect(); + Ok(ResponseListSuccessDto { + data, + meta: result.meta, + }) + } + + #[instrument(skip(self, email, include_deleted), err)] + pub async fn query_mentor_by_email( + &self, + email: String, + include_deleted: bool, + ) -> Result { + let now = Instant::now(); + let db = &self.state.surrealdb_ws; + let mut builder = DetailQueryBuilder::new(ResourceEnum::Mentors.to_string()) + .with_where("email", Some(email.clone())) + .with_select_fields(vec![ + "id", + "user_id", + "user_id.fullname as fullname", + "email", + "legal_name", + "identity_document_url", + "phone_for_verification", + "bio", + "linkedin_url", + "github_url", + "cv_url", + "industries", + "expertise", + "languages", + "current_company", + "current_role", + "years_of_experience", + "topics_of_interest", + "preferred_mentee_level", + "preferred_mentoring_formats", + "availability_commitment", + "mentoring_rate", + "status", + "is_deleted", + "created_at", + "updated_at", + ]); + + if !include_deleted { + builder = builder.with_condition("is_deleted = false"); + } + + let sql = builder.build(); + let mentor_opt: Option = + builder.apply_bindings(db.query(sql)).await?.take(0)?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_mentor_by_email' took: {elapsed:.2?}"); + } + let Some(mentor) = mentor_opt else { + bail!("Mentor not found"); + }; + Ok(mentor) + } + + #[instrument(skip(self, id, include_deleted), err)] + pub async fn query_mentor_by_id( + &self, + id: &Thing, + include_deleted: bool, + ) -> Result { + let now = Instant::now(); + let db = &self.state.surrealdb_ws; + let mut builder = DetailQueryBuilder::new(ResourceEnum::Mentors.to_string()) + .with_id(get_id(id)?.1) + .with_select_fields(vec![ + "id", + "user_id", + "user_id.fullname as fullname", + "email", + "legal_name", + "identity_document_url", + "phone_for_verification", + "bio", + "linkedin_url", + "github_url", + "cv_url", + "industries", + "expertise", + "languages", + "current_company", + "current_role", + "years_of_experience", + "topics_of_interest", + "preferred_mentee_level", + "preferred_mentoring_formats", + "availability_commitment", + "mentoring_rate", + "status", + "is_deleted", + "created_at", + "updated_at", + ]); + + if !include_deleted { + builder = builder.with_condition("is_deleted = false"); + } + + let sql = builder.build(); + let mentor_opt: Option = + builder.apply_bindings(db.query(sql)).await?.take(0)?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_mentor_by_id' took: {elapsed:.2?}"); + } + let Some(mentor) = mentor_opt else { + bail!("Mentor not found in database"); + }; + if mentor.is_deleted && !include_deleted { + bail!("Mentor has been deleted"); + } + Ok(mentor) + } + + #[instrument(skip(self, data), err)] + pub async fn query_create_mentor(&self, data: MentorSchema) -> Result { + let now = Instant::now(); + let db = &self.state.surrealdb_ws; + let dto: MentorInsertDto = data.into(); + let record: Option = db + .create(ResourceEnum::Mentors.to_string()) + .content(dto.clone()) + .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_create_mentor' took: {elapsed:.2?}"); + } + match record { + Some(mentor) => { + let id_str = mentor.id.id.to_raw(); + let _user = format!("{:?}", mentor.user_id); + Ok(id_str) + } + None => { + bail!("Failed to create mentor") + } + } + } + + #[instrument(skip(self, data), err)] + pub async fn query_update_mentor(&self, data: MentorSchema) -> Result { + let now = Instant::now(); + let db = &self.state.surrealdb_ws; + let id_ref = &data.id; + let record_key = get_id(id_ref)?; + let _existing = self.query_mentor_by_id(id_ref, false).await?; + + let mut merged_data_json: Map = + serde_json::to_value(data.clone()) + .map_err(|e| anyhow::anyhow!("Failed to serialize MentorSchema: {}", e))? + .as_object() + .cloned() + .unwrap_or_default(); + + merged_data_json.remove("id"); + merged_data_json.remove("user_id"); + merged_data_json.remove("created_at"); + + merged_data_json.insert("updated_at".to_string(), Value::String(get_iso_date())); + + let record: Option = + db.update(record_key).merge(merged_data_json).await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_update_mentor' took: {elapsed:.2?}"); + } + match record { + Some(_) => Ok("Success update mentor".into()), + None => { + bail!("Failed to update mentor") + } + } + } + + #[instrument(skip(self, id), err)] + pub async fn query_delete_mentor(&self, id: String) -> Result { + let now = Instant::now(); + let db = &self.state.surrealdb_ws; + let thing = make_thing(ResourceEnum::Mentors.to_string().as_str(), &id); + let record_key = get_id(&thing)?; + + let mentor_to_delete_res = self.query_mentor_by_id(&thing, true).await; + + let _mentor_to_delete = match mentor_to_delete_res { + Ok(mentor) => { + if mentor.is_deleted { + bail!("Mentor is already soft deleted"); + } + mentor + } + Err(e) => { + if e.to_string().contains("Mentor has been deleted") { + bail!("Mentor is already soft deleted"); + } else { + return Err(e); + } + } + }; + + let mut patch = Map::new(); + patch.insert("is_deleted".to_string(), Value::Bool(true)); + patch.insert("updated_at".to_string(), Value::String(get_iso_date())); + + let record: Option = db.update(record_key).merge(patch).await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_delete_mentor' took: {elapsed:.2?}"); + } + match record { + Some(_) => Ok("Success soft delete mentor".into()), + None => { + bail!("Failed to soft delete mentor") + } + } + } +} diff --git a/imphnen-dimentorin/src/v1/mentors/mentors_schema.rs b/imphnen-dimentorin/src/v1/mentors/mentors_schema.rs new file mode 100644 index 0000000..b9c7b2d --- /dev/null +++ b/imphnen-dimentorin/src/v1/mentors/mentors_schema.rs @@ -0,0 +1,250 @@ +use super::{ + IdentityAndVerification, MentorDetailQueryDto, MentorUpdateRequestDto, + MentoringLogistics, MentoringRate, ProfessionalProfile, +}; +use imphnen_libs::ResourceEnum; +use imphnen_utils::{get_iso_date, make_thing}; +use serde::{Deserialize, Serialize}; +use surrealdb::{Uuid, sql::Thing}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MentorSchema { + pub id: Thing, + #[serde(skip_serializing_if = "Option::is_none")] + pub user_id: Option, + pub email: Option, + pub legal_name: String, + pub gender: Option, + pub domicile: Option, + pub identity_document_url: String, + pub phone_for_verification: String, + pub bio: String, + pub last_education: Option, + pub linkedin_url: Option, + pub github_url: Option, + pub cv_url: Option, + pub portfolio_url: Option, + pub industries: Vec, + pub expertise: Vec, + pub languages: Vec, + pub current_company: String, + pub current_role: String, + pub years_of_experience: i32, + pub topics_of_interest: Vec, + pub preferred_mentee_level: Vec, + pub preferred_mentoring_formats: Vec, + pub availability_commitment: String, + pub mentoring_rate: MentoringRate, + pub status: String, + pub is_deleted: bool, + pub created_at: String, + pub updated_at: String, +} + +impl Default for MentorSchema { + fn default() -> Self { + Self { + id: make_thing( + ResourceEnum::Mentors.to_string().as_str(), + &Uuid::new_v4().to_string(), + ), + user_id: Some(make_thing( + ResourceEnum::Users.to_string().as_str(), + &Uuid::new_v4().to_string(), + )), + email: None, + legal_name: String::new(), + gender: None, + domicile: None, + identity_document_url: String::new(), + phone_for_verification: String::new(), + bio: String::new(), + last_education: None, + linkedin_url: None, + github_url: None, + cv_url: None, + portfolio_url: None, + industries: Vec::new(), + expertise: Vec::new(), + languages: Vec::new(), + current_company: String::new(), + current_role: String::new(), + years_of_experience: 0, + topics_of_interest: Vec::new(), + preferred_mentee_level: Vec::new(), + preferred_mentoring_formats: Vec::new(), + availability_commitment: String::new(), + mentoring_rate: MentoringRate { + amount: 0, + currency: "IDR".to_string(), + per_duration: "hour".to_string(), + }, + status: "pending".to_string(), + is_deleted: false, + created_at: get_iso_date(), + updated_at: get_iso_date(), + } + } +} + +impl MentorSchema { + pub fn create( + identity_and_verification: IdentityAndVerification, + professional_profile: ProfessionalProfile, + mentoring_logistics: MentoringLogistics, + user_id_raw: String, + email_str: String, + ) -> Self { + Self { + id: make_thing( + &ResourceEnum::Mentors.to_string(), + &Uuid::new_v4().to_string(), + ), + user_id: Some(make_thing(&ResourceEnum::Users.to_string(), &user_id_raw)), + email: Some(email_str), + legal_name: identity_and_verification.legal_name, + gender: identity_and_verification.gender, + domicile: identity_and_verification.domicile, + identity_document_url: identity_and_verification.identity_document_url, + phone_for_verification: identity_and_verification.phone_for_verification, + bio: professional_profile.bio, + last_education: professional_profile.last_education, + linkedin_url: professional_profile.linkedin_url, + github_url: professional_profile.github_url, + cv_url: professional_profile.cv_url, + portfolio_url: professional_profile.portfolio_url, + industries: professional_profile.industries, + expertise: professional_profile.expertise, + languages: professional_profile.languages, + current_company: professional_profile.current_company, + current_role: professional_profile.current_role, + years_of_experience: professional_profile.years_of_experience, + topics_of_interest: mentoring_logistics.topics_of_interest, + preferred_mentee_level: mentoring_logistics.preferred_mentee_level, + preferred_mentoring_formats: mentoring_logistics.preferred_mentoring_formats, + availability_commitment: mentoring_logistics.availability_commitment, + mentoring_rate: MentoringRate { + amount: mentoring_logistics.mentoring_rate_amount, + currency: "IDR".to_string(), + per_duration: "hour".to_string(), + }, + status: "pending".to_string(), + is_deleted: false, + created_at: get_iso_date(), + updated_at: get_iso_date(), + } + } + + pub fn from(dto: MentorDetailQueryDto) -> Self { + Self { + id: dto.id, + user_id: Some(dto.user_id), + email: dto.email, + legal_name: dto.legal_name, + gender: dto.gender, + domicile: dto.domicile, + identity_document_url: dto.identity_document_url, + phone_for_verification: dto.phone_for_verification, + bio: dto.bio, + last_education: dto.last_education, + linkedin_url: dto.linkedin_url, + github_url: dto.github_url, + cv_url: dto.cv_url, + portfolio_url: dto.portfolio_url, + industries: dto.industries, + expertise: dto.expertise, + languages: dto.languages, + current_company: dto.current_company, + current_role: dto.current_role, + years_of_experience: dto.years_of_experience, + topics_of_interest: dto.topics_of_interest, + preferred_mentee_level: dto.preferred_mentee_level, + preferred_mentoring_formats: dto.preferred_mentoring_formats, + availability_commitment: dto.availability_commitment, + mentoring_rate: dto.mentoring_rate, + status: dto.status, + is_deleted: dto.is_deleted, + created_at: dto.created_at, + updated_at: dto.updated_at, + } + } + + pub fn update(mut self, dto: MentorUpdateRequestDto) -> Self { + // Update fields only if they are Some(value), otherwise preserve current value + if let Some(val) = dto.legal_name { + self.legal_name = val; + } + if let Some(val) = dto.gender { + self.gender = Some(val); + } + if let Some(val) = dto.domicile { + self.domicile = Some(val); + } + if let Some(val) = dto.identity_document_url { + self.identity_document_url = val; + } + if let Some(val) = dto.phone_for_verification { + self.phone_for_verification = val; + } + if let Some(val) = dto.bio { + self.bio = val; + } + if let Some(val) = dto.last_education { + self.last_education = Some(val); + } + if let Some(val) = dto.linkedin_url { + self.linkedin_url = Some(val); + } + if let Some(val) = dto.github_url { + self.github_url = Some(val); + } + if let Some(val) = dto.cv_url { + self.cv_url = Some(val); + } + if let Some(val) = dto.portfolio_url { + self.portfolio_url = Some(val); + } + if let Some(val) = dto.industries { + self.industries = val; + } + if let Some(val) = dto.expertise { + self.expertise = val; + } + if let Some(val) = dto.languages { + self.languages = val; + } + if let Some(val) = dto.current_company { + self.current_company = val; + } + if let Some(val) = dto.current_role { + self.current_role = val; + } + if let Some(val) = dto.years_of_experience { + self.years_of_experience = val; + } + if let Some(val) = dto.topics_of_interest { + self.topics_of_interest = val; + } + if let Some(val) = dto.preferred_mentee_level { + self.preferred_mentee_level = val; + } + if let Some(val) = dto.preferred_mentoring_formats { + self.preferred_mentoring_formats = val; + } + if let Some(val) = dto.availability_commitment { + self.availability_commitment = val; + } + if let Some(val) = dto.mentoring_rate_amount { + self.mentoring_rate.amount = val; + } + + self.updated_at = get_iso_date(); + self + } + + pub fn update_status(mut self, status: String) -> Self { + self.status = status; + self.updated_at = get_iso_date(); + self + } +} diff --git a/imphnen-dimentorin/src/v1/mentors/mentors_service.rs b/imphnen-dimentorin/src/v1/mentors/mentors_service.rs new file mode 100644 index 0000000..6c8d633 --- /dev/null +++ b/imphnen-dimentorin/src/v1/mentors/mentors_service.rs @@ -0,0 +1,386 @@ +use crate::v1::mentors::{ + MentorDetailQueryDto, MentorDetailResponseDto, MentorListResponseDto, + MentorRegisterResponseDto, MentorSchema, MentorUpdateRequestDto, + MentorUserRegisterRequestDto, MentorVerifyRequestDto, MentorsRepository, +}; +use axum::http::StatusCode; +use axum::response::Response; +use imphnen_entities::{ + AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto, +}; +use imphnen_iam::{ + AuthRepository, RolesEnum, RolesRepository, UsersRepository, UsersSchema, +}; +use imphnen_libs::ResourceEnum; +use imphnen_utils::{ + common_response, success_list_response, success_response, validate_request, +}; +use surrealdb::Uuid; +use surrealdb::sql::Thing; +use tracing::error; + +pub struct MentorsService; + +impl MentorsService { + pub async fn register_mentor( + state: &AppState, + dto: MentorUserRegisterRequestDto, + ) -> Response { + if let Err((status, message)) = validate_request(&dto) { + return common_response(status, &message); + } + + let user_repo = UsersRepository::new(state); + let mentor_repo = MentorsRepository::new(state); + let role_repo = RolesRepository::new(state); + let auth_repo = AuthRepository::new(state); + + let user_email = dto.email.clone(); + let mut _user_to_update: Option = None; + + let existing_user_result = + user_repo.query_user_by_email(user_email.clone()).await; + + let user_id; + let final_user_email = user_email.clone(); + + if let Ok(user_detail_query_dto) = existing_user_result { + if mentor_repo + .query_mentor_by_email(user_email.clone(), false) + .await + .is_ok() + { + return common_response( + StatusCode::CONFLICT, + "Mentor profile already exists for this user", + ); + } + + let mut user_schema = UsersSchema::from(user_detail_query_dto.clone()); + + user_schema.fullname = dto.fullname.clone(); + user_schema.phone_number = dto.phone_number.clone(); + user_schema.updated_at = imphnen_utils::get_iso_date(); + + let hashed_password = match imphnen_utils::hash_password(&dto.password) { + Ok(hash) => hash, + Err(_e) => { + error!( + "Failed to hash password during update for {}: {}", + final_user_email, _e + ); + return common_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to hash password", + ); + } + }; + user_schema.password = hashed_password; + + let mentor_role = match role_repo + .query_role_by_name(RolesEnum::Mentor.to_string()) + .await + { + Ok(role) => role, + Err(_e) => { + return common_response(StatusCode::BAD_REQUEST, "Mentor Role Not Found"); + } + }; + user_schema.role = + imphnen_utils::make_thing(&ResourceEnum::Roles.to_string(), &mentor_role.id); + user_schema.is_active = false; + + if let Err(_err) = user_repo.query_update_user(user_schema.clone()).await { + error!( + "Failed to update existing user {} to mentor role: {}", + final_user_email, _err + ); + return common_response( + StatusCode::INTERNAL_SERVER_ERROR, + &_err.to_string(), + ); + } + user_id = user_schema.id.clone(); + } else { + let mentor_role = match role_repo + .query_role_by_name(RolesEnum::Mentor.to_string()) + .await + { + Ok(role) => role, + Err(_e) => { + return common_response(StatusCode::BAD_REQUEST, "Mentor Role Not Found"); + } + }; + + let hashed_password = match imphnen_utils::hash_password(&dto.password) { + Ok(hash) => hash, + Err(_e) => { + error!( + "Failed to hash password for new user {}: {}", + final_user_email, _e + ); + return common_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to hash password", + ); + } + }; + + let new_user_schema = UsersSchema { + id: imphnen_utils::make_thing( + &ResourceEnum::Users.to_string(), + &Uuid::new_v4().to_string(), + ), + email: dto.email.clone(), + fullname: dto.fullname.clone(), + password: hashed_password, + phone_number: dto.phone_number.clone(), + created_at: imphnen_utils::get_iso_date(), + updated_at: imphnen_utils::get_iso_date(), + role: imphnen_utils::make_thing( + &ResourceEnum::Roles.to_string(), + &mentor_role.id, + ), + is_active: false, + ..Default::default() + }; + + user_id = new_user_schema.id.clone(); + + match user_repo.query_create_user(new_user_schema).await { + Ok(_) => {} + Err(_err) => { + error!("Failed to create new user {}: {}", final_user_email, _err); + return common_response( + StatusCode::INTERNAL_SERVER_ERROR, + &_err.to_string(), + ); + } + } + } + + let otp = imphnen_utils::generate_otp::OtpManager::generate_otp(); + + match auth_repo + .query_store_otp(final_user_email.clone(), otp) + .await + { + Ok(_) => { + let message = format!("your otp code is {otp}"); + if let Err(_err) = + imphnen_utils::send_email(&final_user_email, "OTP Verification", &message) + { + error!("Failed to send OTP email to {}: {}", final_user_email, _err); + return common_response( + StatusCode::INTERNAL_SERVER_ERROR, + &_err.to_string(), + ); + } + } + Err(_err) => { + error!("Failed to store OTP for {}: {}", final_user_email, _err); + return common_response( + StatusCode::INTERNAL_SERVER_ERROR, + &_err.to_string(), + ); + } + } + + let mentor_schema = MentorSchema::create( + dto.identity_and_verification, + dto.professional_profile, + dto.mentoring_logistics, + user_id.to_raw(), + final_user_email.clone(), + ); + + match mentor_repo.query_create_mentor(mentor_schema.clone()).await { + Ok(mentor_profile_id) => { + let user_after_mentor_creation_dto = user_repo + .query_user_by_email(final_user_email.clone()) + .await + .unwrap(); + let mut user_after_mentor_creation_schema = + UsersSchema::from(user_after_mentor_creation_dto); + user_after_mentor_creation_schema = user_after_mentor_creation_schema + .update_mentor_id(Some(mentor_profile_id)); + if let Err(_e) = user_repo + .query_update_user(user_after_mentor_creation_schema) + .await + { + error!( + "Failed to update user's mentor_id for {}: {}", + final_user_email, _e + ); + return common_response( + StatusCode::INTERNAL_SERVER_ERROR, + &_e.to_string(), + ); + } + + let response_dto = MentorRegisterResponseDto::from(mentor_schema); + success_response(ResponseSuccessDto { data: response_dto }) + } + Err(_e) => { + error!( + "Failed to create mentor profile for {}: {}", + final_user_email, _e + ); + common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()) + } + } + } + + pub async fn get_mentor_list(state: &AppState, meta: MetaRequestDto) -> Response { + let repo = MentorsRepository::new(state); + match repo.query_mentor_list(meta).await { + Ok(result) => { + let data: Vec = result + .data + .into_iter() + .map(MentorDetailQueryDto::from) + .map(MentorListResponseDto::from) + .collect(); + success_list_response(ResponseListSuccessDto { + data, + meta: result.meta, + }) + } + Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()), + } + } + + pub async fn get_mentor_by_id(state: &AppState, id: &str) -> Response { + let repo = MentorsRepository::new(state); + let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id)); + match repo.query_mentor_by_id(&thing_id, false).await { + Ok(mentor) => { + let dto = MentorDetailResponseDto::from(MentorDetailQueryDto::from(mentor)); + success_response(ResponseSuccessDto { data: dto }) + } + Err(_e) => common_response(StatusCode::NOT_FOUND, &_e.to_string()), + } + } + + pub async fn update_mentor( + state: &AppState, + id: &str, + dto: MentorUpdateRequestDto, + ) -> Response { + if let Err((status, message)) = validate_request(&dto) { + return common_response(status, &message); + } + let repo = MentorsRepository::new(state); + let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id)); + let existing_mentor = match repo.query_mentor_by_id(&thing_id, false).await { + Ok(mentor) => mentor, + Err(_e) => return common_response(StatusCode::NOT_FOUND, &_e.to_string()), + }; + + let mut schema = MentorSchema::from(MentorDetailQueryDto::from(existing_mentor)); + schema = schema.update(dto); + + match repo.query_update_mentor(schema).await { + Ok(_) => { + let updated_mentor = + repo.query_mentor_by_id(&thing_id, false).await.unwrap(); + let response_dto = + MentorDetailResponseDto::from(MentorDetailQueryDto::from(updated_mentor)); + success_response(ResponseSuccessDto { data: response_dto }) + } + Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()), + } + } + + pub async fn delete_mentor(state: &AppState, id: &str) -> Response { + let repo = MentorsRepository::new(state); + match repo.query_delete_mentor(id.to_string()).await { + Ok(msg) => common_response(StatusCode::OK, &msg), + Err(_e) => common_response(StatusCode::NOT_FOUND, &_e.to_string()), + } + } + + pub async fn get_mentor_me(state: &AppState, email: &str) -> Response { + let repo = MentorsRepository::new(state); + match repo.query_mentor_by_email(email.to_string(), false).await { + Ok(mentor) => { + let dto = MentorDetailResponseDto::from(MentorDetailQueryDto::from(mentor)); + success_response(ResponseSuccessDto { data: dto }) + } + Err(_e) => common_response( + StatusCode::FORBIDDEN, + "Mentor profile not found for current user", + ), + } + } + + pub async fn update_mentor_me( + state: &AppState, + email: &str, + dto: MentorUpdateRequestDto, + ) -> Response { + if let Err((status, message)) = validate_request(&dto) { + return common_response(status, &message); + } + let repo = MentorsRepository::new(state); + let existing_mentor = + match repo.query_mentor_by_email(email.to_string(), false).await { + Ok(mentor) => mentor, + Err(_e) => return common_response(StatusCode::FORBIDDEN, &_e.to_string()), + }; + + let mut schema = MentorSchema::from(MentorDetailQueryDto::from(existing_mentor)); + schema = schema.update(dto); + + match repo.query_update_mentor(schema).await { + Ok(_) => { + let updated_mentor = repo + .query_mentor_by_email(email.to_string(), false) + .await + .unwrap(); + let response_dto = + MentorDetailResponseDto::from(MentorDetailQueryDto::from(updated_mentor)); + success_response(ResponseSuccessDto { data: response_dto }) + } + Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()), + } + } + + pub async fn get_mentor_status(state: &AppState, email: &str) -> Response { + let repo = MentorsRepository::new(state); + match repo.query_mentor_by_email(email.to_string(), false).await { + Ok(mentor) => common_response(StatusCode::OK, &mentor.status), + Err(_e) => common_response( + StatusCode::FORBIDDEN, + "No mentor application found for current user", + ), + } + } + + pub async fn verify_mentor( + state: &AppState, + id: &str, + dto: MentorVerifyRequestDto, + ) -> Response { + let repo = MentorsRepository::new(state); + let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id)); + let existing_mentor = match repo.query_mentor_by_id(&thing_id, false).await { + Ok(mentor) => mentor, + Err(_e) => return common_response(StatusCode::NOT_FOUND, &_e.to_string()), + }; + + let mut schema = MentorSchema::from(MentorDetailQueryDto::from(existing_mentor)); + schema = schema.update_status(dto.status); + + match repo.query_update_mentor(schema).await { + Ok(_) => { + let updated_mentor = + repo.query_mentor_by_id(&thing_id, false).await.unwrap(); + let response_dto = + MentorDetailResponseDto::from(MentorDetailQueryDto::from(updated_mentor)); + success_response(ResponseSuccessDto { data: response_dto }) + } + Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()), + } + } +} diff --git a/imphnen-dimentorin/src/v1/mentors/mod.rs b/imphnen-dimentorin/src/v1/mentors/mod.rs new file mode 100644 index 0000000..d530b75 --- /dev/null +++ b/imphnen-dimentorin/src/v1/mentors/mod.rs @@ -0,0 +1,30 @@ +use axum::{ + Router, + routing::{delete, get, post, put}, +}; + +pub mod mentors_controller; +pub mod mentors_dto; +pub mod mentors_repository; +pub mod mentors_schema; +pub mod mentors_service; + +pub use mentors_controller::*; +pub use mentors_dto::*; +pub use mentors_repository::*; +pub use mentors_schema::*; +pub use mentors_service::*; + +pub fn mentors_router() -> Router { + Router::new() + .route("/", get(get_mentor_list)) + .route("/register", post(post_register_mentor)) + .route("/me", get(get_mentor_me)) + .route("/update/me", put(put_update_mentor_me)) + .route("/status", get(get_mentor_status)) + .route("/detail/{id}", get(get_mentor_by_id)) + .route("/update/{id}", put(put_update_mentor)) + .route("/update", put(put_update_mentor_no_id)) + .route("/delete/{id}", delete(delete_mentor)) + .route("/verify/{id}", put(put(put_verify_mentor))) +} diff --git a/imphnen-dimentorin/src/v1/mod.rs b/imphnen-dimentorin/src/v1/mod.rs new file mode 100644 index 0000000..7cdb6f5 --- /dev/null +++ b/imphnen-dimentorin/src/v1/mod.rs @@ -0,0 +1,7 @@ +use axum::Router; + +pub mod mentors; + +pub fn dimentorin_router() -> Router { + Router::new().nest("/mentors", mentors::mentors_router()) +} diff --git a/imphnen-dimentorin/src/v2/mod.rs b/imphnen-dimentorin/src/v2/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/imphnen-dimentorin/src/v2/mod.rs @@ -0,0 +1 @@ + diff --git a/imphnen-entities/src/common_dto.rs b/imphnen-entities/src/common_dto.rs index 7a52604..32e89e6 100644 --- a/imphnen-entities/src/common_dto.rs +++ b/imphnen-entities/src/common_dto.rs @@ -1,9 +1,5 @@ use serde::{Deserialize, Serialize}; -use surrealdb::{ - engine::local::Db, - engine::any::Any, - Surreal, -}; +use surrealdb::{Surreal, engine::any::Any, engine::local::Db}; use utoipa::{IntoParams, ToSchema}; #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] diff --git a/imphnen-entities/src/error_dto.rs b/imphnen-entities/src/error_dto.rs index aedc52a..1ed0813 100644 --- a/imphnen-entities/src/error_dto.rs +++ b/imphnen-entities/src/error_dto.rs @@ -1,26 +1,31 @@ pub mod error { + use axum::Json; use axum::http::StatusCode; use axum::response::IntoResponse; use axum::response::Response; - use axum::Json; use thiserror::Error; #[derive(Error, Debug)] pub enum Error { - #[error("database error")] - Db, + #[error("database error: {0}")] + Db(String), } impl IntoResponse for Error { fn into_response(self) -> Response { - (StatusCode::INTERNAL_SERVER_ERROR, Json(self.to_string())).into_response() + let (status, error_message) = match self { + Error::Db(detail) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Database error: {detail}"), + ), + }; + (status, Json(error_message)).into_response() } } impl From for Error { fn from(error: surrealdb::Error) -> Self { - eprintln!("{error}"); - Self::Db + Self::Db(error.to_string()) } } } diff --git a/imphnen-gacha/Cargo.toml b/imphnen-gacha/Cargo.toml index 3b38445..54d8a22 100644 --- a/imphnen-gacha/Cargo.toml +++ b/imphnen-gacha/Cargo.toml @@ -4,10 +4,10 @@ version = "0.1.0" edition = "2024" [dependencies] -imphnen-iam ={ version = "0.1.0", path = "../imphnen-iam" } -imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } -imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } -imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } +imphnen-iam.workspace = true +imphnen-libs.workspace = true +imphnen-utils.workspace = true +imphnen-entities.workspace = true axum.workspace = true serde.workspace = true serde_json.workspace = true @@ -23,4 +23,6 @@ chrono.workspace = true anyhow.workspace = true tower-http.workspace = true utoipa-swagger-ui.workspace = true -rand_distr.workspace = true \ No newline at end of file +rand_distr.workspace = true +log.workspace = true +tracing.workspace = true \ No newline at end of file diff --git a/imphnen-gacha/src/lib.rs b/imphnen-gacha/src/lib.rs index 2c112e9..92b7daa 100644 --- a/imphnen-gacha/src/lib.rs +++ b/imphnen-gacha/src/lib.rs @@ -1,7 +1,3 @@ -use imphnen_entities::*; -use imphnen_libs::*; -use imphnen_utils::*; - pub mod v1; pub use imphnen_entities::*; diff --git a/imphnen-gacha/src/v1/gacha_claims/gacha_claims_repository.rs b/imphnen-gacha/src/v1/gacha_claims/gacha_claims_repository.rs index cada6de..6016bd3 100644 --- a/imphnen-gacha/src/v1/gacha_claims/gacha_claims_repository.rs +++ b/imphnen-gacha/src/v1/gacha_claims/gacha_claims_repository.rs @@ -2,6 +2,8 @@ use super::{GachaClaimQueryDto, GachaClaimSchema}; use crate::{AppState, ResourceEnum}; use anyhow::{Result, bail}; use imphnen_iam::DetailQueryBuilder; +use std::time::Instant; +use tracing::instrument; pub struct GachaClaimRepository<'a> { state: &'a AppState, @@ -12,10 +14,12 @@ impl<'a> GachaClaimRepository<'a> { Self { state } } + #[instrument(skip(self, id), err)] pub async fn query_gacha_claim_by_id( &self, id: String, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let builder = DetailQueryBuilder::new(ResourceEnum::GachaClaims.to_string()) .with_id(id.clone()) @@ -25,21 +29,35 @@ impl<'a> GachaClaimRepository<'a> { let sql = builder.build(); let result: Option = builder.apply_bindings(db.query(sql)).await?.take(0)?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_gacha_claim_by_id' took: {elapsed:.2?}"); + } match result { Some(claim) if !claim.is_deleted => Ok(claim), _ => bail!("Gacha Claim not found"), } } + #[instrument(skip(self, data), err)] pub async fn query_create_gacha_claim( &self, data: GachaClaimSchema, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let record: Option = db .create(ResourceEnum::GachaClaims.to_string()) .content(data) .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_create_gacha_claim' took: {elapsed:.2?}"); + } match record { Some(_) => Ok("Success create Gacha Claim".into()), None => bail!("Failed to create Gacha Claim"), diff --git a/imphnen-gacha/src/v1/gacha_credits/gacha_credits_repository.rs b/imphnen-gacha/src/v1/gacha_credits/gacha_credits_repository.rs index fa9947f..21962bb 100644 --- a/imphnen-gacha/src/v1/gacha_credits/gacha_credits_repository.rs +++ b/imphnen-gacha/src/v1/gacha_credits/gacha_credits_repository.rs @@ -2,7 +2,9 @@ use super::{GachaCreditRequestDto, GachaCreditSchema}; use crate::{AppState, ResourceEnum}; use anyhow::{Result, bail}; use imphnen_iam::make_thing; +use std::time::Instant; use surrealdb::Uuid; +use tracing::instrument; pub struct GachaCreditRepository<'a> { state: &'a AppState, @@ -13,28 +15,54 @@ impl<'a> GachaCreditRepository<'a> { Self { state } } + #[instrument(skip(self, user_id), err)] pub async fn query_by_user_id( &self, user_id: String, ) -> Result> { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let sql = format!( "SELECT * FROM {} WHERE user = {}:⟨$user_id⟩ AND is_deleted = false LIMIT 1", - ResourceEnum::GachaCredits.to_string(), - ResourceEnum::Users.to_string() + ResourceEnum::GachaCredits, + ResourceEnum::Users ); let result: Vec = db.query(sql).bind(("user_id", user_id)).await?.take(0)?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_by_user_id' took: {elapsed:.2?}"); + } Ok(result.into_iter().next()) } + #[instrument(skip(self, user_id), err)] pub async fn query_consume_credit(&self, user_id: String) -> Result<()> { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let credit_opt = self.query_by_user_id(user_id).await?; let Some(mut credit) = credit_opt else { + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!( + "Query 'query_consume_credit' took: {elapsed:.2?} (no credit to consume)" + ); + } return Ok(()); }; if credit.available_rolls <= 0 { + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!( + "Query 'query_consume_credit' took: {elapsed:.2?} (no rolls remaining)" + ); + } bail!("No extra roll credits remaining"); } credit.available_rolls -= 1; @@ -45,13 +73,21 @@ impl<'a> GachaCreditRepository<'a> { )) .merge(credit) .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_consume_credit' took: {elapsed:.2?}"); + } Ok(()) } + #[instrument(skip(self, payload), err)] pub async fn query_add_credit( &self, payload: GachaCreditRequestDto, ) -> Result<()> { + let now = Instant::now(); let db = &self.state.surrealdb_ws; if let Some(mut credit) = self.query_by_user_id(payload.user_id.clone()).await? { credit.available_rolls += payload.amount; @@ -63,7 +99,7 @@ impl<'a> GachaCreditRepository<'a> { .merge(credit) .await?; } else { - let data = GachaCreditSchema::from(&GachaCreditSchema { + let data = GachaCreditSchema { id: make_thing( &ResourceEnum::GachaCredits.to_string(), &Uuid::new_v4().to_string(), @@ -71,12 +107,18 @@ impl<'a> GachaCreditRepository<'a> { user: make_thing(&ResourceEnum::Users.to_string(), &payload.user_id), available_rolls: payload.amount, ..Default::default() - }); + }; let _: Option = db - .create(&ResourceEnum::GachaCredits.to_string()) + .create(ResourceEnum::GachaCredits.to_string()) .content(data) .await?; } + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_add_credit' took: {elapsed:.2?}"); + } Ok(()) } } diff --git a/imphnen-gacha/src/v1/gacha_items/gacha_items_repository.rs b/imphnen-gacha/src/v1/gacha_items/gacha_items_repository.rs index c29bdbe..73fbee8 100644 --- a/imphnen-gacha/src/v1/gacha_items/gacha_items_repository.rs +++ b/imphnen-gacha/src/v1/gacha_items/gacha_items_repository.rs @@ -5,6 +5,10 @@ use crate::{ }; use anyhow::{Result, bail}; use imphnen_iam::QueryListBuilder; +use imphnen_utils::get_iso_date; +use serde_json::{Map, Value}; +use std::time::Instant; +use tracing::instrument; pub struct GachaItemRepository<'a> { state: &'a AppState, @@ -15,10 +19,12 @@ impl<'a> GachaItemRepository<'a> { Self { state } } + #[instrument(skip(self, meta), err)] pub async fn query_gacha_item_list( &self, meta: MetaRequestDto, ) -> Result>> { + let now = Instant::now(); let raw_result: ResponseListSuccessDto> = QueryListBuilder::new( &self.state.surrealdb_ws, @@ -30,6 +36,12 @@ impl<'a> GachaItemRepository<'a> { .select_fields(vec!["*"]) .build() .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_gacha_item_list' took: {elapsed:.2?}"); + } let data = raw_result .data .into_iter() @@ -41,36 +53,54 @@ impl<'a> GachaItemRepository<'a> { }) } + #[instrument(skip(self, id), err)] pub async fn query_gacha_item_by_id(&self, id: String) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let result: Option = db .select((ResourceEnum::GachaItems.to_string(), id.clone())) .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_gacha_item_by_id' took: {elapsed:.2?}"); + } match result { Some(item) if !item.is_deleted => Ok(item), _ => bail!("Gacha Item not found"), } } + #[instrument(skip(self, data), err)] pub async fn query_create_gacha_item( &self, data: GachaItemSchema, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let record: Option = db .create(ResourceEnum::GachaItems.to_string()) .content(data) .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_create_gacha_item' took: {elapsed:.2?}"); + } match record { Some(_) => Ok("Success create Gacha Item".into()), None => bail!("Failed to create Gacha Item"), } } + #[instrument(skip(self, data), err)] pub async fn query_update_gacha_item( &self, data: GachaItemSchema, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let record_key = get_id(&data.id)?; let existing = self.query_gacha_item_by_id(data.id.id.to_raw()).await?; @@ -83,13 +113,21 @@ impl<'a> GachaItemRepository<'a> { }; let record: Option = db.update(record_key).merge(merged).await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_update_gacha_item' took: {elapsed:.2?}"); + } match record { Some(_) => Ok("Success update Gacha Item".into()), None => bail!("Failed to update Gacha Item"), } } + #[instrument(skip(self, id), err)] pub async fn query_delete_gacha_item(&self, id: String) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let item_id = make_thing(&ResourceEnum::GachaItems.to_string(), &id); let item = self.query_gacha_item_by_id(item_id.id.to_raw()).await?; @@ -97,13 +135,20 @@ impl<'a> GachaItemRepository<'a> { bail!("Gacha Item already deleted"); } let record_key = get_id(&item.id)?; - let record: Option = db - .update(record_key) - .merge(serde_json::json!({ "is_deleted": true })) - .await?; + let mut patch = Map::new(); + patch.insert("is_deleted".to_string(), Value::Bool(true)); + patch.insert("updated_at".to_string(), Value::String(get_iso_date())); + + let record: Option = db.update(record_key).merge(patch).await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_delete_gacha_item' took: {elapsed:.2?}"); + } match record { - Some(_) => Ok("Success delete Gacha Item".into()), - None => bail!("Failed to delete Gacha Item"), + Some(_) => Ok("Success soft delete Gacha Item".into()), + None => bail!("Failed to soft delete Gacha Item"), } } } diff --git a/imphnen-gacha/src/v1/gacha_items/gacha_items_service.rs b/imphnen-gacha/src/v1/gacha_items/gacha_items_service.rs index bf6de50..0afa2a5 100644 --- a/imphnen-gacha/src/v1/gacha_items/gacha_items_service.rs +++ b/imphnen-gacha/src/v1/gacha_items/gacha_items_service.rs @@ -45,7 +45,12 @@ impl GachaItemService { return common_response(status, &message); } let repo = GachaItemRepository::new(state); - let schema = GachaItemSchema::from(payload); + let schema = GachaItemSchema { + id: make_thing(&ResourceEnum::GachaItems.to_string(), &payload.name), // Fixed: Use payload.name or some other identifier + name: payload.name, + image_url: payload.image_url, + ..Default::default() + }; match repo.query_create_gacha_item(schema).await { Ok(msg) => common_response(StatusCode::CREATED, &msg), Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), diff --git a/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_controller.rs b/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_controller.rs index edf3b17..d98a4f9 100644 --- a/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_controller.rs +++ b/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_controller.rs @@ -91,3 +91,32 @@ pub async fn post_execute_gacha_roll( Err(response) => response, } } + +#[utoipa::path( + delete, + path = "/v1/gacha/rolls/delete/{id}", + security( + ("Bearer" = []) + ), + params(("id" = String, Path, description = "Gacha Roll ID")), + responses( + (status = 200, description = "Delete Gacha Roll (soft delete)", body = MessageResponseDto) + ), + tag = "Gacha" +)] +pub async fn delete_gacha_roll( + headers: HeaderMap, + Extension(state): Extension, + Path(id): Path, +) -> impl IntoResponse { + match permissions_guard( + &headers, + state.clone(), + vec![PermissionsEnum::DeleteGachaRolls], + ) + .await + { + Ok(_) => GachaRollService::soft_delete_gacha_roll(&state, id).await, + Err(response) => response, + } +} diff --git a/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_dto.rs b/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_dto.rs index 2ec4efb..d27cee3 100644 --- a/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_dto.rs +++ b/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_dto.rs @@ -29,7 +29,7 @@ impl GachaRollItemDto { Self { id: dto.id.id.to_raw(), item: GachaItemDto::from(dto.item.clone()), - weight: dto.weight.clone(), + weight: dto.weight, quantity: dto.quantity, is_deleted: dto.is_deleted, created_at: dto.created_at.clone(), diff --git a/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_repository.rs b/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_repository.rs index 5517cf5..abec2aa 100644 --- a/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_repository.rs +++ b/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_repository.rs @@ -1,11 +1,15 @@ use super::GachaRollQueryDto; use super::GachaRollSchema; -use crate::{AppState, DetailQueryBuilder, ResourceEnum}; +use crate::{AppState, DetailQueryBuilder, ResourceEnum, get_id, make_thing}; use anyhow::{Result, bail}; -use imphnen_iam::ListQueryBuilder; + use rand::prelude::*; -use rand::rng; + +use imphnen_utils::get_iso_date; use rand_distr::weighted::WeightedIndex; +use serde_json::{Map, Value}; +use std::time::Instant; +use tracing::instrument; pub struct GachaRollRepository<'a> { state: &'a AppState, @@ -16,46 +20,70 @@ impl<'a> GachaRollRepository<'a> { Self { state } } + #[instrument(skip(self, id), err)] pub async fn query_gacha_roll_by_id( &self, id: String, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let builder = DetailQueryBuilder::new(ResourceEnum::GachaRolls.to_string()) .with_id(id.clone()) + .with_condition("is_deleted = false") .with_select_fields(vec!["*"]) .with_fetch("item"); let sql = builder.build(); let result: Option = builder.apply_bindings(db.query(sql)).await?.take(0)?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_gacha_roll_by_id' took: {elapsed:.2?}"); + } match result { Some(roll) if !roll.is_deleted => Ok(roll), _ => bail!("Gacha Roll not found"), } } + #[instrument(skip(self, data), err)] pub async fn query_create_gacha_roll( &self, data: GachaRollSchema, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let record: Option = db .create(ResourceEnum::GachaRolls.to_string()) .content(data) .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_create_gacha_roll' took: {elapsed:.2?}"); + } match record { Some(_) => Ok("Success create Gacha Roll".into()), None => bail!("Failed to create Gacha Roll"), } } + #[instrument(skip(self), err)] pub async fn query_all_active_rolls(&self) -> Result> { + let now = Instant::now(); let db = &self.state.surrealdb_ws; - let builder = ListQueryBuilder::new(ResourceEnum::GachaRolls.to_string()) - .with_select_fields(vec!["*"]) - .with_fetch(Some(vec!["item"])); - let sql = builder.build(); + let table_name = ResourceEnum::GachaRolls.to_string(); + let sql = + format!("SELECT * FROM {table_name} WHERE is_deleted = false FETCH item"); let result: Vec = db.query(sql).await?.take(0)?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_all_active_rolls' took: {elapsed:.2?}"); + } Ok(result) } @@ -72,8 +100,36 @@ impl<'a> GachaRollRepository<'a> { return None; } let dist = WeightedIndex::new(&weights).ok()?; - let mut rng = rng(); + let mut rng = rand::rngs::ThreadRng::default(); let index = dist.sample(&mut rng); Some(filtered[index].clone()) } + + #[instrument(skip(self, id), err)] + pub async fn query_soft_delete_gacha_roll(&self, id: String) -> Result { + let now = Instant::now(); + let db = &self.state.surrealdb_ws; + let roll_id_thing = make_thing(&ResourceEnum::GachaRolls.to_string(), &id); + let roll = self.query_gacha_roll_by_id(id.clone()).await?; + if roll.is_deleted { + bail!("Gacha Roll already deleted"); + } + let record_key = get_id(&roll_id_thing)?; + + let mut patch = Map::new(); + patch.insert("is_deleted".to_string(), Value::Bool(true)); + patch.insert("updated_at".to_string(), Value::String(get_iso_date())); + + let record: Option = db.update(record_key).merge(patch).await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_soft_delete_gacha_roll' took: {elapsed:.2?}"); + } + match record { + Some(_) => Ok("Success soft delete Gacha Roll".into()), + None => bail!("Failed to soft delete Gacha Roll"), + } + } } diff --git a/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_service.rs b/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_service.rs index aab5d80..a3cb17d 100644 --- a/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_service.rs +++ b/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_service.rs @@ -63,4 +63,12 @@ impl GachaRollService { Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), } } + + pub async fn soft_delete_gacha_roll(state: &AppState, id: String) -> Response { + let repo = GachaRollRepository::new(state); + match repo.query_soft_delete_gacha_roll(id).await { + Ok(msg) => common_response(StatusCode::OK, &msg), + Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()), + } + } } diff --git a/imphnen-gateway/Cargo.toml b/imphnen-gateway/Cargo.toml index c907ac5..2b9e66b 100644 --- a/imphnen-gateway/Cargo.toml +++ b/imphnen-gateway/Cargo.toml @@ -4,13 +4,14 @@ version = "0.1.0" edition = "2024" [dependencies] -imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" } -imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } -imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } -imphnen-gacha = { version = "0.1.0", path = "../imphnen-gacha" } -imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } -imphnen-middleware = { version = "0.1.0", path = "../imphnen-middleware" } -imphnen-cms = { version = "0.1.0", path = "../imphnen-cms" } +imphnen-iam.workspace = true +imphnen-libs.workspace = true +imphnen-utils.workspace = true +imphnen-gacha.workspace = true +imphnen-entities.workspace = true +imphnen-middleware.workspace = true +imphnen-cms.workspace = true +imphnen-dimentorin.workspace = true axum.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/imphnen-gateway/src/docs.rs b/imphnen-gateway/src/docs.rs index 49362ec..955b9cc 100644 --- a/imphnen-gateway/src/docs.rs +++ b/imphnen-gateway/src/docs.rs @@ -1,18 +1,44 @@ +use imphnen_cms::{ + events_controller, + events_dto::{EventsDetailItemDto, EventsListItemDto}, + testimonials_controller, + testimonials_dto::{ + TestimonialsCreateRequestDto, TestimonialsDetailItemDto, + TestimonialsListItemDto, TestimonialsUpdateRequestDto, + }, +}; +use imphnen_dimentorin::v1::mentors::{ + mentors_controller, + mentors_dto::{ + IdentityAndVerification, MentorDetailResponseDto, MentorListResponseDto, + MentorRegisterFromTokenRequestDto, MentorRegisterResponseDto, + MentorUpdateRequestDto, MentorUserRegisterRequestDto, MentorVerifyRequestDto, + MentoringLogistics, MentoringRate, ProfessionalProfile, + }, +}; +use imphnen_gacha::{ + GachaClaimItemDto, GachaClaimRequestDto, GachaItemDto, GachaItemRequestDto, + GachaRollItemDto, GachaRollRequestDto, gacha_claims, gacha_items, gacha_rolls, +}; use imphnen_iam::{ -auth, permissions, roles, users, AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto, AuthRefreshTokenRequestDto, AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, MessageResponseDto, MetaRequestDto, MetaResponseDto, PermissionsItemDto, PermissionsRequestDto, ResponseListSuccessDto, ResponseSuccessDto, RolesDetailItemDto, RolesListItemDto, RolesRequestCreateDto, RolesRequestUpdateDto, TokenDto, UsersCreateRequestDto, UsersDetailItemDto, UsersListItemDto, UsersUpdateRequestDto + AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto, + AuthRefreshTokenRequestDto, AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, + MessageResponseDto, MetaRequestDto, MetaResponseDto, PermissionsItemDto, + PermissionsRequestDto, ResponseListSuccessDto, ResponseSuccessDto, + RolesDetailItemDto, RolesListItemDto, RolesRequestCreateDto, + RolesRequestUpdateDto, TokenDto, UsersCreateRequestDto, UsersDetailItemDto, + UsersListItemDto, UsersUpdateRequestDto, auth, permissions, roles, users, }; use utoipa::{ - openapi::security::{Http, HttpAuthScheme, SecurityScheme}, Modify, OpenApi, + openapi::security::{Http, HttpAuthScheme, SecurityScheme}, }; -use imphnen_gacha::{gacha_claims, gacha_items, gacha_rolls, GachaClaimItemDto, GachaClaimRequestDto, GachaItemDto, GachaItemRequestDto, GachaRollItemDto, GachaRollRequestDto}; -use imphnen_cms::{events_controller, events_dto::{EventsDetailItemDto, EventsListItemDto}, testimonials_controller, testimonials_dto::{TestimonialsCreateRequestDto, TestimonialsDetailItemDto, TestimonialsListItemDto, TestimonialsUpdateRequestDto}}; - #[derive(OpenApi)] #[openapi( paths( auth::auth_controller::post_login, + auth::auth_controller::post_login_mentor, auth::auth_controller::post_register, auth::auth_controller::post_verify_email, auth::auth_controller::post_resend_otp, @@ -47,16 +73,25 @@ use imphnen_cms::{events_controller, events_dto::{EventsDetailItemDto, EventsLis gacha_rolls::get_detail_gacha_roll, gacha_rolls::post_create_gacha_roll, gacha_rolls::post_execute_gacha_roll, - events_controller::get_event_list, - events_controller::get_event_by_id, - events_controller::post_create_event, - events_controller::patch_update_event, - events_controller::delete_event, - testimonials_controller::get_testimonial_list, - testimonials_controller::get_testimonial_by_id, - testimonials_controller::post_create_testimonial, - testimonials_controller::patch_update_testimonial, - testimonials_controller::delete_testimonial, + events_controller::get_event_list, + events_controller::get_event_by_id, + events_controller::post_create_event, + events_controller::patch_update_event, + events_controller::delete_event, + testimonials_controller::get_testimonial_list, + testimonials_controller::get_testimonial_by_id, + testimonials_controller::post_create_testimonial, + testimonials_controller::patch_update_testimonial, + testimonials_controller::delete_testimonial, + mentors_controller::get_mentor_list, + mentors_controller::get_mentor_by_id, + mentors_controller::post_register_mentor, + mentors_controller::get_mentor_me, + mentors_controller::put_update_mentor_me, + mentors_controller::get_mentor_status, + mentors_controller::put_update_mentor, + mentors_controller::put_verify_mentor, + mentors_controller::delete_mentor, ), components( schemas( @@ -71,7 +106,7 @@ use imphnen_cms::{events_controller, events_dto::{EventsDetailItemDto, EventsLis AuthRefreshTokenRequestDto, ResponseSuccessDto, RolesListItemDto, - RolesRequestCreateDto, + RolesRequestCreateDto, RolesRequestUpdateDto, PermissionsRequestDto, PermissionsItemDto, @@ -96,13 +131,26 @@ use imphnen_cms::{events_controller, events_dto::{EventsDetailItemDto, EventsLis ResponseSuccessDto, ResponseListSuccessDto>, ResponseSuccessDto, - ResponseListSuccessDto>, - ResponseSuccessDto, - ResponseListSuccessDto>, - ResponseSuccessDto, - TestimonialsCreateRequestDto, - TestimonialsUpdateRequestDto, - MessageResponseDto, + ResponseListSuccessDto>, + ResponseSuccessDto, + ResponseListSuccessDto>, + ResponseSuccessDto, + TestimonialsCreateRequestDto, + TestimonialsUpdateRequestDto, + MentorUserRegisterRequestDto, + MentorRegisterFromTokenRequestDto, + MentorRegisterResponseDto, + MentorListResponseDto, + MentorDetailResponseDto, + MentorUpdateRequestDto, + MentorVerifyRequestDto, + IdentityAndVerification, + ProfessionalProfile, + MentoringLogistics, + MentoringRate, + ResponseListSuccessDto>, + ResponseSuccessDto, + ResponseSuccessDto, ) ), info( @@ -121,6 +169,14 @@ use imphnen_cms::{events_controller, events_dto::{EventsDetailItemDto, EventsLis modifiers(&SecurityAddon), tags( (name = "Authentication", description = "List of Authentication Endpoints"), + (name = "Users", description = "User Management Endpoints"), + (name = "Roles", description = "Role Management Endpoints"), + (name = "Permissions", description = "Permission Management Endpoints"), + (name = "Events", description = "Event Management Endpoints"), + (name = "Testimonials", description = "Testimonial Management Endpoints"), + (name = "Mentors", description = "Mentor Management Endpoints"), + (name = "Mentors - Admin", description = "Mentor Admin Management Endpoints (Admin Access Required)"), + (name = "Gacha", description = "Gacha System Endpoints"), ) )] diff --git a/imphnen-gateway/src/lib.rs b/imphnen-gateway/src/lib.rs index 32dd671..825aba7 100644 --- a/imphnen-gateway/src/lib.rs +++ b/imphnen-gateway/src/lib.rs @@ -5,6 +5,7 @@ use imphnen_cms::{ events_protected_routes, events_public_routes, testimonials_protected_routes, testimonials_public_routes, }; +use imphnen_dimentorin::dimentorin_router; use imphnen_entities::{AppState, SurrealMemClient, SurrealWsClient}; use imphnen_gacha::gacha_router; use imphnen_iam::{iam_protected_routes, iam_public_routes}; @@ -32,6 +33,7 @@ pub async fn gateway_service( .merge(iam_protected_routes()) .merge(events_protected_routes()) .merge(testimonials_protected_routes()) + .merge(dimentorin_router()) .merge(gacha_router()) .layer(from_fn(auth_middleware)); diff --git a/imphnen-iam/Cargo.toml b/imphnen-iam/Cargo.toml index 6266523..1f2bcb0 100644 --- a/imphnen-iam/Cargo.toml +++ b/imphnen-iam/Cargo.toml @@ -4,9 +4,9 @@ version = "0.1.0" edition = "2024" [dependencies] -imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } -imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } -imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } +imphnen-libs.workspace = true +imphnen-utils.workspace = true +imphnen-entities.workspace = true axum.workspace = true serde.workspace = true serde_json.workspace = true @@ -22,3 +22,13 @@ chrono.workspace = true anyhow.workspace = true tower-http.workspace = true utoipa-swagger-ui.workspace = true +strum.workspace = true +strum_macros.workspace = true +log.workspace = true +once_cell.workspace = true +tracing.workspace = true +uuid.workspace = true + +[dev-dependencies] +dotenvy.workspace = true +http-body-util.workspace = true diff --git a/imphnen-iam/src/lib.rs b/imphnen-iam/src/lib.rs index 3249e5f..92b7daa 100644 --- a/imphnen-iam/src/lib.rs +++ b/imphnen-iam/src/lib.rs @@ -1,34 +1,6 @@ -use ::surrealdb::Uuid; -use imphnen_entities::*; -use imphnen_libs::*; -use imphnen_utils::*; - pub mod v1; pub use imphnen_entities::*; pub use imphnen_libs::*; pub use imphnen_utils::*; pub use v1::*; - -pub fn create_test_user( - email: &str, - fullname: &str, - is_active: bool, - role_id: &str, -) -> UsersSchema { - UsersSchema { - id: make_thing("app_users", &Uuid::new_v4().to_string()), - email: email.to_string(), - fullname: format!("Randomize {} {}", fullname, rand::random::()), - password: hash_password("secret").unwrap(), - is_deleted: false, - avatar: None, - phone_number: "081234567890".to_string(), - is_active, - gender: None, - birthdate: None, - role: make_thing("app_roles", role_id), - created_at: get_iso_date(), - updated_at: get_iso_date(), - } -} diff --git a/imphnen-iam/src/v1/auth/auth_controller.rs b/imphnen-iam/src/v1/auth/auth_controller.rs index d267c48..6ff9ef6 100644 --- a/imphnen-iam/src/v1/auth/auth_controller.rs +++ b/imphnen-iam/src/v1/auth/auth_controller.rs @@ -2,9 +2,9 @@ use super::{ AuthLoginRequestDto, AuthRefreshTokenRequestDto, AuthRegisterRequestDto, AuthResendOtpRequestDto, AuthService, AuthVerifyEmailRequestDto, }; -use crate::{v1::AuthLoginResponsetDto, AppState}; +use crate::{AppState, v1::AuthLoginResponsetDto}; use crate::{AuthNewPasswordRequestDto, MessageResponseDto, ResponseSuccessDto}; -use axum::{response::IntoResponse, Extension, Json}; +use axum::{Extension, Json, response::IntoResponse}; #[utoipa::path( post, @@ -23,6 +23,24 @@ pub async fn post_login( AuthService::mutation_login(payload, &state).await } +#[utoipa::path( + post, + path = "/v1/auth/login-mentor", + request_body = AuthLoginRequestDto, + responses( + (status = 200, description = "Mentor login successful", body = ResponseSuccessDto), + (status = 401, description = "Mentor login failed", body = MessageResponseDto), + (status = 403, description = "Forbidden - Not a mentor", body = MessageResponseDto) + ), + tag = "Authentication" +)] +pub async fn post_login_mentor( + Extension(state): Extension, + Json(payload): Json, +) -> impl IntoResponse { + AuthService::mutation_mentor_login(payload, &state).await +} + #[utoipa::path( post, path = "/v1/auth/register", diff --git a/imphnen-iam/src/v1/auth/auth_dto.rs b/imphnen-iam/src/v1/auth/auth_dto.rs index f3a1a0c..d85c198 100644 --- a/imphnen-iam/src/v1/auth/auth_dto.rs +++ b/imphnen-iam/src/v1/auth/auth_dto.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use validator::{Validate, ValidationError}; -fn validate_password_complexity(password: &str) -> Result<(), ValidationError> { +pub fn validate_password_complexity(password: &str) -> Result<(), ValidationError> { let has_uppercase = password.chars().any(|c| c.is_ascii_uppercase()); let has_lowercase = password.chars().any(|c| c.is_ascii_lowercase()); let has_digit = password.chars().any(|c| c.is_ascii_digit()); @@ -117,3 +117,8 @@ pub struct AuthSetNewPasswordRequestDto { ))] pub password: String, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct UserCacheSchema { + pub email: String, + pub permissions: Vec, +} diff --git a/imphnen-iam/src/v1/auth/auth_repository.rs b/imphnen-iam/src/v1/auth/auth_repository.rs index 9b44161..79d9722 100644 --- a/imphnen-iam/src/v1/auth/auth_repository.rs +++ b/imphnen-iam/src/v1/auth/auth_repository.rs @@ -1,7 +1,12 @@ use super::AuthOtpSchema; -use crate::{AppState, ResourceEnum, UsersDetailQueryDto, make_thing}; +use super::UserCacheSchema; +use crate::{ + AppState, PermissionsQueryDto, ResourceEnum, RolesDetailQueryDto, + UsersDetailQueryDto, +}; use anyhow::{Result, anyhow, bail}; use chrono::{Duration, Utc}; +use surrealdb::sql::Thing; pub struct AuthRepository<'a> { pub state: &'a AppState, @@ -18,20 +23,26 @@ impl<'a> AuthRepository<'a> { } let table = ResourceEnum::UsersCache.to_string(); let user_id = user.email.clone(); - let id = make_thing(&table, &user_id); - let _ = self + let permissions: Vec = + user.role.permissions.into_iter().map(|p| p.name).collect(); + let user_cache = UserCacheSchema { + email: user_id.clone(), + permissions, + }; + + let _record: Option = self .state .surrealdb_mem - .delete::>((table.clone(), user_id.clone())) + .delete::>((table.clone(), user_id.clone())) .await?; - let mut user_to_store = user.clone(); - user_to_store.id = id.clone(); - let record: Option = self + + let record: Option = self .state .surrealdb_mem .create((table, user_id)) - .content(user_to_store) + .content(user_cache) .await?; + match record { Some(_) => Ok("Success store user data".to_string()), None => bail!("Failed store user data"), @@ -42,13 +53,54 @@ impl<'a> AuthRepository<'a> { &self, email: String, ) -> Result { - let user: Option = self + let user_cache: Option = self .state .surrealdb_mem - .select((ResourceEnum::UsersCache.to_string(), email)) + .select((ResourceEnum::UsersCache.to_string(), email.clone())) .await?; - match user { - Some(u) => Ok(u), + + match user_cache { + Some(cache) => { + let permissions_query_dto: Vec = cache + .permissions + .into_iter() + .map(|name| PermissionsQueryDto { + id: Thing::from(( + "app_permissions".to_string(), + surrealdb::sql::Id::rand(), + )), + name, + created_at: None, + updated_at: None, + }) + .collect(); + + let role_detail_query_dto = RolesDetailQueryDto { + id: Thing::from(("app_roles".to_string(), surrealdb::sql::Id::rand())), + name: "CachedRole".to_string(), + permissions: permissions_query_dto, + is_deleted: false, + created_at: None, + updated_at: None, + }; + + Ok(UsersDetailQueryDto { + id: Thing::from(("app_users".to_string(), email.clone())), + fullname: "Cached User".to_string(), + email: cache.email, + avatar: None, + phone_number: String::new(), + is_active: true, + is_deleted: false, + gender: None, + birthdate: None, + password: String::new(), + role: role_detail_query_dto, + created_at: String::new(), + updated_at: String::new(), + mentor_id: None, + }) + } None => bail!("No stored user data found"), } } @@ -59,7 +111,6 @@ impl<'a> AuthRepository<'a> { .surrealdb_mem .delete((ResourceEnum::UsersCache.to_string(), email)) .await?; - dbg!(record.clone()); match record { Some(_) => Ok("Success delete stored user".to_string()), None => bail!("Failed delete stored user"), diff --git a/imphnen-iam/src/v1/auth/auth_service.rs b/imphnen-iam/src/v1/auth/auth_service.rs index d1c9a81..9076d23 100644 --- a/imphnen-iam/src/v1/auth/auth_service.rs +++ b/imphnen-iam/src/v1/auth/auth_service.rs @@ -13,6 +13,7 @@ use crate::{ }; use axum::{http::StatusCode, response::Response}; use surrealdb::Uuid; +use tracing::error; pub struct AuthService; @@ -49,7 +50,11 @@ impl AuthService { let access_token = match encode_access_token(payload.email.clone()) { Ok(token) => token, - Err(_) => { + Err(_e) => { + error!( + "Failed to generate access token for {}: {}", + payload.email, _e + ); return common_response( StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate access token", @@ -59,7 +64,11 @@ impl AuthService { let refresh_token = match encode_refresh_token(payload.email.clone()) { Ok(token) => token, - Err(_) => { + Err(_e) => { + error!( + "Failed to generate refresh token for {}: {}", + payload.email, _e + ); return common_response( StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate refresh token", @@ -77,13 +86,116 @@ impl AuthService { }, }; - if let Err(_err) = auth_repo.query_store_user(user).await { - return common_response(StatusCode::BAD_REQUEST, "User already login"); + if let Err(err_store) = auth_repo.query_store_user(user.clone()).await { + error!( + "Failed to store user cache for {}: {}", + user.email, err_store + ); + return common_response( + StatusCode::BAD_REQUEST, + "User already login or failed to cache", + ); } - success_response(response) } - Err(err) => common_response(StatusCode::UNAUTHORIZED, &err.to_string()), + Err(err_find) => { + common_response(StatusCode::UNAUTHORIZED, &err_find.to_string()) + } + } + } + + pub async fn mutation_mentor_login( + payload: AuthLoginRequestDto, + state: &AppState, + ) -> Response { + if let Err((status, message)) = validate_request(&payload) { + return common_response(status, &message); + } + + let user_repo = UsersRepository::new(state); + let auth_repo = AuthRepository::new(state); + + match user_repo.query_user_by_email(payload.email.clone()).await { + Ok(user) => { + let is_password_correct = + verify_password(&payload.password, &user.password).unwrap_or(false); + + if !is_password_correct { + return common_response( + StatusCode::BAD_REQUEST, + "Email or password not correct", + ); + } + + if !user.is_active { + return common_response( + StatusCode::BAD_REQUEST, + "Account not active, please verify your email", + ); + } + + let user_detail = UsersDetailItemDto::from(&user); + + if user_detail.role.name != RolesEnum::Mentor.to_string() { + return common_response( + StatusCode::FORBIDDEN, + "User does not have mentor privileges", + ); + } + + let access_token = match encode_access_token(payload.email.clone()) { + Ok(token) => token, + Err(_e) => { + error!( + "Failed to generate access token for {}: {}", + payload.email, _e + ); + return common_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to generate access token", + ); + } + }; + + let refresh_token = match encode_refresh_token(payload.email.clone()) { + Ok(token) => token, + Err(_e) => { + error!( + "Failed to generate refresh token for {}: {}", + payload.email, _e + ); + return common_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to generate refresh token", + ); + } + }; + + let response = ResponseSuccessDto { + data: AuthLoginResponsetDto { + user: UsersDetailItemDto::from(&user), + token: TokenDto { + access_token, + refresh_token, + }, + }, + }; + + if let Err(err_store) = auth_repo.query_store_user(user.clone()).await { + error!( + "Failed to store user cache for {}: {}", + user.email, err_store + ); + return common_response( + StatusCode::BAD_REQUEST, + "User already login or failed to cache", + ); + } + success_response(response) + } + Err(err_find) => { + common_response(StatusCode::UNAUTHORIZED, &err_find.to_string()) + } } } @@ -102,7 +214,10 @@ impl AuthService { .await { Ok(role) => role, - Err(_) => return common_response(StatusCode::BAD_REQUEST, "Role Not Found"), + Err(_e) => { + error!("Failed to retrieve User role during registration: {}", _e); + return common_response(StatusCode::BAD_REQUEST, "Role Not Found"); + } }; if user_repo .query_user_by_email(payload.email.clone()) @@ -113,7 +228,11 @@ impl AuthService { } let hashed_password = match hash_password(&payload.password) { Ok(hash) => hash, - Err(_) => { + Err(_e) => { + error!( + "Failed to hash password during registration for {}: {}", + payload.email, _e + ); return common_response( StatusCode::INTERNAL_SERVER_ERROR, "Failed to hash password", @@ -121,27 +240,34 @@ impl AuthService { } }; let new_user = AuthRegisterRequestDto { - email: payload.email, + email: payload.email.clone(), password: hashed_password, fullname: payload.fullname, phone_number: payload.phone_number, }; let otp = generate_otp::OtpManager::generate_otp(); - match auth_repo - .query_store_otp(new_user.email.clone(), otp.clone()) - .await - { + match auth_repo.query_store_otp(new_user.email.clone(), otp).await { Ok(_) => { - let message = format!("your otp code is {}", otp); - if let Err(err) = send_email(&new_user.email, "OTP Verification", &message) { + let message = format!("your otp code is {otp}"); + if let Err(err_send) = + send_email(&new_user.email, "OTP Verification", &message) + { + error!( + "Failed to send OTP email to {}: {}", + new_user.email, err_send + ); return common_response( StatusCode::INTERNAL_SERVER_ERROR, - &err.to_string(), + &err_send.to_string(), ); } } - Err(err) => { - return common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string()); + Err(err_store) => { + error!("Failed to store OTP for {}: {}", new_user.email, err_store); + return common_response( + StatusCode::INTERNAL_SERVER_ERROR, + &err_store.to_string(), + ); } } let role_thing = make_thing(&ResourceEnum::Roles.to_string(), &role.id); @@ -159,13 +285,15 @@ impl AuthService { created_at: get_iso_date(), updated_at: get_iso_date(), role: role_thing, + is_active: true, ..Default::default() }) .await { Ok(msg) => common_response(StatusCode::CREATED, &msg), - Err(err) => { - common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string()) + Err(err_create) => { + error!("Failed to create user {}: {}", new_user.email, err_create); + common_response(StatusCode::INTERNAL_SERVER_ERROR, &err_create.to_string()) } } } @@ -188,13 +316,22 @@ impl AuthService { let auth_repo = AuthRepository::new(state); let _ = auth_repo.query_get_stored_otp(payload.email.clone()).await; let otp = generate_otp::OtpManager::generate_otp(); - let message = format!("Your OTP code is {}", otp); + let message = format!("Your OTP code is {otp}"); match auth_repo.query_store_otp(payload.email.clone(), otp).await { Ok(_) => match send_email(&payload.email, "OTP Verification", &message) { Ok(_) => common_response(StatusCode::OK, "OTP resent successfully"), - Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()), + Err(err_send) => { + error!( + "Failed to send OTP email to {}: {}", + payload.email, err_send + ); + common_response(StatusCode::BAD_REQUEST, &err_send.to_string()) + } }, - Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()), + Err(err_store) => { + error!("Failed to store OTP for {}: {}", payload.email, err_store); + common_response(StatusCode::BAD_REQUEST, &err_store.to_string()) + } } } @@ -206,13 +343,14 @@ impl AuthService { } let email = match decode_refresh_token(&payload.refresh_token) { Ok(token) => token.claims.sub, - Err(_) => { + Err(_e) => { return common_response(StatusCode::UNAUTHORIZED, "Invalid refresh token"); } }; let access_token = match encode_access_token(email.clone()) { Ok(token) => token, - Err(_) => { + Err(_e) => { + error!("Failed to generate access token for {}: {}", email, _e); return common_response( StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate access token", @@ -221,7 +359,8 @@ impl AuthService { }; let refresh_token = match encode_refresh_token(email.clone()) { Ok(token) => token, - Err(_) => { + Err(_e) => { + error!("Failed to generate refresh token for {}: {}", email, _e); return common_response( StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate refresh token", @@ -248,16 +387,27 @@ impl AuthService { let user_result = user_repo.query_user_by_email(payload.email.clone()).await; let user = match user_result { Ok(user) => user, - Err(err) if err.to_string().contains("User not found") => { + Err(err_find) if err_find.to_string().contains("User not found") => { return common_response(StatusCode::BAD_REQUEST, "User not found"); } - Err(err) => { - return common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string()); + Err(err_other) => { + error!( + "Error finding user for forgot password {}: {}", + payload.email, err_other + ); + return common_response( + StatusCode::INTERNAL_SERVER_ERROR, + &err_other.to_string(), + ); } }; - let token = match encode_reset_password_token(user.email) { + let token = match encode_reset_password_token(user.email.clone()) { Ok(token) => token, - Err(_) => { + Err(_e) => { + error!( + "Failed to generate reset password token for {}: {}", + user.email, _e + ); return common_response( StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate access token", @@ -267,12 +417,17 @@ impl AuthService { let env = Env::new(); let fe_url = env.fe_url; let message = format!( - "You have requested a password reset. Please click the link below to continue: {}/auth/reset-password?token={}", - fe_url, token + "You have requested a password reset. Please click the link below to continue: {fe_url}/auth/reset-password?token={token}" ); match send_email(&payload.email, "Reset Password Request", &message) { Ok(_) => common_response(StatusCode::OK, "Reset Password request send"), - Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()), + Err(err_send) => { + error!( + "Failed to send reset password email to {}: {}", + payload.email, err_send + ); + common_response(StatusCode::BAD_REQUEST, &err_send.to_string()) + } } } @@ -288,33 +443,38 @@ impl AuthService { let email = payload.email.clone(); let user = match user_repo.query_user_by_email(email.clone()).await { Ok(user) if !user.is_deleted => user, - _ => return common_response(StatusCode::NOT_FOUND, "User not found"), + _ => { + return common_response(StatusCode::NOT_FOUND, "User not found"); + } }; let patch = UsersSchema { id: user.id.clone(), is_active: true, - ..UsersSchema::from(user) + ..UsersSchema::from(user.clone()) }; match auth_repo.query_get_stored_otp(email.clone()).await { Ok(stored_otp) => match stored_otp == payload.otp { true => match user_repo.query_update_user(patch).await { - Ok(_) => match auth_repo.query_delete_stored_otp(email).await { + Ok(_) => match auth_repo.query_delete_stored_otp(email.clone()).await { Ok(_) => common_response(StatusCode::OK, "Email verified successfully"), - Err(e) => { - common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()) + Err(e_del) => { + error!("Failed to delete OTP for {}: {}", email, e_del); + common_response(StatusCode::INTERNAL_SERVER_ERROR, &e_del.to_string()) } }, - Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()), + Err(err_update) => { + common_response(StatusCode::BAD_REQUEST, &err_update.to_string()) + } }, - false => match auth_repo.query_delete_stored_otp(email).await { + false => match auth_repo.query_delete_stored_otp(email.clone()).await { Ok(_) => common_response(StatusCode::BAD_REQUEST, "Failed to verify OTP"), - Err(e) => common_response( + Err(e_del_mismatch) => common_response( StatusCode::INTERNAL_SERVER_ERROR, - &format!("Failed to delete OTP: {}", e), + &format!("Failed to delete OTP: {e_del_mismatch}"), ), }, }, - Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()), + Err(err_get) => common_response(StatusCode::BAD_REQUEST, &err_get.to_string()), } } @@ -327,14 +487,15 @@ impl AuthService { } let repo = UsersRepository::new(state); let email = match extract_email_token(payload.token.clone()) { - Some(email) => email, + Some(token) => token, None => { return common_response(StatusCode::BAD_REQUEST, "Invalid or missing token"); } }; let password = match hash_password(&payload.password) { Ok(p) => p, - Err(_) => { + Err(_e) => { + error!("Failed to hash new password for {}: {}", email, _e); return common_response( StatusCode::INTERNAL_SERVER_ERROR, "Failed to hash password", @@ -343,7 +504,9 @@ impl AuthService { }; let user = match repo.query_user_by_email(email.clone()).await { Ok(user) if !user.is_deleted => user, - _ => return common_response(StatusCode::NOT_FOUND, "User not found"), + _ => { + return common_response(StatusCode::NOT_FOUND, "User not found"); + } }; let patch = UsersSchema { id: user.id.clone(), @@ -352,7 +515,7 @@ impl AuthService { }; match repo.query_update_user(patch).await { Ok(msg) => common_response(StatusCode::OK, &msg), - Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), + Err(_e) => common_response(StatusCode::BAD_REQUEST, &_e.to_string()), } } } diff --git a/imphnen-iam/src/v1/auth/mod.rs b/imphnen-iam/src/v1/auth/mod.rs index a08978b..a548a80 100644 --- a/imphnen-iam/src/v1/auth/mod.rs +++ b/imphnen-iam/src/v1/auth/mod.rs @@ -15,6 +15,7 @@ pub fn auth_router() -> Router { Router::new() .route("/forgot", post(auth_controller::post_forgot_password)) .route("/login", post(auth_controller::post_login)) + .route("/login-mentor", post(auth_controller::post_login_mentor)) .route("/new-password", post(auth_controller::post_new_password)) .route("/refresh", post(auth_controller::post_refresh_token)) .route("/register", post(auth_controller::post_register)) diff --git a/imphnen-iam/src/v1/permissions/permissions_enum.rs b/imphnen-iam/src/v1/permissions/permissions_enum.rs index c28d8a4..5e34a07 100644 --- a/imphnen-iam/src/v1/permissions/permissions_enum.rs +++ b/imphnen-iam/src/v1/permissions/permissions_enum.rs @@ -1,6 +1,8 @@ use std::fmt; -#[derive(Debug, Clone, PartialEq, Eq)] +use strum_macros::EnumIter; + +#[derive(Debug, Clone, PartialEq, Eq, EnumIter)] pub enum PermissionsEnum { ReadListUsers, ReadDetailUsers, @@ -28,6 +30,16 @@ pub enum PermissionsEnum { ReadDetailGachaRolls, CreateGachaRolls, ExecuteGachaRolls, + DeleteGachaRolls, + ReadListMentors, + ReadDetailMentors, + RegisterMentors, + ReadOwnMentorProfile, + UpdateOwnMentorProfile, + ReadOwnMentorStatus, + UpdateMentors, + VerifyMentors, + DeleteMentors, } impl fmt::Display for PermissionsEnum { @@ -59,8 +71,18 @@ impl fmt::Display for PermissionsEnum { PermissionsEnum::ReadDetailGachaRolls => "Read Detail Gacha Rolls", PermissionsEnum::CreateGachaRolls => "Create Gacha Rolls", PermissionsEnum::ExecuteGachaRolls => "Execute Gacha Rolls", + PermissionsEnum::DeleteGachaRolls => "Delete Gacha Rolls", + PermissionsEnum::ReadListMentors => "Read List Mentors", + PermissionsEnum::ReadDetailMentors => "Read Detail Mentors", + PermissionsEnum::RegisterMentors => "Register Mentors", + PermissionsEnum::ReadOwnMentorProfile => "Read Own Mentor Profile", + PermissionsEnum::UpdateOwnMentorProfile => "Update Own Mentor Profile", + PermissionsEnum::ReadOwnMentorStatus => "Read Own Mentor Status", + PermissionsEnum::UpdateMentors => "Update Mentors", + PermissionsEnum::VerifyMentors => "Verify Mentors", + PermissionsEnum::DeleteMentors => "Delete Mentors", }; - write!(f, "{}", permission_str) + write!(f, "{permission_str}") } } @@ -101,6 +123,20 @@ impl PermissionsEnum { } PermissionsEnum::CreateGachaRolls => "18e36c63-fcb7-4877-b911-c5aa611e878f", PermissionsEnum::ExecuteGachaRolls => "14c6a1cd-5c63-4643-89b5-b1a5f9920cc0", + PermissionsEnum::DeleteGachaRolls => "12345678-ABCD-EFAB-CDEF-0123456789AB", + PermissionsEnum::ReadListMentors => "a1b2c3d4-5e6f-7890-abcd-ef1234567890", + PermissionsEnum::ReadDetailMentors => "b2c3d4e5-6f78-9012-bcde-f23456789012", + PermissionsEnum::RegisterMentors => "c3d4e5f6-7890-1234-cdef-345678901234", + PermissionsEnum::ReadOwnMentorProfile => { + "d4e5f6a7-8901-2345-def0-456789012345" + } + PermissionsEnum::UpdateOwnMentorProfile => { + "e5f6a7b8-9012-3456-ef01-567890123456" + } + PermissionsEnum::ReadOwnMentorStatus => "f6a7b8c9-0123-4567-f012-678901234567", + PermissionsEnum::UpdateMentors => "a7b8c9d0-1234-5678-0123-789012345678", + PermissionsEnum::VerifyMentors => "b8c9d0e1-2345-6789-1234-890123456789", + PermissionsEnum::DeleteMentors => "c9d0e1f2-3456-7890-2345-901234567890", } } } diff --git a/imphnen-iam/src/v1/permissions/permissions_guard.rs b/imphnen-iam/src/v1/permissions/permissions_guard.rs index e04d513..125721e 100644 --- a/imphnen-iam/src/v1/permissions/permissions_guard.rs +++ b/imphnen-iam/src/v1/permissions/permissions_guard.rs @@ -1,5 +1,5 @@ use super::PermissionsEnum; -use crate::{common_response, extract_email, AppState, AuthRepository}; +use crate::{AppState, AuthRepository, common_response, extract_email}; use axum::{ http::{HeaderMap, StatusCode}, response::Response, @@ -29,14 +29,16 @@ pub async fn permissions_guard( let role = raw_user.role; let role_permissions: Vec = role.permissions.into_iter().map(|perm| perm.name).collect(); - let has_all_permissions = required_permissions - .iter() - .all(|required| role_permissions.contains(&required.to_string())); - if !has_all_permissions { - return Err(common_response( - StatusCode::FORBIDDEN, - "You don't have the required permissions", - )); + + for required in &required_permissions { + let required_str = required.to_string(); + if !role_permissions.contains(&required_str) { + eprintln!(" MISSING REQUIRED PERMISSION: {required_str}"); + return Err(common_response( + StatusCode::FORBIDDEN, + "You don't have the required permissions", + )); + } } Ok(()) } diff --git a/imphnen-iam/src/v1/permissions/permissions_repository.rs b/imphnen-iam/src/v1/permissions/permissions_repository.rs index a0dc30a..0f77ad2 100644 --- a/imphnen-iam/src/v1/permissions/permissions_repository.rs +++ b/imphnen-iam/src/v1/permissions/permissions_repository.rs @@ -4,6 +4,9 @@ use crate::{ }; use anyhow::{Result, bail}; use imphnen_utils::{DetailQueryBuilder, QueryListBuilder, extract_id}; +use serde_json; +use std::time::Instant; +use tracing::instrument; pub struct PermissionsRepository<'a> { state: &'a AppState, @@ -14,10 +17,12 @@ impl<'a> PermissionsRepository<'a> { Self { state } } + #[instrument(skip(self, meta), err)] pub async fn query_permission_list( &self, meta: MetaRequestDto, ) -> Result>> { + let now = Instant::now(); let raw_result: ResponseListSuccessDto> = QueryListBuilder::new( &self.state.surrealdb_ws, @@ -30,6 +35,13 @@ impl<'a> PermissionsRepository<'a> { .build() .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_permission_list' took: {elapsed:.2?}"); + } + let transformed_data = raw_result .data .into_iter() @@ -42,25 +54,41 @@ impl<'a> PermissionsRepository<'a> { }) } + #[instrument(skip(self, id), err)] pub async fn query_permission_by_id( &self, id: String, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let result: Option = db .select((ResourceEnum::Permissions.to_string(), id.clone())) .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_permission_by_id' took: {elapsed:.2?}"); + } match result { Some(permission) if !permission.is_deleted => Ok(permission), _ => bail!("Permission not found"), } } + #[instrument(skip(self, id), err)] pub async fn transformed_query_permission_by_id( &self, id: String, ) -> Result { + let now = Instant::now(); let raw_result = self.query_permission_by_id(id.clone()).await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'transformed_query_permission_by_id' took: {elapsed:.2?}"); + } let transformed_data = PermissionsItemDto { id: extract_id(&raw_result.id), name: raw_result.name, @@ -70,43 +98,60 @@ impl<'a> PermissionsRepository<'a> { Ok(transformed_data) } + #[instrument(skip(self, name), err)] pub async fn query_permission_by_name( &self, name: String, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let builder = DetailQueryBuilder::new(ResourceEnum::Permissions.to_string()) - .with_where("name") - .where_value(name.clone()) + .with_where("name", Some(name.clone())) .with_select_fields(vec!["*"]); let sql = builder.build(); let result: Option = builder.apply_bindings(db.query(sql)).await?.take(0)?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_permission_by_name' took: {elapsed:.2?}"); + } match result { Some(permission) => Ok(permission), None => bail!("Permission not found"), } } + #[instrument(skip(self, data), err)] pub async fn query_create_permission( &self, data: PermissionsSchema, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let record: Option = db .create(ResourceEnum::Permissions.to_string()) .content(data) .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_create_permission' took: {elapsed:.2?}"); + } match record { Some(_) => Ok("Success create permission".into()), None => bail!("Failed to create permission"), } } + #[instrument(skip(self, data), err)] pub async fn query_update_permission( &self, data: PermissionsSchema, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let record_key = get_id(&data.id)?; let existing = self.query_permission_by_id(data.id.id.to_raw()).await?; @@ -119,13 +164,21 @@ impl<'a> PermissionsRepository<'a> { }; let record: Option = db.update(record_key).merge(merged).await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_update_permission' took: {elapsed:.2?}"); + } match record { Some(_) => Ok("Success update permission".into()), None => bail!("Failed to update permission"), } } + #[instrument(skip(self, id), err)] pub async fn query_delete_permission(&self, id: String) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let permission_id = make_thing(&ResourceEnum::Permissions.to_string(), &id); let permission = self @@ -139,6 +192,12 @@ impl<'a> PermissionsRepository<'a> { .update(record_key) .merge(serde_json::json!({ "is_deleted": true })) .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_delete_permission' took: {elapsed:.2?}"); + } match record { Some(_) => Ok("Success delete permission".into()), None => bail!("Failed to delete permission"), diff --git a/imphnen-iam/src/v1/permissions/permissions_service.rs b/imphnen-iam/src/v1/permissions/permissions_service.rs index 8ce21f2..7d8f730 100644 --- a/imphnen-iam/src/v1/permissions/permissions_service.rs +++ b/imphnen-iam/src/v1/permissions/permissions_service.rs @@ -1,7 +1,7 @@ use crate::{ - common_response, make_thing, success_list_response, success_response, - validate_request, AppState, MetaRequestDto, PermissionsRepository, - PermissionsSchema, ResourceEnum, ResponseListSuccessDto, ResponseSuccessDto, + AppState, MetaRequestDto, PermissionsRepository, PermissionsSchema, ResourceEnum, + ResponseListSuccessDto, ResponseSuccessDto, common_response, make_thing, + success_list_response, success_response, validate_request, }; use axum::http::StatusCode; use axum::response::Response; diff --git a/imphnen-iam/src/v1/roles/roles_dto.rs b/imphnen-iam/src/v1/roles/roles_dto.rs index 6cb541e..5cc289f 100644 --- a/imphnen-iam/src/v1/roles/roles_dto.rs +++ b/imphnen-iam/src/v1/roles/roles_dto.rs @@ -64,3 +64,16 @@ pub struct RolesDetailQueryDto { pub created_at: Option, pub updated_at: Option, } + +impl Default for RolesDetailQueryDto { + fn default() -> Self { + Self { + id: Thing::from(("".to_string(), surrealdb::sql::Id::Number(0))), + name: String::new(), + permissions: Vec::new(), + is_deleted: false, + created_at: None, + updated_at: None, + } + } +} diff --git a/imphnen-iam/src/v1/roles/roles_enum.rs b/imphnen-iam/src/v1/roles/roles_enum.rs index 202c22f..5b43c09 100644 --- a/imphnen-iam/src/v1/roles/roles_enum.rs +++ b/imphnen-iam/src/v1/roles/roles_enum.rs @@ -5,6 +5,7 @@ pub enum RolesEnum { Admin, User, Staff, + Mentor, // Added Mentor role } impl fmt::Display for RolesEnum { @@ -13,7 +14,8 @@ impl fmt::Display for RolesEnum { RolesEnum::Admin => "Admin", RolesEnum::User => "User", RolesEnum::Staff => "Staff", + RolesEnum::Mentor => "Mentor", // Added Mentor role }; - write!(f, "{}", roles_str) + write!(f, "{roles_str}") } } diff --git a/imphnen-iam/src/v1/roles/roles_repository.rs b/imphnen-iam/src/v1/roles/roles_repository.rs index 13fce3d..90743c3 100644 --- a/imphnen-iam/src/v1/roles/roles_repository.rs +++ b/imphnen-iam/src/v1/roles/roles_repository.rs @@ -7,8 +7,11 @@ use crate::{ }; use anyhow::{Result, bail}; use imphnen_utils::{DetailQueryBuilder, QueryListBuilder}; +use serde_json; +use std::time::Instant; use surrealdb::Uuid; use surrealdb::sql::Thing; +use tracing::instrument; pub struct RolesRepository<'a> { state: &'a AppState, @@ -19,10 +22,12 @@ impl<'a> RolesRepository<'a> { Self { state } } + #[instrument(skip(self, meta), err)] pub async fn query_role_list( &self, meta: MetaRequestDto, ) -> Result>> { + let now = Instant::now(); let result: ResponseListSuccessDto> = QueryListBuilder::new( &self.state.surrealdb_ws, &ResourceEnum::Roles.to_string(), @@ -31,6 +36,12 @@ impl<'a> RolesRepository<'a> { .search_field("name") .build() .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_role_list' took: {elapsed:.2?}"); + } let data = result .data .into_iter() @@ -42,48 +53,71 @@ impl<'a> RolesRepository<'a> { }) } + #[instrument(skip(self, name), err)] pub async fn query_role_by_name( &self, name: String, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let builder = DetailQueryBuilder::new(ResourceEnum::Roles.to_string()) - .with_where("name") - .where_value(name.clone()) + .with_where("name", Some(name.clone())) .with_select_fields(vec!["*"]) .with_fetch("permissions"); let sql = builder.build(); - let result: Option = builder - .apply_bindings(db.query(sql).bind(("name", name))) - .await? - .take(0)?; - let role = match result { - Some(r) if !r.is_deleted => r, - _ => bail!("Role not found"), - }; + let mut response = builder.apply_bindings(db.query(sql)).await?; + + let result_vec: Vec = response.take(0).map_err(|e| { + anyhow::anyhow!("Failed to take result from response: {:?}", e) + })?; + + let role = result_vec + .into_iter() + .next() + .ok_or_else(|| anyhow::anyhow!("Role not found"))?; + + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_role_by_name' took: {elapsed:.2?}"); + } Ok(RolesDetailItemDto::from(&role)) } + #[instrument(skip(self, id), err)] pub async fn query_role_by_id(&self, id: String) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; + let thing_id = make_thing(&ResourceEnum::Roles.to_string(), &id); let builder = DetailQueryBuilder::new(ResourceEnum::Roles.to_string()) .with_id(&id) .with_select_fields(vec!["*"]) .with_fetch("permissions"); + let sql = builder.build(); + let sql_debug = sql.to_string(); // Move this line here let result: Option = builder.apply_bindings(db.query(sql)).await?.take(0)?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_role_by_id' took: {elapsed:.2?}"); + } let role = match result { Some(r) if !r.is_deleted => r, - _ => bail!("Role not found"), + _ => bail!("Role not found sql: {} id: {}", sql_debug, thing_id), }; Ok(RolesDetailItemDto::from(&role)) } + #[instrument(skip(self, payload), err)] pub async fn query_create_role( &self, payload: RolesRequestCreateDto, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let role_id = Uuid::new_v4().to_string(); let permission_things: Vec = payload @@ -103,14 +137,22 @@ impl<'a> RolesRepository<'a> { .create((&ResourceEnum::Roles.to_string(), role_id)) .content(role) .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_create_role' took: {elapsed:.2?}"); + } Ok("Role with permissions created successfully".into()) } + #[instrument(skip(self, id, data), err)] pub async fn query_update_role( &self, id: String, data: RolesRequestUpdateDto, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let existing = self.query_role_by_id(id.clone()).await?; if existing.is_deleted { @@ -119,13 +161,21 @@ impl<'a> RolesRepository<'a> { let merged = RolesSchema::update(data, id.clone(), existing); let record: Option = db.update(get_id(&merged.id)?).content(merged).await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_update_role' took: {elapsed:.2?}"); + } match record { Some(_) => Ok("Success update role".into()), None => bail!("Failed to update role"), } } + #[instrument(skip(self, id), err)] pub async fn query_delete_role(&self, id: String) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let role_id = make_thing(&ResourceEnum::Roles.to_string(), &id); let role = self.query_role_by_id(role_id.id.to_raw()).await?; @@ -137,6 +187,12 @@ impl<'a> RolesRepository<'a> { .update(record_key) .merge(serde_json::json!({ "is_deleted": true })) .await?; + let elapsed = now.elapsed(); + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_delete_role' took: {elapsed:.2?}"); + } match record { Some(_) => Ok("Success delete role".into()), None => bail!("Failed to delete role"), diff --git a/imphnen-iam/src/v1/users/users_dto.rs b/imphnen-iam/src/v1/users/users_dto.rs index 98f8b55..afd532f 100644 --- a/imphnen-iam/src/v1/users/users_dto.rs +++ b/imphnen-iam/src/v1/users/users_dto.rs @@ -1,13 +1,13 @@ use crate::{RolesDetailItemDto, RolesDetailQueryDto}; use lazy_static::lazy_static; -use regex::Regex; use serde::{Deserialize, Serialize}; use surrealdb::sql::Thing; use utoipa::ToSchema; use validator::Validate; lazy_static! { - static ref PASSWORD_REGEX: Regex = Regex::new(r"^[A-Za-z\d@$!%*?&]{8,}$").unwrap(); + static ref PASSWORD_REGEX: regex::Regex = + regex::Regex::new(r"^[A-Za-z\d@$!%*?&]{8,}$").unwrap(); } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] @@ -34,7 +34,7 @@ pub struct UsersCreateRequestDto { message = "Password must have at least 8 characters" ))] #[validate(regex( - path = "PASSWORD_REGEX", + path = "*PASSWORD_REGEX", message = "Password must include uppercase, lowercase, number, and special character" ))] pub password: String, @@ -62,6 +62,7 @@ pub struct UsersUpdateRequestDto { min = 8, message = "Password must have at least 8 characters" ))] + pub password: String, #[validate(length(min = 2, message = "Fullname at least have 2 character"))] pub fullname: String, #[validate(length( @@ -103,7 +104,7 @@ impl UsersDetailItemDto { email: dto.email.clone(), avatar: dto.avatar.clone(), phone_number: dto.phone_number.clone(), - is_active: dto.is_active.clone(), + is_active: dto.is_active, gender: dto.gender.clone(), birthdate: dto.birthdate.clone(), created_at: dto.created_at.clone(), @@ -169,18 +170,20 @@ pub struct UsersDetailQueryDto { pub role: RolesDetailQueryDto, pub created_at: String, pub updated_at: String, + pub mentor_id: Option, } impl UsersDetailQueryDto { pub fn from(&self) -> Self { Self { id: self.id.clone(), - role: RolesDetailQueryDto::from(self.role.clone()), + role: self.role.clone(), fullname: self.fullname.clone(), email: self.email.clone(), avatar: self.avatar.clone(), phone_number: self.phone_number.clone(), is_active: self.is_active, + mentor_id: self.mentor_id.clone(), gender: self.gender.clone(), is_deleted: self.is_deleted, password: self.password.clone(), @@ -190,3 +193,24 @@ impl UsersDetailQueryDto { } } } + +impl From<&UsersDetailItemDto> for UsersDetailQueryDto { + fn from(dto: &UsersDetailItemDto) -> Self { + Self { + id: crate::make_thing(&imphnen_libs::ResourceEnum::Users.to_string(), &dto.id), + fullname: dto.fullname.clone(), + email: dto.email.clone(), + avatar: dto.avatar.clone(), + phone_number: dto.phone_number.clone(), + is_active: dto.is_active, + is_deleted: false, + gender: dto.gender.clone(), + birthdate: dto.birthdate.clone(), + password: String::new(), + role: RolesDetailQueryDto::default(), + created_at: dto.created_at.clone(), + updated_at: dto.updated_at.clone(), + mentor_id: None, + } + } +} diff --git a/imphnen-iam/src/v1/users/users_repository.rs b/imphnen-iam/src/v1/users/users_repository.rs index 4492e92..36224e2 100644 --- a/imphnen-iam/src/v1/users/users_repository.rs +++ b/imphnen-iam/src/v1/users/users_repository.rs @@ -2,10 +2,16 @@ use super::{UsersDetailQueryDto, UsersListItemDto, UsersListQueryDto, UsersSchem use crate::{ AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto, get_id, make_thing, }; +use surrealdb::sql::Thing; use anyhow::{Result, bail}; use imphnen_utils::{DetailQueryBuilder, QueryListBuilder}; +use serde_json; +use std::time::Instant; use surrealdb::{Surreal, engine::remote::ws::Client}; + + + pub struct UsersRepository<'a> { state: &'a AppState, } @@ -30,10 +36,12 @@ impl<'a> UsersRepository<'a> { Self { state } } + pub async fn query_user_list( &self, meta: MetaRequestDto, ) -> Result>> { + let now = Instant::now(); let result: ResponseListSuccessDto> = QueryListBuilder::new( &self.state.surrealdb_ws, @@ -46,6 +54,14 @@ impl<'a> UsersRepository<'a> { .fetch_fields(vec!["role", "role.permissions"]) .build() .await?; + let elapsed = now.elapsed(); + + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_user_list' took: {elapsed:.2?}"); + } + let data = result .data .into_iter() @@ -57,70 +73,103 @@ impl<'a> UsersRepository<'a> { }) } + pub async fn query_user_by_email( &self, email: String, ) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let builder = DetailQueryBuilder::new(ResourceEnum::Users.to_string()) - .with_where("email") - .where_value(email.clone()) + .with_where("email", Some(email.clone())) .with_select_fields(vec!["*"]) .with_fetch("role") .with_fetch("role.permissions"); let sql = builder.build(); let user_opt: Option = builder.apply_bindings(db.query(sql)).await?.take(0)?; + let elapsed = now.elapsed(); + + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_user_by_email' took: {elapsed:.2?}"); + } + let Some(user) = user_opt else { bail!("User not found"); }; if user.is_deleted { bail!("User not found"); } - if user.role.is_deleted { + if user.role.updated_at.is_none() || user.role.is_deleted { bail!("User not found"); } Ok(UsersDetailQueryDto::from(&user)) } - pub async fn query_user_by_id(&self, id: String) -> Result { + + + + pub async fn query_user_by_id(&self, id: &Thing) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let builder = DetailQueryBuilder::new(ResourceEnum::Users.to_string()) - .with_id(&id) + .with_id(&id.id.to_raw()) .with_select_fields(vec!["*"]) .with_fetch("role") .with_fetch("role.permissions"); let sql = builder.build(); let result: Option = builder.apply_bindings(db.query(sql)).await?.take(0)?; + let elapsed = now.elapsed(); + + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_user_by_id' took: {elapsed:.2?}"); + } + let Some(user) = result else { - bail!("User not found"); + bail!("User not found in database"); }; if user.is_deleted { bail!("User not found"); } if user.role.is_deleted { - bail!("User not found"); + bail!("User's role has been deleted"); } Ok(UsersDetailQueryDto::from(&user)) } + pub async fn query_create_user(&self, data: UsersSchema) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let record: Option = db .create(ResourceEnum::Users.to_string()) .content(data) .await?; + let elapsed = now.elapsed(); + + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_create_user' took: {elapsed:.2?}"); + } + match record { Some(_) => Ok("Success create user".into()), None => bail!("Failed to create user"), } } + pub async fn query_update_user(&self, data: UsersSchema) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; let record_key = get_id(&data.id)?; - let existing = self.query_user_by_id(data.id.id.to_raw()).await?; + let existing = self.query_user_by_id(&data.id).await?; if existing.is_deleted { bail!("User already deleted"); } @@ -136,15 +185,25 @@ impl<'a> UsersRepository<'a> { ..data.clone() }; let record: Option = db.update(record_key).merge(merged).await?; + let elapsed = now.elapsed(); + + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_update_user' took: {elapsed:.2?}"); + } + match record { Some(_) => Ok("Success update user".into()), None => bail!("Failed to update user"), } } + pub async fn query_delete_user(&self, id: String) -> Result { + let now = Instant::now(); let db = &self.state.surrealdb_ws; - let user = self.query_user_by_id(id).await?; + let user = self.query_user_by_id(&make_thing(&ResourceEnum::Users.to_string(), &id)).await?; if user.is_deleted { bail!("User not found"); } @@ -153,9 +212,18 @@ impl<'a> UsersRepository<'a> { .update(record_key) .merge(serde_json::json!({ "is_deleted": true })) .await?; + let elapsed = now.elapsed(); + + if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) + == "development" + { + println!("Query 'query_delete_user' took: {elapsed:.2?}"); + } + match record { Some(_) => Ok("Success delete user".into()), None => bail!("Failed to delete user"), } } } + diff --git a/imphnen-iam/src/v1/users/users_schema.rs b/imphnen-iam/src/v1/users/users_schema.rs index fa5393d..62ea6bf 100644 --- a/imphnen-iam/src/v1/users/users_schema.rs +++ b/imphnen-iam/src/v1/users/users_schema.rs @@ -1,5 +1,6 @@ use super::{UsersCreateRequestDto, UsersDetailQueryDto, UsersUpdateRequestDto}; use imphnen_libs::{ResourceEnum, hash_password}; +use imphnen_utils::extract_id; use imphnen_utils::{get_iso_date, make_thing}; use serde::{Deserialize, Serialize}; use surrealdb::{Uuid, sql::Thing}; @@ -10,11 +11,16 @@ pub struct UsersSchema { pub fullname: String, pub email: String, pub password: String, + #[serde(skip_serializing_if = "Option::is_none")] pub avatar: Option, pub phone_number: String, pub is_active: bool, pub is_deleted: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub mentor_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub gender: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub birthdate: Option, pub role: Thing, pub created_at: String, @@ -35,6 +41,10 @@ impl Default for UsersSchema { phone_number: String::new(), is_active: false, is_deleted: false, + mentor_id: Some(make_thing( + &ResourceEnum::Users.to_string(), + &Uuid::new_v4().to_string(), + )), gender: None, birthdate: None, role: make_thing( @@ -57,12 +67,18 @@ impl UsersSchema { phone_number: dto.phone_number, is_active: dto.is_active, is_deleted: dto.is_deleted, + mentor_id: Some(dto.mentor_id.unwrap_or_else(|| { + make_thing( + &ResourceEnum::Users.to_string(), + &Uuid::new_v4().to_string(), + ) + })), gender: dto.gender, birthdate: dto.birthdate, password: dto.password, created_at: dto.created_at, updated_at: dto.updated_at, - role: make_thing(&ResourceEnum::Roles.to_string(), &dto.role.id.id.to_raw()), + role: make_thing(&ResourceEnum::Roles.to_string(), &extract_id(&dto.role.id)), } } @@ -95,6 +111,10 @@ impl UsersSchema { password, phone_number: user.phone_number, is_active: false, + mentor_id: Some(make_thing( + &ResourceEnum::Users.to_string(), + &Uuid::new_v4().to_string(), + )), gender: None, birthdate: None, avatar: None, @@ -112,4 +132,16 @@ impl UsersSchema { ..Self::from(dto) } } + + pub fn update_mentor_id(mut self, mentor_id: Option) -> Self { + self.mentor_id = match mentor_id { + Some(id) => Some(make_thing(&ResourceEnum::Users.to_string(), &id)), + None => Some(make_thing( + &ResourceEnum::Users.to_string(), + &Uuid::new_v4().to_string(), + )), + }; + self.updated_at = get_iso_date(); + self + } } diff --git a/imphnen-iam/src/v1/users/users_service.rs b/imphnen-iam/src/v1/users/users_service.rs index b401fc6..74f02d8 100644 --- a/imphnen-iam/src/v1/users/users_service.rs +++ b/imphnen-iam/src/v1/users/users_service.rs @@ -6,12 +6,15 @@ use crate::{ AppState, MetaRequestDto, ResponseListSuccessDto, UsersRepository, UsersSchema, }; use crate::{ - ResourceEnum, ResponseSuccessDto, common_response, extract_email, make_thing, - success_list_response, success_response, validate_request, + ResponseSuccessDto, common_response, extract_email, success_list_response, + success_response, validate_request, }; use axum::http::HeaderMap; use axum::{http::StatusCode, response::Response}; -use imphnen_libs::{hash_password, verify_password}; +use imphnen_libs::{ResourceEnum, hash_password, verify_password}; +use imphnen_utils::make_thing; +use uuid::Uuid; + pub struct UsersService; @@ -31,8 +34,12 @@ impl UsersService { } pub async fn get_user_by_id(state: &AppState, id: String) -> Response { + if Uuid::parse_str(&id).is_err() { + return common_response(StatusCode::BAD_REQUEST, "Invalid User ID format"); + } let repo = UsersRepository::new(state); - match repo.query_user_by_id(id).await { + let thing_id = make_thing(&ResourceEnum::Users.to_string(), &id); + match repo.query_user_by_id(&thing_id).await { Ok(user) if !user.is_deleted => success_response(ResponseSuccessDto { data: UsersDetailItemDto::from(&user), }), @@ -84,6 +91,9 @@ impl UsersService { id: String, user: UsersUpdateRequestDto, ) -> Response { + if Uuid::parse_str(&id).is_err() { + return common_response(StatusCode::BAD_REQUEST, "Invalid User ID format"); + } let repo = UsersRepository::new(state); if let Err((status, message)) = validate_request(&user) { return common_response(status, &message); @@ -124,9 +134,12 @@ impl UsersService { id: String, payload: UsersActiveInactiveRequestDto, ) -> Response { + if Uuid::parse_str(&id).is_err() { + return common_response(StatusCode::BAD_REQUEST, "Invalid User ID format"); + } let repo = UsersRepository::new(state); let thing_id = make_thing(&ResourceEnum::Users.to_string(), &id); - match repo.query_user_by_id(thing_id.id.to_raw()).await { + match repo.query_user_by_id(&thing_id).await { Ok(user) if !user.is_deleted => { let patch = UsersSchema { id: user.id.clone(), @@ -186,9 +199,28 @@ impl UsersService { } } - pub async fn delete_user(state: &AppState, id: String) -> Response { + pub async fn get_user_by_mentor_id( + state: &AppState, + mentor_id: String, + ) -> Response { let repo = UsersRepository::new(state); - if repo.query_user_by_id(id.clone()).await.is_err() { + let thing_id = make_thing(&ResourceEnum::Mentors.to_string(), &mentor_id); + match repo.query_user_by_id(&thing_id).await { + Ok(user) if !user.is_deleted => success_response(ResponseSuccessDto { + data: UsersDetailItemDto::from(&user), + }), + Ok(_) => common_response(StatusCode::NOT_FOUND, "User not found"), + Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()), + } + } + + pub async fn delete_user(state: &AppState, id: String) -> Response { + if Uuid::parse_str(&id).is_err() { + return common_response(StatusCode::BAD_REQUEST, "Invalid User ID format"); + } + let repo = UsersRepository::new(state); + let thing_id = make_thing(&ResourceEnum::Users.to_string(), &id); + if repo.query_user_by_id(&thing_id).await.is_err() { return common_response(StatusCode::BAD_REQUEST, "User not found"); } match repo.query_delete_user(id).await { diff --git a/imphnen-libs/Cargo.toml b/imphnen-libs/Cargo.toml index 92c4ade..29fcc4f 100644 --- a/imphnen-libs/Cargo.toml +++ b/imphnen-libs/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -imphnen-entities = { path = "../imphnen-entities" } +imphnen-entities.workspace = true log.workspace = true axum.workspace = true tokio.workspace = true @@ -15,4 +15,3 @@ chrono.workspace = true surrealdb.workspace = true jsonwebtoken.workspace = true dotenvy.workspace = true - diff --git a/imphnen-libs/src/argon/mod.rs b/imphnen-libs/src/argon/mod.rs index dd0ab23..6367b7b 100644 --- a/imphnen-libs/src/argon/mod.rs +++ b/imphnen-libs/src/argon/mod.rs @@ -1,9 +1,9 @@ use argon2::{ - password_hash::{ - rand_core::OsRng, Error, PasswordHash, PasswordHasher, PasswordVerifier, - SaltString, - }, Argon2, + password_hash::{ + Error, PasswordHash, PasswordHasher, PasswordVerifier, SaltString, + rand_core::OsRng, + }, }; pub fn hash_password(password: &str) -> Result { diff --git a/imphnen-libs/src/axum/mod.rs b/imphnen-libs/src/axum/mod.rs index cecaeae..e41ba93 100644 --- a/imphnen-libs/src/axum/mod.rs +++ b/imphnen-libs/src/axum/mod.rs @@ -1,11 +1,9 @@ -use crate::{load_env, surrealdb_init_mem, surrealdb_init_ws, Env}; +use crate::{Env, load_env, surrealdb_init_mem, surrealdb_init_ws}; use axum::{Router, serve}; use imphnen_entities::{SurrealMemClient, SurrealWsClient}; -use log::{debug, error, info}; use std::{future::Future, net::SocketAddr}; use tokio::net::TcpListener; - pub async fn axum_init(router_fn: F) where F: FnOnce(SurrealWsClient, SurrealMemClient) -> Fut, @@ -13,24 +11,19 @@ where { load_env(); let env = Env::new(); - info!("Environment loaded with port: {}", env.port); let surrealdb_ws = surrealdb_init_ws().await.expect("Failed surrealdb ws"); - info!("SurrealDB WS client initialized"); let surrealdb_mem = surrealdb_init_mem().await.expect("Failed surrealdb mem"); - info!("SurrealDB MEM client initialized"); let router = router_fn(surrealdb_ws, surrealdb_mem).await; - debug!("Router created successfully"); let port = env.port; let addr = SocketAddr::from(([0, 0, 0, 0], port)); let listener = TcpListener::bind(&addr).await.unwrap(); - info!("Listening on http://{}", addr); match serve(listener, router).await { - Ok(_) => info!("Server stopped gracefully."), - Err(err) => error!("Server encountered an error: {}", err), + Ok(_) => {} + Err(_err) => {} } } diff --git a/imphnen-libs/src/enviroment/mod.rs b/imphnen-libs/src/enviroment/mod.rs index 9d29a69..7c2fbe5 100644 --- a/imphnen-libs/src/enviroment/mod.rs +++ b/imphnen-libs/src/enviroment/mod.rs @@ -1,7 +1,7 @@ use std::env; pub fn load_env() { - dotenvy::dotenv().ok(); + if dotenvy::dotenv().is_ok() {} } pub struct Env { @@ -13,6 +13,7 @@ pub struct Env { pub surrealdb_password: String, pub surrealdb_namespace: String, pub surrealdb_dbname: String, + pub surrealdb_url_ws: String, pub smtp_email: String, pub smtp_password: String, pub smtp_name: String, @@ -31,112 +32,63 @@ impl Env { pub fn new() -> Self { Self { port: env::var("PORT") - .unwrap_or_else(|_| { - println!("INFO: PORT is not set, using default '3000'."); - "3000".to_string() - }) + .unwrap_or_else(|_| "3000".to_string()) .parse() .unwrap_or(3000), - rust_env: env::var("RUST_ENV").unwrap_or_else(|_| { - println!("INFO: RUST_ENV is not set, using default 'development'."); - "development".to_string() - }), + rust_env: env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()), - access_token_secret: env::var("ACCESS_TOKEN_SECRET").unwrap_or_else(|_| { - println!("WARNING: ACCESS_TOKEN_SECRET is not set, using fallback!"); - "default_access_secret".to_string() - }), + access_token_secret: env::var("ACCESS_TOKEN_SECRET") + .unwrap_or_else(|_| "default_access_secret".to_string()), - refresh_token_secret: env::var("REFRESH_TOKEN_SECRET").unwrap_or_else(|_| { - println!("WARNING: REFRESH_TOKEN_SECRET is not set, using fallback!"); - "default_refresh_secret".to_string() - }), + refresh_token_secret: env::var("REFRESH_TOKEN_SECRET") + .unwrap_or_else(|_| "default_refresh_secret".to_string()), - surrealdb_url: env::var("SURREALDB_URL").unwrap_or_else(|_| { - println!( - "WARNING: SURREALDB_URL is not set, using fallback 'http://localhost:8000'!" - ); - "http://localhost:8000".to_string() - }), + surrealdb_url: env::var("SURREALDB_URL") + .unwrap_or_else(|_| "http://localhost:8000".to_string()), - surrealdb_username: env::var("SURREALDB_USERNAME").unwrap_or_else(|_| { - println!("WARNING: SURREALDB_USERNAME is not set, using fallback 'root'!"); - "root".to_string() - }), + surrealdb_username: env::var("SURREALDB_USERNAME") + .unwrap_or_else(|_| "root".to_string()), - surrealdb_password: env::var("SURREALDB_PASSWORD").unwrap_or_else(|_| { - println!("WARNING: SURREALDB_PASSWORD is not set, using fallback!"); - "password".to_string() - }), + surrealdb_password: env::var("SURREALDB_PASSWORD") + .unwrap_or_else(|_| "password".to_string()), - surrealdb_namespace: env::var("SURREALDB_NAMESPACE").unwrap_or_else(|_| { - println!( - "WARNING: SURREALDB_NAMESPACE is not set, using fallback 'namespace'!" - ); - "namespace".to_string() - }), + surrealdb_namespace: env::var("SURREALDB_NAMESPACE") + .unwrap_or_else(|_| "namespace".to_string()), - surrealdb_dbname: env::var("SURREALDB_DBNAME").unwrap_or_else(|_| { - println!("WARNING: SURREALDB_DBNAME is not set, using fallback 'database'!"); - "database".to_string() - }), + surrealdb_dbname: env::var("SURREALDB_DBNAME") + .unwrap_or_else(|_| "database".to_string()), - smtp_email: env::var("SMTP_EMAIL").unwrap_or_else(|_| { - println!( - "WARNING: SMTP_EMAIL is not set, using fallback 'no-reply@example.com'!" - ); - "no-reply@example.com".to_string() - }), + surrealdb_url_ws: env::var("SURREALDB_URL_WS") + .unwrap_or_else(|_| "ws://localhost:8000/rpc".to_string()), - smtp_password: env::var("SMTP_PASSWORD").unwrap_or_else(|_| { - println!("WARNING: SMTP_PASSWORD is not set, using fallback!"); - "default_smtp_password".to_string() - }), + smtp_email: env::var("SMTP_EMAIL") + .unwrap_or_else(|_| "no-reply@example.com".to_string()), - smtp_name: env::var("SMTP_NAME").unwrap_or_else(|_| { - println!("WARNING: SMTP_NAME is not set, using fallback 'MyApp SMTP'!"); - "MyApp SMTP".to_string() - }), + smtp_password: env::var("SMTP_PASSWORD") + .unwrap_or_else(|_| "default_smtp_password".to_string()), - smtp_host: env::var("SMTP_HOST").unwrap_or_else(|_| { - println!("WARNING: SMTP_HOST is not set, using fallback 'smtp.gmail.com'!"); - "smtp.gmail.com".to_string() - }), + smtp_name: env::var("SMTP_NAME").unwrap_or_else(|_| "MyApp SMTP".to_string()), - redisdb_url: env::var("REDISDB_URL").unwrap_or_else(|_| { - println!("WARNING: REDISDB_URL is not set, using fallback 'localhost'!"); - "localhost".to_string() - }), + smtp_host: env::var("SMTP_HOST") + .unwrap_or_else(|_| "smtp.gmail.com".to_string()), - fe_url: env::var("FE_URL").unwrap_or_else(|_| { - println!("WARNING: FE_URL is not set, using fallback 'http://localhost'!"); - "http://localhost".to_string() - }), + redisdb_url: env::var("REDISDB_URL") + .unwrap_or_else(|_| "localhost".to_string()), - minio_endpoint: env::var("MINIO_ENDPOINT").unwrap_or_else(|_| { - println!( - "WARNING: MINIO_ENDPOINT is not set, using fallback 'http://localhost:9000'!" - ); - "http://localhost:9000".to_string() - }), + fe_url: env::var("FE_URL").unwrap_or_else(|_| "http://localhost".to_string()), - minio_bucket_name: env::var("MINIO_BUCKET_NAME").unwrap_or_else(|_| { - println!( - "WARNING: MINIO_BUCKET_NAME is not set, using fallback 'default_bucket'!" - ); - "default_bucket".to_string() - }), + minio_endpoint: env::var("MINIO_ENDPOINT") + .unwrap_or_else(|_| "http://localhost:9000".to_string()), - minio_access_key: env::var("MINIO_ACCESS_KEY").unwrap_or_else(|_| { - println!("WARNING: MINIO_ACCESS_KEY is not set, using fallback!"); - "minio_access".to_string() - }), + minio_bucket_name: env::var("MINIO_BUCKET_NAME") + .unwrap_or_else(|_| "default_bucket".to_string()), - minio_secret_key: env::var("MINIO_SECRET_KEY").unwrap_or_else(|_| { - println!("WARNING: MINIO_SECRET_KEY is not set, using fallback!"); - "minio_secret".to_string() - }), + minio_access_key: env::var("MINIO_ACCESS_KEY") + .unwrap_or_else(|_| "minio_access".to_string()), + + minio_secret_key: env::var("MINIO_SECRET_KEY") + .unwrap_or_else(|_| "minio_secret".to_string()), } } } diff --git a/imphnen-libs/src/jsonwebtoken/mod.rs b/imphnen-libs/src/jsonwebtoken/mod.rs index 5fb358f..c79c3f0 100644 --- a/imphnen-libs/src/jsonwebtoken/mod.rs +++ b/imphnen-libs/src/jsonwebtoken/mod.rs @@ -2,7 +2,7 @@ use super::Env; use axum::http::StatusCode; use chrono::{Duration, TimeDelta, Utc}; use jsonwebtoken::{ - decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation, + DecodingKey, EncodingKey, Header, TokenData, Validation, decode, encode, }; use serde::{Deserialize, Serialize}; @@ -51,7 +51,7 @@ pub fn decode_access_token( let env = Env::new(); let secret: String = env.access_token_secret; let result: Result, StatusCode> = decode( - &jwt_token, + jwt_token, &DecodingKey::from_secret(secret.as_ref()), &Validation::default(), ) @@ -81,7 +81,7 @@ pub fn decode_refresh_token( let env = Env::new(); let secret: String = env.refresh_token_secret; let result: Result, StatusCode> = decode( - &jwt_token, + jwt_token, &DecodingKey::from_secret(secret.as_ref()), &Validation::default(), ) diff --git a/imphnen-libs/src/lettre/mod.rs b/imphnen-libs/src/lettre/mod.rs index 9932265..e7a5fea 100644 --- a/imphnen-libs/src/lettre/mod.rs +++ b/imphnen-libs/src/lettre/mod.rs @@ -29,13 +29,7 @@ pub fn send_email( .credentials(smtp_credentials) .build(); match mailer.send(&email) { - Ok(_) => { - println!("Email sent successfully to {}", to); - Ok(()) - } - Err(e) => { - println!("Failed to send email: {}", e); - Err(Box::new(e)) - } + Ok(_) => Ok(()), + Err(e) => Err(Box::new(e)), } } diff --git a/imphnen-libs/src/surrealdb/mod.rs b/imphnen-libs/src/surrealdb/mod.rs index 86b95f6..e2ea1f1 100644 --- a/imphnen-libs/src/surrealdb/mod.rs +++ b/imphnen-libs/src/surrealdb/mod.rs @@ -1,10 +1,10 @@ use super::Env; use crate::SurrealMemClient; -use surrealdb::engine::local::Mem; +use crate::enviroment::load_env; use surrealdb::engine::any; +use surrealdb::engine::local::Mem; use surrealdb::opt::auth::Root; use surrealdb::{Result, Surreal}; -use crate::enviroment::load_env; pub mod resource; pub use resource::*; @@ -13,7 +13,6 @@ pub async fn surrealdb_init_ws() -> Result> { load_env(); let env = Env::new(); let db = any::connect(&env.surrealdb_url).await?; - db.signin(Root { username: &env.surrealdb_username, diff --git a/imphnen-libs/src/surrealdb/resource.rs b/imphnen-libs/src/surrealdb/resource.rs index 9167ba9..0b62b75 100644 --- a/imphnen-libs/src/surrealdb/resource.rs +++ b/imphnen-libs/src/surrealdb/resource.rs @@ -14,6 +14,7 @@ pub enum ResourceEnum { RolesPermissions, Events, Testimonials, + Mentors, } impl fmt::Display for ResourceEnum { @@ -31,7 +32,8 @@ impl fmt::Display for ResourceEnum { ResourceEnum::GachaCredits => "app_gacha_credits", ResourceEnum::Events => "app_events", ResourceEnum::Testimonials => "app_testimonials", + ResourceEnum::Mentors => "app_mentors", }; - write!(f, "{}", str) + write!(f, "{str}") } } diff --git a/imphnen-middleware/Cargo.toml b/imphnen-middleware/Cargo.toml index cafff4f..796ddc2 100644 --- a/imphnen-middleware/Cargo.toml +++ b/imphnen-middleware/Cargo.toml @@ -4,10 +4,10 @@ version = "0.1.0" edition = "2024" [dependencies] -imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" } -imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } -imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } -imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } +imphnen-iam.workspace = true +imphnen-libs.workspace = true +imphnen-utils.workspace = true +imphnen-entities.workspace = true axum.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/imphnen-utils/Cargo.toml b/imphnen-utils/Cargo.toml index 4823820..a008f34 100644 --- a/imphnen-utils/Cargo.toml +++ b/imphnen-utils/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2024" [dependencies] -imphnen-libs = { path = "../imphnen-libs"} -imphnen-entities = { path = "../imphnen-entities" } +imphnen-libs.workspace = true +imphnen-entities.workspace = true surrealdb.workspace = true axum.workspace = true chrono.workspace = true @@ -15,3 +15,7 @@ axum-test.workspace = true serde.workspace = true serde_json.workspace = true validator.workspace = true +strum.workspace = true +strum_macros.workspace = true +uuid.workspace = true +tracing.workspace = true diff --git a/imphnen-utils/src/bind_filter.rs b/imphnen-utils/src/bind_filter.rs index 70ee400..fedf43c 100644 --- a/imphnen-utils/src/bind_filter.rs +++ b/imphnen-utils/src/bind_filter.rs @@ -1,9 +1,9 @@ -use surrealdb::method::Query; use surrealdb::engine::any; +use surrealdb::method::Query; pub fn bind_filter_value( query: Query<'_, any::Any>, val: String, ) -> Query<'_, any::Any> { - query.bind(("filter", val)) // langsung string aja, udah cukup + query.bind(("filter", val)) } diff --git a/imphnen-utils/src/extract_email.rs b/imphnen-utils/src/extract_email.rs index 471bb83..6d61f3b 100644 --- a/imphnen-utils/src/extract_email.rs +++ b/imphnen-utils/src/extract_email.rs @@ -1,24 +1,13 @@ use crate::decode_access_token; -use axum::http::{header::AUTHORIZATION, HeaderMap}; +use axum::http::{HeaderMap, header::AUTHORIZATION}; pub fn extract_email(headers: &HeaderMap) -> Option { - println!("📥 Received headers: {:?}", headers); - let auth_header = headers.get(AUTHORIZATION)?.to_str().ok()?; - println!("🔍 Authorization Header: {}", auth_header); - let token = auth_header.strip_prefix("Bearer ")?; - println!("🧪 Token: {}", token); match decode_access_token(token) { - Ok(data) => { - println!("✅ Token claims: {:?}", data.claims); - Some(data.claims.sub) - } - Err(e) => { - eprintln!("❌ Failed to decode token: {}", e); - None - } + Ok(data) => Some(data.claims.sub), + Err(_e) => None, } } diff --git a/imphnen-utils/src/generate_otp.rs b/imphnen-utils/src/generate_otp.rs index 0769d72..ee98cb1 100644 --- a/imphnen-utils/src/generate_otp.rs +++ b/imphnen-utils/src/generate_otp.rs @@ -1,4 +1,4 @@ -use rand::{rng, Rng}; +use rand::{Rng, rng}; pub struct OtpManager; diff --git a/imphnen-utils/src/get_id.rs b/imphnen-utils/src/get_id.rs index edf19b4..3901507 100644 --- a/imphnen-utils/src/get_id.rs +++ b/imphnen-utils/src/get_id.rs @@ -11,6 +11,5 @@ pub fn get_id(thing: &Thing) -> Result<(&str, &str)> { } pub fn extract_id(thing: &Thing) -> String { - let id = thing.id.to_raw(); - id + thing.id.to_raw() } diff --git a/imphnen-utils/src/lib.rs b/imphnen-utils/src/lib.rs index a400ee9..a8b4713 100644 --- a/imphnen-utils/src/lib.rs +++ b/imphnen-utils/src/lib.rs @@ -1,16 +1,13 @@ -use imphnen_entities::*; -use imphnen_libs::*; - pub mod bind_filter; pub mod extract_email; pub mod generate_date; pub mod generate_otp; pub mod get_id; pub mod make_thing; -pub mod mock_test; pub mod query_builder; pub mod query_list; pub mod response_format; +pub mod serde_helpers; pub mod validator; pub use bind_filter::*; @@ -21,8 +18,11 @@ pub use get_id::*; pub use imphnen_entities::*; pub use imphnen_libs::*; pub use make_thing::*; -pub use mock_test::*; pub use query_builder::*; pub use query_list::*; pub use response_format::*; +pub use serde_helpers::{ + option_thing_or_string, serialize_option_thing, serialize_thing, + string_or_empty_string, thing_or_string, +}; pub use validator::*; diff --git a/imphnen-utils/src/make_thing.rs b/imphnen-utils/src/make_thing.rs index 735bf1a..1be94da 100644 --- a/imphnen-utils/src/make_thing.rs +++ b/imphnen-utils/src/make_thing.rs @@ -5,5 +5,5 @@ pub fn make_thing(table: &str, id: &str) -> Thing { } pub fn make_thing_str(table: &str, id: &str) -> String { - format!("{}:⟨{}⟩", table, id) + format!("{table}:⟨{id}⟩") } diff --git a/imphnen-utils/src/mock_test.rs b/imphnen-utils/src/mock_test.rs index e6beece..8b13789 100644 --- a/imphnen-utils/src/mock_test.rs +++ b/imphnen-utils/src/mock_test.rs @@ -1,52 +1 @@ -use crate::AppState; -use imphnen_libs::enviroment::load_env; -use surrealdb::engine::any; -use surrealdb::{Surreal, engine::local::Mem, opt::auth::Root}; -use super::Env; -pub async fn create_mock_app_state() -> AppState { - load_env(); - let env = Env::new(); - let db_mem = Surreal::new::(()).await.unwrap(); - let db_ws = any::connect(&env.surrealdb_url).await.unwrap(); - db_mem.use_ns("test").use_db("test").await.unwrap(); - db_ws - .signin(Root { - username: "root", - password: "root", - }) - .await - .unwrap(); - db_ws.use_ns("test").use_db("test").await.unwrap(); - - AppState { - surrealdb_mem: db_mem, - surrealdb_ws: db_ws, - } -} - -pub async fn cleanup_db() { - let app_state = create_mock_app_state().await; - let _ = app_state - .surrealdb_mem - .query( - r#" - REMOVE TABLE app_users; - REMOVE TABLE app_roles; - REMOVE TABLE app_users_cache; - REMOVE TABLE app_otp_cache; -"#, - ) - .await; - let _ = app_state - .surrealdb_ws - .query( - r#" - REMOVE TABLE app_users; - REMOVE TABLE app_roles; - REMOVE TABLE app_users_cache; - REMOVE TABLE app_otp_cache; -"#, - ) - .await; -} diff --git a/imphnen-utils/src/query_builder.rs b/imphnen-utils/src/query_builder.rs index ac996f2..6ac08a9 100644 --- a/imphnen-utils/src/query_builder.rs +++ b/imphnen-utils/src/query_builder.rs @@ -1,7 +1,8 @@ use imphnen_libs::MetaRequestDto; +use serde_json::{Map, Value}; +use surrealdb::engine::any; use surrealdb::method::Query; use surrealdb::sql::Thing; -use surrealdb::engine::any; pub struct ListQueryBuilder { resource: String, @@ -57,8 +58,7 @@ impl ListQueryBuilder { if let Some(search) = search { if !search.is_empty() { self.conditions.push(format!( - "string::contains(string::lowercase({} ?? ''), string::lowercase($search))", - field + "string::contains(string::lowercase({field} ?? ''), string::lowercase($search))" )); } } @@ -69,8 +69,7 @@ impl ListQueryBuilder { if let (Some(f), Some(v)) = (field, value) { if !v.is_empty() { self.conditions.push(format!( - "string::contains(string::join('', [{}]), $filter)", - f + "string::contains(string::join('', [{f}]), $filter)" )); } } @@ -111,7 +110,7 @@ impl ListQueryBuilder { let order_clause = if let Some(field) = self.order_by { let ord = self.order.unwrap_or_else(|| "ASC".into()); - format!("ORDER BY {} {}", field, ord) + format!("ORDER BY {field} {ord}") } else { String::new() }; @@ -145,16 +144,26 @@ impl ListQueryBuilder { fetch_clause ) } + + pub fn build_count(self) -> String { + let where_clause = if !self.conditions.is_empty() { + format!("WHERE {}", self.conditions.join(" AND ")) + } else { + String::new() + }; + + format!("SELECT count() FROM {} {}", self.resource, where_clause) + } } pub struct DetailQueryBuilder { resource: String, id: Option, thing: Option, - where_field: Option, - where_value: Option, select_fields: Vec, fetch_fields: Vec, + conditions: Vec, + bindings: Map, } impl DetailQueryBuilder { @@ -163,40 +172,59 @@ impl DetailQueryBuilder { resource: resource.into(), id: None, thing: None, - where_field: None, - where_value: None, select_fields: vec![], fetch_fields: vec![], + conditions: vec![], + bindings: Map::new(), } } pub fn with_id(mut self, id: impl Into) -> Self { - if self.where_field.is_some() || self.thing.is_some() { - panic!("Cannot use with_id() after with_where() or with_thing()"); + if self.thing.is_some() || !self.conditions.is_empty() { + panic!( + "Cannot use with_id() after with_thing() or with_where()/with_condition()" + ); } self.id = Some(id.into()); self } pub fn with_thing(mut self, thing: &Thing) -> Self { - if self.id.is_some() || self.where_field.is_some() { - panic!("Cannot use with_thing() after with_id() or with_where()"); + if self.id.is_some() || !self.conditions.is_empty() { + panic!( + "Cannot use with_thing() after with_id() or with_where()/with_condition()" + ); } - self.thing = Some(thing.to_string()); // app_users:uuid - self.resource = thing.tb.clone(); // update resource dari thing + self.thing = Some(thing.to_string()); + self.resource = thing.tb.clone(); self } - pub fn with_where(mut self, field: impl Into) -> Self { + // Modified with_where method + pub fn with_where( + mut self, + field: impl Into, + value: Option>, + ) -> Self { if self.id.is_some() || self.thing.is_some() { panic!("Cannot use with_where() after with_id() or with_thing()"); } - self.where_field = Some(field.into()); + let field_str = field.into(); + if let Some(val) = value { + // Using a distinct binding key to avoid conflicts + self.conditions.push(format!("{field_str} = $value_where")); + self + .bindings + .insert("value_where".to_string(), Value::String(val.into())); + } else { + // If no value, assume it's a direct condition string (e.g., "is_active = true") + self.conditions.push(field_str); + } self } - pub fn where_value(mut self, value: impl Into) -> Self { - self.where_value = Some(value.into()); + pub fn with_condition(mut self, condition: &str) -> Self { + self.conditions.push(condition.to_string()); self } @@ -220,32 +248,41 @@ impl DetailQueryBuilder { let fetch_clause = if self.fetch_fields.is_empty() { String::new() } else { - format!("FETCH {}", self.fetch_fields.join(", ")) + format!("FETCH {}", self.fetch_fields.join(", ")) // Fixed: Changed self.fetch to self.fetch_fields }; - let from_clause = if let Some(thing) = &self.thing { + // Determine the base FROM clause + let mut from_clause_base = if let Some(thing) = &self.thing { thing.to_string() - } else if let Some(id) = &self.id { - format!("{}:⟨{}⟩", self.resource, id) - } else if let (Some(field), Some(_)) = (&self.where_field, &self.where_value) { - format!("{} WHERE {} = $value", self.resource, field) + } else if let Some(id_val) = &self.id { + format!("{}:⟨{}⟩", self.resource, id_val) } else { - panic!( - "You must set one of with_id(), with_thing(), or with_where()+where_value()" - ); + self.resource.clone() // Start with resource name for WHERE queries }; - format!( - "SELECT {} FROM {} {}", - select_clause, from_clause, fetch_clause - ) + // Add WHERE clause based on accumulated conditions + if !self.conditions.is_empty() { + // This logic needs to be careful: if `from_clause_base` already contains `WHERE` (e.g. from `id` lookup), + // then `conditions` should append with `AND`. But for `DetailQueryBuilder`, only one `WHERE` style is expected. + // The panic conditions in `with_id`, `with_thing`, `with_where` should prevent logical conflicts. + from_clause_base = format!( + "{} WHERE {}", + from_clause_base, + self.conditions.join(" AND ") + ); + } + + format!("SELECT {select_clause} FROM {from_clause_base} {fetch_clause}") } - pub fn apply_bindings<'q>(&self, query: Query<'q, any::Any>) -> Query<'q, any::Any> { - if let (Some(_), Some(value)) = (&self.where_field, &self.where_value) { - query.bind(("value", value.clone())) - } else { - query + // Modified apply_bindings to clone both key and value + pub fn apply_bindings<'q>( + &self, + mut query: Query<'q, any::Any>, + ) -> Query<'q, any::Any> { + for (key, val) in &self.bindings { + query = query.bind((key.clone(), val.clone())); // Clone both key and value } + query } } diff --git a/imphnen-utils/src/query_list.rs b/imphnen-utils/src/query_list.rs index 94b616b..6777304 100644 --- a/imphnen-utils/src/query_list.rs +++ b/imphnen-utils/src/query_list.rs @@ -1,117 +1,142 @@ -use crate::{CountResult, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto}; use anyhow::Result; -use serde::{Serialize, de::DeserializeOwned}; -use surrealdb::Surreal; +use imphnen_entities::{ + CountResult, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto, +}; +use serde::{de::DeserializeOwned, Serialize}; use surrealdb::engine::any; +use surrealdb::Surreal; +use tracing; pub struct QueryListBuilder<'a> { - db: &'a Surreal, - table: &'a str, - meta: &'a MetaRequestDto, - conditions: Vec, - search_field: String, - select_fields: Option>, - fetch_fields: Option>, + db: &'a Surreal, + table: &'a str, + meta: &'a MetaRequestDto, + conditions: Vec, + search_field: String, + select_fields: Option>, + fetch_fields: Option>, + cast_thing_fields: bool, } impl<'a> QueryListBuilder<'a> { - pub fn new( - db: &'a Surreal, - table: &'a str, - meta: &'a MetaRequestDto, - ) -> Self { - Self { - db, - table, - meta, - conditions: vec![], - search_field: "name".to_string(), - select_fields: None, - fetch_fields: None, - } - } + pub fn new( + db: &'a Surreal, + table: &'a str, + meta: &'a MetaRequestDto, + ) -> Self { + Self { + db, + table, + meta, + conditions: vec![], + search_field: "name".to_string(), + select_fields: None, + fetch_fields: None, + cast_thing_fields: false, + } + } - pub fn search_field(mut self, field: &'a str) -> Self { - self.search_field = field.to_string(); - self - } + pub fn search_field(mut self, field: &'a str) -> Self { + self.search_field = field.to_string(); + self + } - pub fn select_fields(mut self, fields: Vec<&'a str>) -> Self { - self.select_fields = Some(fields); - self - } + pub fn select_fields(mut self, fields: Vec<&'a str>) -> Self { + self.select_fields = Some(fields); + self + } - pub fn fetch_fields(mut self, fields: Vec<&'a str>) -> Self { - self.fetch_fields = Some(fields); - self - } + pub fn fetch_fields(mut self, fields: Vec<&'a str>) -> Self { + self.fetch_fields = Some(fields); + self + } - pub fn with_condition(mut self, condition: &str) -> Self { - self.conditions.push(condition.to_string()); - self - } + pub fn with_condition(mut self, condition: &str) -> Self { + self.conditions.push(condition.to_string()); + self + } - pub async fn build(self) -> Result>> - where - T: DeserializeOwned + Serialize, - { - let page = self.meta.page.unwrap_or(1).max(1); - let per_page = self.meta.per_page.unwrap_or(10).max(1); - let start = (page - 1) * per_page; + pub fn with_cast_thing_fields(mut self) -> Self { + self.cast_thing_fields = true; + self + } - let sql = crate::ListQueryBuilder::from_meta( - self.table, - self.meta, - &self.search_field, - self.select_fields, - self.fetch_fields, - ) - .build(); + pub async fn build(self) -> Result>> + where + T: DeserializeOwned + Serialize, + { + let page = self.meta.page.unwrap_or(1).max(1); + let per_page = self.meta.per_page.unwrap_or(10).max(1); + let start = (page - 1) * per_page; - let mut query_exec = self.db.query(sql); - if let Some(search) = &self.meta.search { - if !search.is_empty() { - query_exec = query_exec.bind(("search", search.to_lowercase())); - } - } - if let Some(filter_val) = &self.meta.filter { - query_exec = crate::bind_filter_value(query_exec, filter_val.clone()); - } - query_exec = query_exec - .bind(("per_page", per_page)) - .bind(("start", start)); + // --- Data Query --- + let data_query_builder = crate::ListQueryBuilder::from_meta( + self.table, + self.meta, + &self.search_field, + self.select_fields, + self.fetch_fields, + ); + let data_sql = data_query_builder.build(); - let raw: Vec = query_exec.await?.take(0)?; + // --- Count Query --- + let count_query_builder = crate::ListQueryBuilder::from_meta( + self.table, + self.meta, + &self.search_field, + None, // No select fields for count + None, // No fetch fields for count + ); + let count_sql = count_query_builder.build_count(); - let mut count_query = self.db.query(format!( - "SELECT count() FROM {} {}", - self.table, - if self.conditions.is_empty() { - "".into() - } else { - format!("WHERE {}", self.conditions.join(" AND ")) - } - )); + // Combine both queries into a single query string within a transaction for a single database call + let combined_sql = format!( + "BEGIN; {}; {}; COMMIT;", + data_sql, + count_sql + ); - if let Some(search) = &self.meta.search { - if !search.is_empty() { - count_query = count_query.bind(("search", search.clone())); - } - } - if let Some(filter_val) = &self.meta.filter { - count_query = crate::bind_filter_value(count_query, filter_val.clone()); - } + let mut query_exec = self.db.query(combined_sql.clone()); - let count_result: Vec = count_query.await?.take(0)?; - let total = count_result.first().map(|c| c.count); + // Bind parameters for both data and count queries. + // It's assumed that the parameters are named consistently and applied to both. + // The ListQueryBuilder already uses $search, $per_page, $start, $filter. + if let Some(search) = &self.meta.search { + if !search.is_empty() { + query_exec = query_exec.bind(("search", search.to_lowercase())); + } + } + if let Some(filter_val) = &self.meta.filter { + query_exec = crate::bind_filter_value(query_exec, filter_val.clone()); + } + query_exec = query_exec + .bind(("per_page", per_page)) + .bind(("start", start)); - Ok(ResponseListSuccessDto { - data: raw, - meta: Some(MetaResponseDto { - page: Some(page), - per_page: Some(per_page), - total, - }), - }) - } + let query_debug_str = format!("{:?}", &query_exec); + + let mut response = query_exec.await.map_err(|e| { + tracing::error!( + query = %combined_sql, // `combined_sql` is cloned, so it can be borrowed here + full_query_object = %query_debug_str, + "Failed to execute combined query: {:?}", e + ); + e + })?; + + // Extract results: first for the data, then for the count + let raw: Vec = response.take(0)?; // First result is the data + let count_result: Vec = response.take(1)?; // Second result is the count + + let total = count_result.first().map(|c| c.count); + + Ok(ResponseListSuccessDto { + data: raw, + meta: Some(MetaResponseDto { + page: Some(page), + per_page: Some(per_page), + total, + }), + }) + } } diff --git a/imphnen-utils/src/response_format.rs b/imphnen-utils/src/response_format.rs index caf701e..ec9285b 100644 --- a/imphnen-utils/src/response_format.rs +++ b/imphnen-utils/src/response_format.rs @@ -1,7 +1,7 @@ use axum::{ + Json, http::StatusCode, response::{IntoResponse, Response}, - Json, }; use serde::Serialize; use serde_json::json; diff --git a/imphnen-utils/src/serde_helpers.rs b/imphnen-utils/src/serde_helpers.rs new file mode 100644 index 0000000..3cfe5e6 --- /dev/null +++ b/imphnen-utils/src/serde_helpers.rs @@ -0,0 +1,130 @@ +use serde::{self, Deserialize, Deserializer, Serializer}; +use surrealdb::sql::Thing; + +use serde::de::{self}; +use serde::ser::Serialize; +use serde_json::Value; +use std::str::FromStr; + +pub fn thing_or_string<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let v = Value::deserialize(deserializer)?; + match &v { + Value::Object(map) => { + if let Some(id_val) = map.get("Id") { + if let Value::Object(id_map) = id_val { + if let Some(Value::String(s)) = id_map.get("String") { + return Thing::from_str(s).map_err(|e| { + de::Error::custom(format!("Thing::from_str error: {e:?}")) + }); + } + } + } + serde_json::from_value(v).map_err(de::Error::custom) + } + Value::String(s) => { + if s.is_empty() { + Thing::from_str("unknown:empty") + .map_err(|e| de::Error::custom(format!("Thing::from_str error: {e:?}"))) + } else { + Thing::from_str(s) + .map_err(|e| de::Error::custom(format!("Thing::from_str error: {e:?}"))) + } + } + _ => Err(de::Error::custom( + "Expected SurrealDB Thing object, string, or enum Id::String", + )), + } +} + +pub fn option_thing_or_string<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let v = Value::deserialize(deserializer)?; + match &v { + Value::Null => Ok(None), + Value::Object(map) => { + if let Some(id_val) = map.get("Id") { + if let Value::Object(id_map) = id_val { + if let Some(Value::String(s)) = id_map.get("String") { + return Ok(Some(Thing::from_str(s).map_err(|e| { + de::Error::custom(format!("Thing::from_str error: {e:?}")) + })?)); + } + } + } + Ok(Some(serde_json::from_value(v).map_err(de::Error::custom)?)) + } + Value::String(s) => { + if s.is_empty() { + Ok(None) + } else { + Ok(Some(Thing::from_str(s).map_err(|e| { + de::Error::custom(format!("Thing::from_str error: {e:?}")) + })?)) + } + } + _ => Err(de::Error::custom( + "Expected SurrealDB Thing object, string, enum Id::String, or null", + )), + } +} + +pub fn string_or_empty_string<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let v = Value::deserialize(deserializer)?; + match v { + Value::String(s) => Ok(s), + Value::Null => Ok(String::new()), + _ => Err(de::Error::custom("Expected a string or null")), + } +} + +pub fn serialize_thing(thing: &Thing, serializer: S) -> Result +where + S: Serializer, +{ + thing.to_string().serialize(serializer) +} + +pub fn serialize_option_thing( + thing: &Option, + serializer: S, +) -> Result +where + S: Serializer, +{ + match thing { + Some(t) => Some(t.to_string()).serialize(serializer), + None => None::.serialize(serializer), + } +} + +pub fn serialize_datetime( + datetime: &chrono::DateTime, + serializer: S, +) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&datetime.to_rfc3339()) +} + +pub fn deserialize_datetime<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + chrono::DateTime::parse_from_rfc3339(&s) + .map_err(de::Error::custom) + .map(|dt| dt.with_timezone(&chrono::Utc)) +} diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..340ac25 --- /dev/null +++ b/test.sh @@ -0,0 +1,905 @@ +#!/bin/bash + +# ============================================================================== +# IMPHNEN API Comprehensive Test Suite (Bash Version) +# ============================================================================== + +BASE_URL="http://127.0.0.1:4099" +TEST_EMAIL="admin@example.com" +TEST_PASSWORD="password" + +declare -A ALL_USERS=( + ["admin@example.com"]="Admin" + ["staff@example.com"]="Staff" + ["user@example.com"]="User" + ["mentor@example.com"]="Mentor User" +) + +START_SERVER=false +SKIP_BASIC=false +SKIP_COMPREHENSIVE=false +SKIP_CRUD=false +GENERATE_REPORT=false +VERBOSE=false + +while getopts "sbcrgvh" opt; do + case ${opt} in + s ) START_SERVER=true ;; + b ) SKIP_BASIC=true ;; + c ) SKIP_COMPREHENSIVE=true ;; + r ) SKIP_CRUD=true ;; + g ) GENERATE_REPORT=true ;; + v ) VERBOSE=true ;; + h ) + echo "IMPHNEN API Test Suite" + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " -s Start server automatically" + echo " -b Skip basic tests (auth, error handling)" + echo " -c Skip comprehensive tests (users, roles, mentors, etc.)" + echo " -r Skip CRUD and advanced tests" + echo " -g Generate JSON test report" + echo " -v Verbose output (show all INFO logs)" + echo " -h Show this help message" + echo "" + echo "Examples:" + echo " $0 # Run all tests" + echo " $0 -s # Start server and run all tests" + echo " $0 -g # Run tests and generate report" + echo " $0 -sv # Start server with verbose output" + echo " $0 -bc # Run only public endpoint tests" + exit 0 + ;; + \? ) echo "Invalid option: -$OPTARG" >&2; echo "Use -h for help" >&2; exit 1 ;; + esac +done + +if ! command -v curl &> /dev/null; then + echo "Error: 'curl' tidak ditemukan. Mohon install terlebih dahulu." >&2 + exit 1 +fi +if ! command -v jq &> /dev/null; then + echo "Error: 'jq' tidak ditemukan. Mohon install terlebih dahulu." >&2 + exit 1 +fi + +TEST_START_TIME=$(date +%s) +AUTH_TOKEN="" +SERVER_PID="" +TEST_RESULTS=() +FALED_TESTS_SUMMARY=() +PASS_COUNT=0 +FAIL_COUNT=0 +TEST_TESTIMONIAL_ID="" + +CYAN='\033[0;36m' +YELLOW='\033[0;33m' +GREEN='\033[0;32m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' + +cleanup() { + if [ -n "$SERVER_PID" ]; then + printf "\n${YELLOW}Menghentikan proses server...${NC}\n" + kill "$SERVER_PID" &>/dev/null + fi +} +trap cleanup EXIT + +write_test_log() { + local level=$1 + local message=$2 + local color=$NC + + case $level in + "SUCCESS") color=$GREEN ;; + "ERROR") color=$RED ;; + "WARN") color=$YELLOW ;; + "INFO") color=$CYAN ;; + esac + + if [[ "$VERBOSE" = true || "$level" != "INFO" ]]; then + printf "[$(date +'%H:%M:%S')] [${color}%-7s${NC}] %s\n" "$level" "$message" >&2 + fi +} + +test_api_endpoint() { + local test_name=$1 + local method=$2 + local endpoint=$3 + local expected_status=$4 + local body=$5 + local require_auth=$6 + + local headers=(-H "Content-Type: application/json") + if [[ "$require_auth" = true && -n "$AUTH_TOKEN" ]]; then + headers+=(-H "Authorization: Bearer $AUTH_TOKEN") + elif [[ "$require_auth" = true && -z "$AUTH_TOKEN" ]]; then + write_test_log "WARN" "✗ $test_name - Dilewati: token autentikasi tidak tersedia" + return + fi + + local start_req_time=$(date +%s%3N) + + response=$(curl -s -w "\n%{http_code}" -X "$method" "${headers[@]}" -d "$body" "$BASE_URL$endpoint") + + http_status=$(echo "$response" | tail -n1) + response_body=$(echo "$response" | sed '$d') + + local end_req_time=$(date +%s%3N) + local duration=$((end_req_time - start_req_time)) + + local status="FAIL" + local error_msg="" + + if [ "$http_status" -eq "$expected_status" ]; then + status="PASS" + ((PASS_COUNT++)) + write_test_log "SUCCESS" "✓ $test_name - Sukses (Status: $http_status, Waktu: ${duration}ms)" + else + status="FAIL" + ((FAIL_COUNT++)) +write_test_log "ERROR" " Request Body: $body" + write_test_log "ERROR" " Response Body: $response_body" + error_msg="Status yang diharapkan $expected_status, tetapi mendapat $http_status." + write_test_log "ERROR" "✗ $test_name - Gagal: $error_msg" + FAILED_TESTS_SUMMARY+=("✗ $test_name - $error_msg") + fi + + result_json=$(jq -n --arg name "$test_name" --arg ep "$endpoint" --arg meth "$method" \ + --arg stat "$status" --arg code "$http_status" --arg dur "$duration" \ + --arg err "$error_msg" \ + '{TestName: $name, Endpoint: $ep, Method: $meth, Status: $stat, StatusCode: $code, ResponseTimeMs: $dur, Error: $err}') + TEST_RESULTS+=("$result_json") + # Return response_body for further processing if needed by the caller +} + +test_server_connection() { + curl -s --head "$BASE_URL/v1/cms/landing/events" > /dev/null + return $? +} + +clear_database() { + write_test_log "INFO" "Membersihkan database via WebSocket..." + if ! cargo run --bin clear_db_test --release; then + write_test_log "ERROR" "Gagal membersihkan database." + exit 1 + fi + write_test_log "SUCCESS" "Pembersihan database selesai." +} + +get_auth_token() { + write_test_log "INFO" "Mengautentikasi test user..." + local login_data + login_data=$(jq -n --arg email "$TEST_EMAIL" --arg pass "$TEST_PASSWORD" '{email: $email, password: $pass}') + + local headers=(-H "Content-Type: application/json") + local start_req_time=$(date +%s%3N) + + response=$(curl -s -w "\n%{http_code}" -X "POST" "${headers[@]}" -d "$login_data" "$BASE_URL/v1/auth/login") + + local http_status=$(echo "$response" | tail -n1) + local response_body=$(echo "$response" | sed '$d') + local end_req_time=$(date +%s%3N) + local duration=$((end_req_time - start_req_time)) + + if [ "$http_status" -eq 200 ]; then + if echo "$response_body" | jq . > /dev/null 2>&1; then + AUTH_TOKEN=$(echo "$response_body" | jq -r '.data.token.access_token // empty') + if [[ -n "$AUTH_TOKEN" && "$AUTH_TOKEN" != "null" ]]; then + write_test_log "SUCCESS" "✓ User Authentication - Sukses (Status: $http_status, Waktu: ${duration}ms)" + write_test_log "SUCCESS" "Autentikasi berhasil" + ((PASS_COUNT++)) + else + write_test_log "ERROR" "Autentikasi gagal - token tidak ditemukan dalam response" + AUTH_TOKEN="" + ((FAIL_COUNT++)) + fi + else + write_test_log "ERROR" "Autentikasi gagal - response bukan JSON valid" + AUTH_TOKEN="" + ((FAIL_COUNT++)) + fi + else + write_test_log "ERROR" "✗ User Authentication - Gagal (Status: $http_status, Waktu: ${duration}ms)" + AUTH_TOKEN="" + ((FAIL_COUNT++)) + fi + + local status="PASS" + local error_msg="" + if [ "$http_status" -ne 200 ] || [[ -z "$AUTH_TOKEN" ]]; then + status="FAIL" + error_msg="Authentication failed" + fi + + result_json=$(jq -n --arg name "User Authentication" --arg ep "/v1/auth/login" --arg meth "POST" \ + --arg stat "$status" --arg code "$http_status" --arg dur "$duration" \ + --arg err "$error_msg" \ + '{TestName: $name, Endpoint: $ep, Method: $meth, Status: $stat, StatusCode: $code, ResponseTimeMs: $dur, Error: $err}') + TEST_RESULTS+=("$result_json") +} + +test_all_users_login_performance() { + printf "\n${CYAN}=== Menguji Login Performance Semua User ===${NC}\n" + + local total_login_time=0 + local successful_logins=0 + local failed_logins=0 + + local email="admin@example.com" + local fullname="${ALL_USERS[$email]}" + write_test_log "INFO" "Testing login for: $fullname ($email)" + + local login_data + login_data=$(jq -n --arg email "$email" --arg pass "$TEST_PASSWORD" '{email: $email, password: $pass}') + + local start_time=$(date +%s%3N) + + response=$(curl -s -w "\n%{http_code}" -X "POST" \ + -H "Content-Type: application/json" \ + -d "$login_data" \ + "$BASE_URL/v1/auth/login") + + local http_status=$(echo "$response" | tail -n1) + local response_body=$(echo "$response" | sed '$d') + local end_time=$(date +%s%3N) + local duration=$((end_time - start_time)) + + total_login_time=$((total_login_time + duration)) + + if [ "$http_status" -eq 200 ]; then + if echo "$response_body" | jq -e '.data.token.access_token' > /dev/null 2>&1; then + ((successful_logins++)) + ((PASS_COUNT++)) + write_test_log "SUCCESS" "✓ Login $fullname - ${duration}ms" + + result_json=$(jq -n --arg name "Login Performance - $fullname" --arg ep "/v1/auth/login" --arg meth "POST" \ + --arg stat "PASS" --arg code "$http_status" --arg dur "$duration" \ + --arg err "" \ + '{TestName: $name, Endpoint: $ep, Method: $meth, Status: $stat, StatusCode: $code, ResponseTimeMs: $dur, Error: $err}') + TEST_RESULTS+=("$result_json") + else + ((failed_logins++)) + ((FAIL_COUNT++)) + write_test_log "ERROR" "✗ Login $fullname - No token (${duration}ms)" + FAILED_TESTS_SUMMARY+=("✗ Login $fullname - No token in response") + fi + else + ((failed_logins++)) + ((FAIL_COUNT++)) + write_test_log "ERROR" "✗ Login $fullname - HTTP $http_status (${duration}ms)" + FAILED_TESTS_SUMMARY+=("✗ Login $fullname - HTTP $http_status") + fi + + local total_users=1 + local avg_login_time=0 + if [ "$total_users" -gt 0 ]; then + avg_login_time=$((total_login_time / total_users)) + fi + + printf "\n${BLUE}=== Login Performance Summary ===${NC}\n" + printf "Total Users Tested: %d\n" "$total_users" + printf "${GREEN}Successful Logins: %d${NC}\n" "$successful_logins" + printf "${RED}Failed Logins: %d${NC}\n" "$failed_logins" + printf "${BLUE}Average Login Time: %dms${NC}\n" "$avg_login_time" + printf "${BLUE}Total Login Time: %dms${NC}\n" "$total_login_time" + + if [ "$avg_login_time" -lt 2000 ]; then + printf "${GREEN}✅ Performance Status: EXCELLENT (< 2s average)${NC}\n" + elif [ "$avg_login_time" -lt 5000 ]; then + printf "${YELLOW}⚠️ Performance Status: GOOD (2-5s average)${NC}\n" + else + printf "${RED}❌ Performance Status: POOR (> 5s average)${NC}\n" + fi + printf "\n" +} + +test_with_user() { + local email=$1 + local fullname=$2 + local test_name=$3 + + write_test_log "INFO" "Testing $test_name dengan user: $fullname ($email)" + + local login_data + login_data=$(jq -n --arg email "$email" --arg pass "$TEST_PASSWORD" '{email: $email, password: $pass}') + + local start_time=$(date +%s%3N) + + response=$(curl -s -w "\n%{http_code}" -X "POST" \ + -H "Content-Type: application/json" \ + -d "$login_data" \ + "$BASE_URL/v1/auth/login") + + local http_status=$(echo "$response" | tail -n1) + local response_body=$(echo "$response" | sed '$d') + local end_time=$(date +%s%3N) + local duration=$((end_time - start_time)) + + if [ "$http_status" -eq 200 ]; then + if echo "$response_body" | jq -e '.data.token.access_token' > /dev/null 2>&1; then + local user_auth_token=$(echo "$response_body" | jq -r '.data.token.access_token') + write_test_log "SUCCESS" "✓ Login $fullname berhasil - ${duration}ms" + + local me_response + me_response=$(curl -s -w "\n%{http_code}" -X "GET" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $user_auth_token" \ + "$BASE_URL/v1/users/me") + + local me_status=$(echo "$me_response" | tail -n1) + local me_body=$(echo "$me_response" | sed '$d') + + if [ "$me_status" -eq 200 ]; then + ((PASS_COUNT++)) + write_test_log "SUCCESS" "✓ Get profile $fullname berhasil" + + local user_email=$(echo "$me_body" | jq -r '.data.email // empty') + local user_name=$(echo "$me_body" | jq -r '.data.fullname // empty') + + if [ "$user_email" = "$email" ]; then + write_test_log "SUCCESS" "✓ User data verified: $user_name ($user_email)" + else + write_test_log "WARN" "⚠ User data mismatch: expected $email, got $user_email" + fi + else + ((FAIL_COUNT++)) + write_test_log "ERROR" "✗ Get profile $fullname gagal - HTTP $me_status" + FAILED_TESTS_SUMMARY+=("✗ Get profile $fullname - HTTP $me_status") + fi + + else + ((FAIL_COUNT++)) + write_test_log "ERROR" "✗ Login $fullname - No token (${duration}ms)" + FAILED_TESTS_SUMMARY+=("✗ Login $fullname - No token in response") + fi + else + write_test_log "ERROR" "✗ Login $fullname gagal - HTTP $http_status (${duration}ms)" + FAILED_TESTS_SUMMARY+=("✗ Login $fullname - HTTP $http_status") + fi +} + +test_all_users_individually() { + printf "\n${CYAN}=== Menguji Semua User Secara Individual ===${NC}\n" + + for email in "${!ALL_USERS[@]}"; do + local fullname="${ALL_USERS[$email]}" + test_with_user "$email" "$fullname" "Individual User Test" + echo "" + done +} + +test_comprehensive_with_user() { + printf "\n${CYAN}=== Comprehensive Test untuk $fullname ($email) ===${NC}\n" + + local login_data + login_data=$(jq -n --arg email "$email" --arg pass "$TEST_PASSWORD" '{email: $email, password: $pass}') + + local start_time=$(date +%s%3N) + + response=$(curl -s -w "\n%{http_code}" -X "POST" \ + -H "Content-Type: application/json" \ + -d "$login_data" \ + "$BASE_URL/v1/auth/login") + + local http_status=$(echo "$response" | tail -n1) + local response_body=$(echo "$response" | sed '$d') + local end_time=$(date +%s%3N) + local duration=$((end_time - start_time)) + + if [ "$http_status" -eq 200 ]; then + if echo "$response_body" | jq -e '.data.token.access_token' > /dev/null 2>&1; then + user_token=$(echo "$response_body" | jq -r '.data.token.access_token') + write_test_log "SUCCESS" "✓ Login $fullname berhasil - ${duration}ms" + + local original_auth_token="$AUTH_TOKEN" + + AUTH_TOKEN="$user_token" + + printf "\n${BLUE}--- Testing dengan $fullname (Expected results berdasarkan role) ---${NC}\n" + + test_api_endpoint "Get Current User Profile - $fullname" "GET" "/v1/users/me" 200 "" true + + case "$email" in + "admin@example.com") + test_api_endpoint "Get Users List - $fullname" "GET" "/v1/users" 200 "" true + test_api_endpoint "Get Roles List - $fullname" "GET" "/v1/roles" 200 "" true + test_api_endpoint "Get Permissions List - $fullname" "GET" "/v1/permissions" 200 "" true + test_api_endpoint "Get Mentors List - $fullname" "GET" "/v1/mentors" 200 "" true + test_api_endpoint "Get Mentor Me - $fullname" "GET" "/v1/mentors/me" 403 "" true # Admin is not a mentor + test_api_endpoint "Get Mentor Status - $fullname" "GET" "/v1/mentors/status" 403 "" true # Admin is not a mentor + test_api_endpoint "Get Gacha Items - $fullname" "GET" "/v1/gacha/items" 200 "" true + test_api_endpoint "Execute Gacha Roll - $fullname" "POST" "/v1/gacha/rolls/execute" 200 "" true + + local testimonial_data + testimonial_data=$(jq -n --arg content "Test testimonial by $fullname $(date +%s)" '{role: "Student", content: $content}') + test_api_endpoint "Create Testimonial - $fullname" "POST" "/v1/cms/landing/testimonials/create" 201 "$testimonial_data" true + ;; + + "staff@example.com") + test_api_endpoint "Get Users List - $fullname" "GET" "/v1/users" 200 "" true + test_api_endpoint "Get Roles List - $fullname" "GET" "/v1/roles" 200 "" true + test_api_endpoint "Get Permissions List - $fullname" "GET" "/v1/permissions" 200 "" true + test_api_endpoint "Get Mentors List - $fullname" "GET" "/v1/mentors" 200 "" true + test_api_endpoint "Get Gacha Items - $fullname" "GET" "/v1/gacha/items" 200 "" true + test_api_endpoint "Execute Gacha Roll - $fullname" "POST" "/v1/gacha/rolls/execute" 200 "" true + + local testimonial_data + testimonial_data=$(jq -n --arg content "Test testimonial by $fullname $(date +%s)" '{role: "Student", content: $content}') + test_api_endpoint "Create Testimonial - $fullname" "POST" "/v1/cms/landing/testimonials/create" 201 "$testimonial_data" true + ;; + + "mentor@example.com") + test_api_endpoint "Get Users List - $fullname" "GET" "/v1/users" 403 "" true + test_api_endpoint "Get Roles List - $fullname" "GET" "/v1/roles" 403 "" true + test_api_endpoint "Get Permissions List - $fullname" "GET" "/v1/permissions" 403 "" true + test_api_endpoint "Get Mentors List - $fullname" "GET" "/v1/mentors" 200 "" true + test_api_endpoint "Get Mentor Me - $fullname" "GET" "/v1/mentors/me" 200 "" true + test_api_endpoint "Get Mentor Status - $fullname" "GET" "/v1/mentors/status" 200 "" true + test_api_endpoint "Get Gacha Items - $fullname" "GET" "/v1/gacha/items" 200 "" true + test_api_endpoint "Execute Gacha Roll - $fullname" "POST" "/v1/gacha/rolls/execute" 200 "" true + + local testimonial_data + testimonial_data=$(jq -n --arg content "Test testimonial by $fullname $(date +%s)" '{role: "Student", content: $content}') + test_api_endpoint "Create Testimonial - $fullname" "POST" "/v1/cms/landing/testimonials/create" 201 "$testimonial_data" true + ;; + + "user@example.com") + test_api_endpoint "Get Users List - $fullname" "GET" "/v1/users" 403 "" true + test_api_endpoint "Get Roles List - $fullname" "GET" "/v1/roles" 403 "" true + test_api_endpoint "Get Permissions List - $fullname" "GET" "/v1/permissions" 403 "" true + test_api_endpoint "Get Mentors List - $fullname" "GET" "/v1/mentors" 200 "" true + test_api_endpoint "Get Mentor Me - $fullname" "GET" "/v1/mentors/me" 403 "" true # User is not a mentor + test_api_endpoint "Get Mentor Status - $fullname" "GET" "/v1/mentors/status" 403 "" true # User is not a mentor + test_api_endpoint "Get Gacha Items - $fullname" "GET" "/v1/gacha/items" 200 "" true + test_api_endpoint "Execute Gacha Roll - $fullname" "POST" "/v1/gacha/rolls/execute" 200 "" true + + local testimonial_data + testimonial_data=$(jq -n --arg content "Test testimonial by $fullname $(date +%s)" '{role: "Student", content: $content}') + test_api_endpoint "Create Testimonial - $fullname" "POST" "/v1/cms/landing/testimonials/create" 201 "$testimonial_data" true + ;; + esac + + test_api_endpoint "Events with Advanced Filter - $fullname" "GET" "/v1/cms/landing/events?filter=online&filter_by=is_online" 200 "" false + test_api_endpoint "Testimonials with Search - $fullname" "GET" "/v1/cms/landing/testimonials?search=test" 200 "" false + + case "$email" in + "admin@example.com"|"staff@example.com") + test_api_endpoint "Users with Sort - $fullname" "GET" "/v1/users?sort_by=created_at&order=DESC" 200 "" true + ;; + *) + test_api_endpoint "Users with Sort - $fullname" "GET" "/v1/users?sort_by=created_at&order=DESC" 403 "" true + ;; + esac + + AUTH_TOKEN="$original_auth_token" + + write_test_log "SUCCESS" "✓ Comprehensive test untuk $fullname selesai" + + else + write_test_log "ERROR" "✗ Login $fullname gagal - No token (${duration}ms)" + fi + else + write_test_log "ERROR" "✗ Login $fullname gagal - HTTP $http_status (${duration}ms)" + fi +} + +test_all_endpoints_with_all_users() { + printf "\n${CYAN}=== Menjalankan Semua Test dengan Semua User ===${NC}\n" + + for email in "${!ALL_USERS[@]}"; do + local fullname="${ALL_USERS[$email]}" + test_comprehensive_with_user "$email" "$fullname" + printf "\n${BLUE}--- Selesai testing dengan $fullname ---${NC}\n\n" + done +} + +test_public_endpoints() { + printf "\n${CYAN}=== Menguji Public Endpoints ===${NC}\n" + test_api_endpoint "Get Events List" "GET" "/v1/cms/landing/events" 200 + test_api_endpoint "Get Testimonials List" "GET" "/v1/cms/landing/testimonials" 200 +} + +test_authentication_endpoints() { + printf "\n${CYAN}=== Menguji Authentication Endpoints ===${NC}\n" + get_auth_token + + local invalid_login + invalid_login=$(jq -n '{email: "invalid@example.com", password: "wrongpassword"}') + test_api_endpoint "Invalid Login Test" "POST" "/v1/auth/login" 401 "$invalid_login" + + # User registration and verification tests currently rely on external email service or OTP logic + # that is not easily testable in a simple curl script without actual email sending/receiving. + # Skipping these tests for now. + # local register_email="test_user_$(date +%s%N)@example.com" + # local register_data=$(jq -n --arg email "$register_email" --arg pass "$TEST_PASSWORD" --arg fullname "Test Register" --arg phone "081234567899" '{email: $email, password: $pass, fullname: $fullname, phone_number: $phone}') + # test_api_endpoint "User Registration Test" "POST" "/v1/auth/register" 200 "$register_data" + # local verify_otp_data=$(jq -n --arg email "$register_email" --arg otp "123456" '{email: $email, otp: ($otp | tonumber)}') + # test_api_endpoint "Verify Email Test (Invalid OTP)" "POST" "/v1/auth/verify-email" 400 "$verify_otp_data" + + local forgot_password_data + forgot_password_data=$(jq -n --arg email "$TEST_EMAIL" '{email: $email}') + test_api_endpoint "Forgot Password Test" "POST" "/v1/auth/forgot" 400 "$forgot_password_data" + + local new_password_data + new_password_data=$(jq -n --arg token "some_reset_token" --arg pass "newpassword123!A" '{token: $token, password: $pass}') + test_api_endpoint "New Password Test (Invalid Token)" "POST" "/v1/auth/new-password" 400 "$new_password_data" + + local refresh_token=$(curl -s -X POST -H "Content-Type: application/json" -d "$(jq -n --arg email "$TEST_EMAIL" --arg pass "$TEST_PASSWORD" '{email: $email, password: $pass}')" "$BASE_URL/v1/auth/login" | jq -r '.data.token.refresh_token // empty') + if [ -n "$refresh_token" ]; then + local refresh_data + refresh_data=$(jq -n --arg token "$refresh_token" '{refresh_token: $token}') + test_api_endpoint "Refresh Token Test" "POST" "/v1/auth/refresh" 200 "$refresh_data" + else + write_test_log "WARN" "✗ Refresh Token Test - Dilewati: Refresh token tidak tersedia dari login" + fi +} + +test_error_handling() { + printf "\n${CYAN}=== Menguji Error Handling ===${NC}\n" + test_api_endpoint "Non-existent Endpoint" "GET" "/v1/nonexistent" 404 + test_api_endpoint "Unauthorized Access" "GET" "/v1/users" 401 "" false +} + +test_user_management_endpoints() { + printf "\n${CYAN}=== Menguji User Management Endpoints ===${NC}\n" + test_api_endpoint "Get Users List" "GET" "/v1/users" 200 "" true + + local test_user_id="c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2" + test_api_endpoint "Get User By ID" "GET" "/v1/users/detail/$test_user_id" 200 "" true + + local new_user_email="new_test_user_$(date +%s%N)@example.com" + local new_user_fullname="New Test User $(date +%s%N)" + local new_user_phone="089876543211" + local new_user_password="NewPassword123!" + local new_user_role_id="5713cb37-dc02-4e87-8048-d7a41d352059" # User role ID from seed_users.rs + + local create_user_data=$(jq -n \ + --arg email "$new_user_email" \ + --arg pass "$new_user_password" \ + --arg fullname "$new_user_fullname" \ + --arg phone "$new_user_phone" \ + --arg is_active true \ + --arg role_id "$new_user_role_id" \ + '{email: $email, password: $pass, fullname: $fullname, phone_number: $phone, is_active: $is_active | fromjson, role_id: $role_id}') + test_api_endpoint "Create New User" "POST" "/v1/users/create" 201 "$create_user_data" true + + # Assuming the created user can be fetched by email for update/delete + local created_user_id=$(curl -s -X GET -H "Authorization: Bearer $AUTH_TOKEN" "$BASE_URL/v1/users?search=$new_user_email" | jq -r '.data[0].id // empty') + + if [ -n "$created_user_id" ]; then + local updated_user_fullname="Updated Test User $(date +%s%N)" + local updated_user_data=$(jq -n \ + --arg email "$new_user_email" \ + --arg pass "$new_user_password" \ + --arg fullname "$updated_user_fullname" \ + --arg phone "$new_user_phone" \ + --arg is_active true \ + --arg gender "Male" \ + --arg birthdate "1990-01-01" \ + --arg avatar "https://example.com/avatar.jpg" \ + --arg role_id "$new_user_role_id" \ + '{email: $email, password: $pass, fullname: $fullname, phone_number: $phone, is_active: $is_active | fromjson, gender: $gender, birthdate: $birthdate, avatar: $avatar, role_id: $role_id}') + test_api_endpoint "Update User" "PUT" "/v1/users/update/$created_user_id" 200 "$updated_user_data" true + + local set_active_data=$(jq -n --arg is_active false '{is_active: $is_active | fromjson}') + test_api_endpoint "Deactivate User" "PUT" "/v1/users/activate/$created_user_id" 200 "$set_active_data" true + + local set_active_data=$(jq -n --arg is_active true '{is_active: $is_active | fromjson}') + test_api_endpoint "Reactivate User" "PUT" "/v1/users/activate/$created_user_id" 200 "$set_active_data" true + + test_api_endpoint "Delete User" "DELETE" "/v1/users/delete/$created_user_id" 200 "" true + else + write_test_log "WARN" "✗ Skipping User Update/Delete tests: Failed to retrieve ID of newly created user." + fi +} + +test_crud_operations() { + printf "\n${CYAN}=== Menguji CRUD Operations ===${NC}\n" + + local testimonial_data + testimonial_data=$(jq -n --arg content "Test testimonial via Bash $(date +%s)" '{role: "Student", content: $content}') + local testimonial_response=$(test_api_endpoint "Create Testimonial" "POST" "/v1/cms/landing/testimonials/create" 201 "$testimonial_data" true) + TEST_TESTIMONIAL_ID=$(echo "$testimonial_response" | jq -r '.data.id // empty') + write_test_log "INFO" "TEST_TESTIMONIAL_ID: $TEST_TESTIMONIAL_ID" + write_test_log "INFO" "Captured Testimonial ID: $TEST_TESTIMONIAL_ID" + sleep 0.2 + + local permission_data + permission_data=$(jq -n --arg name "Test Permission $(date +%s)" '{name: $name}') + test_api_endpoint "Create Permission" "POST" "/v1/permissions/create" 201 "$permission_data" true + + local gacha_item_data + gacha_item_data=$(jq -n --arg name "Test Item $(date +%s)" '{name: $name, image_url: "https://example.com/id.jpg"}') + test_api_endpoint "Create Gacha Item" "POST" "/v1/gacha/items/create" 201 "$gacha_item_data" true + + local event_data + event_data=$(jq -n --arg name "Test Event $(date +%s)" '{ + name: $name, + description: "Test event description", + detail_link: "https://example.com/event", + price: 50.0, + is_online: true, + start_date: "2025-12-01T10:00:00Z", + end_date: "2025-12-01T16:00:00Z", + location: null + }') + test_api_endpoint "Create Event" "POST" "/v1/cms/landing/events/create" 201 "$event_data" true +} + +test_roles_and_permissions() { + printf "\n${CYAN}=== Menguji Roles & Permissions Endpoints ===${NC}\n" + test_api_endpoint "Get Roles List" "GET" "/v1/roles" 200 "" true + test_api_endpoint "Get Permissions List" "GET" "/v1/permissions" 200 "" true + + local test_role_id="3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a" + test_api_endpoint "Get Role By ID" "GET" "/v1/roles/detail/$test_role_id" 200 "" true +} + +test_mentor_endpoints() { + printf "\n${CYAN}=== Menguji Mentor Endpoints ===${NC}\n" + test_api_endpoint "Get Mentors List" "GET" "/v1/mentors" 200 "" true + # These tests are run with AUTH_TOKEN set to admin. Since admin is not a mentor, these should be 403. + test_api_endpoint "Get Mentor Me" "GET" "/v1/mentors/me" 403 "" true + test_api_endpoint "Get Mentor Status" "GET" "/v1/mentors/status" 403 "" true + + local test_mentor_id="e6f78d23-83bf-5c2b-bcd4-001345678901" + test_api_endpoint "Get Mentor By ID" "GET" "/v1/mentors/detail/$test_mentor_id" 200 "" true + + local mentor_register_data + mentor_register_data=$(jq -n --arg email "test.mentor.$(date +%s%N)@example.com" '{ + identity_and_verification: { + legal_name: "Test Mentor Legal Name", + identity_document_url: "https://example.com/id.jpg", + phone_for_verification: "+1234567890" + }, + professional_profile: { + bio: "Test mentor bio", + linkedin_url: "https://linkedin.com/in/testmentor", + industries: ["Technology", "Software"], + expertise: ["JavaScript", "Python"], + languages: ["English", "Indonesian"], + current_company: "Test Company", + current_role: "Senior Developer", + years_of_experience: 5 + }, + mentoring_logistics: { + topics_of_interest: ["Career Development", "Technical Skills"], + preferred_mentee_level: ["Junior", "Mid-level"], + preferred_mentoring_formats: ["1-on-1", "Group"], + availability_commitment: "2-3 hours per week", + mentoring_rate: { + amount: 100000, + currency: "IDR", + per_duration: "hour" + } + }, + email: $email + }') + test_api_endpoint "Register as Mentor" "POST" "/v1/mentors/register" 422 "$mentor_register_data" true +} + +test_events_endpoints() { + printf "\n${CYAN}=== Menguji Events Endpoints ===${NC}\n" + test_api_endpoint "Get Events with Pagination" "GET" "/v1/cms/landing/events?page=1&per_page=5" 200 + test_api_endpoint "Get Events with Search" "GET" "/v1/cms/landing/events?search=tech" 200 + + local test_event_id="e1a2b3c4-5d6e-7f8g-9h0i-1j2k3l4m5n6o" + test_api_endpoint "Get Event By ID" "GET" "/v1/cms/landing/events/detail/$test_event_id" 200 +} + +test_testimonials_endpoints() { + printf "\n${CYAN}=== Menguji Testimonials Endpoints ===${NC}\n" + test_api_endpoint "Get Testimonials with Pagination" "GET" "/v1/cms/landing/testimonials?page=1&per_page=5" 200 + + # Ensure TEST_TESTIMONIAL_ID is not empty before testing + if [ -n "$TEST_TESTIMONIAL_ID" ]; then + test_api_endpoint "Get Testimonial By ID" "GET" "/v1/cms/landing/testimonials/detail/$TEST_TESTIMONIAL_ID" 200 + else + write_test_log "WARN" "✗ Get Testimonial By ID - Dilewati: TEST_TESTIMONIAL_ID tidak tersedia" + fi +} + +test_gacha_endpoints() { + printf "\n${CYAN}=== Menguji Gacha Endpoints ===${NC}\n" + test_api_endpoint "Get Gacha Items" "GET" "/v1/gacha/items" 200 "" true + test_api_endpoint "Execute Gacha Roll" "POST" "/v1/gacha/rolls/execute" 200 "" true +} + +test_advanced_scenarios() { + printf "\n${CYAN}=== Menguji Advanced Scenarios ===${NC}\n" + + test_api_endpoint "Events with Advanced Filter" "GET" "/v1/cms/landing/events?filter=online&filter_by=is_online" 200 + test_api_endpoint "Users with Sort" "GET" "/v1/users?sort_by=created_at&order=DESC" 200 "" true + test_api_endpoint "Testimonials with Search" "GET" "/v1/cms/landing/testimonials?search=test" 200 + + local mentor_register_data + mentor_register_data=$(jq -n --arg email "test.mentor.$(date +%s%N)@example.com" '{ + identity_and_verification: { + legal_name: "Test Mentor Legal Name", + identity_document_url: "https://example.com/id.jpg", + phone_for_verification: "+1234567890" + }, + professional_profile: { + bio: "Test mentor bio", + linkedin_url: "https://linkedin.com/in/testmentor", + industries: ["Technology", "Software"], + expertise: ["JavaScript", "Python"], + languages: ["English", "Indonesian"], + current_company: "Test Company", + current_role: "Senior Developer", + years_of_experience: 5 + }, + mentoring_logistics: { + topics_of_interest: ["Career Development", "Technical Skills"], + preferred_mentee_level: ["Junior", "Mid-level"], + preferred_mentoring_formats: ["1-on-1", "Group"], + availability_commitment: "2-3 hours per week", + mentoring_rate: { + amount: 100000, + currency: "IDR", + per_duration: "hour" + } + }, + email: $email + }') + test_api_endpoint "Register as Mentor" "POST" "/v1/mentors/register" 422 "$mentor_register_data" true + + test_api_endpoint "Invalid POST to GET endpoint" "POST" "/v1/cms/landing/events" 405 + test_api_endpoint "Invalid PUT with Invalid ID" "PUT" "/v1/users/update/some_invalid_id" 400 "" true +} + +show_test_summary() { + printf "\n${YELLOW}=== Test Coverage Summary ===${NC}\n" + printf "📋 Authentication: Login, Forgot Password, OTP\n" + printf "👥 Users: List, Details, Profile Management\n" + printf "🔐 Roles & Permissions: RBAC System Testing\n" + printf "👨‍🏫 Mentors: Registration, Profile, Status\n" + printf "📅 Events: CRUD Operations, Filtering\n" + printf "💬 Testimonials: Management & Creation\n" + printf "🎲 Gacha: Items, Rolls, Claims\n" + printf "🔧 Advanced: Pagination, Search, Edge Cases\n" + printf "❌ Error Handling: 401, 404, Invalid Requests\n" + printf "\n" +} + +printf "${CYAN}=== IMPHNEN API Comprehensive Test Suite ===${NC}\n" +printf "${YELLOW}Base URL: %s${NC}\n" "$BASE_URL" +show_test_summary + +if [ "$START_SERVER" = true ]; then + if ! command -v cargo &> /dev/null; then + write_test_log "ERROR" "Perintah 'cargo' tidak ditemukan. Tidak bisa memulai server." + exit 1 + fi + printf "${YELLOW}Memulai server backend...${NC}\n" + cargo run --bin api & + SERVER_PID=$! + + printf "${YELLOW}Menunggu server siap...${NC}\n" + retries=0 + max_retries=100 # Increased from 15 to 30 + until test_server_connection; do + ((retries++)) + if [ $retries -ge $max_retries ]; then + write_test_log "ERROR" "Gagal memulai server dalam timeout\. Cek output terminal untuk detail\." + exit 1 + fi + sleep 2 + done + write_test_log "SUCCESS" "Server berjalan!" +else + if ! test_server_connection; then + write_test_log "ERROR" "Server tidak berjalan di $BASE_URL" + write_test_log "WARN" "Silakan jalankan server secara manual atau gunakan flag -s" + exit 1 + fi + write_test_log "SUCCESS" "Server sudah berjalan di $BASE_URL" +fi + +clear_database + +printf "\n${CYAN}=== Menjalankan Seeders ===${NC}\n" +if ! cargo run --bin seeder; then + write_test_log "ERROR" "Gagal menjalankan seeder roles permissions." + exit 1 +fi +write_test_log "SUCCESS" "Seeders selesai." + + +printf "\n${CYAN}=== Menampilkan User yang Tersedia ===${NC}\n" +for email in "${!ALL_USERS[@]}"; do + fullname="${ALL_USERS[$email]}" + printf "${BLUE}• $fullname${NC} - ${email}\n" +done +printf "\n" + +test_public_endpoints + +test_all_users_login_performance + +test_all_users_individually + +if [ "$SKIP_BASIC" = false ]; then + test_authentication_endpoints + test_error_handling +fi + +if [[ "$SKIP_CRUD" = false && -n "$AUTH_TOKEN" ]]; then + test_crud_operations # This will now set TEST_TESTIMONIAL_ID +fi + +if [ "$SKIP_COMPREHENSIVE" = false ]; then + test_all_endpoints_with_all_users +fi + +if [[ "$SKIP_COMPREHENSIVE" = false && -n "$AUTH_TOKEN" ]]; then + printf "\n${CYAN}=== Test Comprehensive dengan Admin Token ===${NC}\n" + test_user_management_endpoints + test_roles_and_permissions + test_mentor_endpoints + test_events_endpoints + test_testimonials_endpoints # This will now use TEST_TESTIMONIAL_ID + test_gacha_endpoints +fi + +test_advanced_scenarios + +TEST_END_TIME=$(date +%s) +TOTAL_DURATION=$((TEST_END_TIME - TEST_START_TIME)) +TOTAL_TESTS=$((PASS_COUNT + FAIL_COUNT)) +SUCCESS_RATE="0" +if [ "$TOTAL_TESTS" -gt 0 ]; then + SUCCESS_RATE=$(( (PASS_COUNT * 100) / TOTAL_TESTS )) +fi + +if [ "$GENERATE_REPORT" = true ]; then + printf "\n${CYAN}=== Membuat Laporan Tes ===${NC}\n" + + all_results_json=$(printf "%s," "${TEST_RESULTS[@]}") + all_results_json="[${all_results_json%,}]" + + report_file="api-test-report-$(date +'%Y%m%d-%H%M%S').json" + + jq -n --arg start "$(date -d @$TEST_START_TIME +'%Y-%m-%d %H:%M:%S')" \ + --arg end "$(date -d @$TEST_END_TIME +'%Y-%m-%d %H:%M:%S')" \ + --arg dur "$TOTAL_DURATION" \ + --arg url "$BASE_URL" \ + --arg total "$TOTAL_TESTS" \ + --arg pass "$PASS_COUNT" \ + --arg fail "$FAIL_COUNT" \ + --arg rate "${SUCCESS_RATE}%" \ + --argjson results "$all_results_json" \ + '{ + TestRun: {StartTime: $start, EndTime: $end, DurationSec: $dur, BaseUrl: $url}, + Summary: {TotalTests: $total, PassedTests: $pass, FailedTests: $fail, SuccessRate: $rate}, + Results: $results + }' > "$report_file" + + printf "${BLUE}Laporan tes detail disimpan di: %s${NC}\n" "$report_file" +fi + +printf "\n${CYAN}=== Ringkasan Test Suite ===${NC}\n" +printf "Total Durasi: %s detik\n" "$TOTAL_DURATION" +printf "Total Tes : %s\n" "$TOTAL_TESTS" +printf "${GREEN}Lolos : %s${NC}\n" "$PASS_COUNT" +printf "${RED}Gagal : %s${NC}\n" "$FAIL_COUNT" +printf "Tingkat Sukses: %s%%\n" "$SUCCESS_RATE" + +if [ "$FAIL_COUNT" -gt 0 ]; then + printf "\n${RED}Tes yang Gagal:${NC}\n" + for summary in "${FAILED_TESTS_SUMMARY[@]}"; do + printf " %s\n" "$summary" + done +fi + +if [ "$FAIL_COUNT" -eq 0 ]; then + printf "\n${GREEN}Test suite selesai dengan sukses.${NC}\n" + exit 0 +else + printf "\n${RED}Test suite selesai dengan beberapa kegagalan.${NC}\n" + exit 1 +fi \ No newline at end of file diff --git a/tests/Cargo.toml b/tests/Cargo.toml index f858262..24d892d 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,16 +1,29 @@ [package] name = "tests" version = "0.1.0" -edition = "2024" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" } -imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } -imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } -imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } -axum.workspace = true -axum-test.workspace = true -chrono.workspace = true -surrealdb.workspace = true -tokio.workspace = true +tokio.workspace = true +imphnen-iam.workspace = true +imphnen-dimentorin.workspace = true +imphnen-entities.workspace = true +imphnen-libs.workspace = true +imphnen-utils.workspace = true +http-body-util.workspace = true +hyper.workspace = true +hyper-util.workspace = true serde_json.workspace = true +dotenvy.workspace = true +tower.workspace = true +tracing.workspace = true +surrealdb.workspace = true +serde.workspace = true +strum.workspace = true +uuid.workspace = true +rand.workspace = true +async-trait.workspace = true +chrono.workspace = true +axum.workspace = true \ No newline at end of file diff --git a/tests/src/dimentorin/mentors/mentor_registration_tests.rs b/tests/src/dimentorin/mentors/mentor_registration_tests.rs new file mode 100644 index 0000000..e8f8355 --- /dev/null +++ b/tests/src/dimentorin/mentors/mentor_registration_tests.rs @@ -0,0 +1,758 @@ +use axum::{ + body::Body, + http::{Method, Request, StatusCode}, + response::Response, + routing::post, + Router, +}; +use http_body_util::BodyExt; // for `buffer` method +use imphnen_dimentorin::{ + mentors_controller, + mentors_dto::{MentorUserRegisterRequestDto, MentorRegisterResponseDto}, +}; +use imphnen_entities::{AppState, SurrealMemClient, SurrealWsClient}; +use imphnen_iam::{RolesRepository, UsersRepository}; +use imphnen_iam::{RolesEnum, UsersSchema, RolesSchema}; +use imphnen_utils::{hash_password, make_thing, get_iso_date}; +use surrealdb::{Uuid, sql::Thing}; +use imphnen_libs::{ResourceEnum, surrealdb_init_ws, surrealdb_init_mem, Env}; +use dotenvy::dotenv; +use tower::ServiceExt; // Added ServiceExt for .oneshot() +use crate::mock_test::setup_all_test_environment; // Import the new setup function + +// Helper function to create a test AppState +async fn setup_app_state() -> AppState { + dotenv().ok(); + // Use the surrealdb initialization functions from imphnen_libs + let surrealdb_ws = surrealdb_init_ws().await.expect("Failed to initialize SurrealDB WS client"); + let surrealdb_mem = surrealdb_init_mem().await.expect("Failed to initialize SurrealDB MEM client"); + + let app_state = AppState { + surrealdb_ws, + surrealdb_mem, + }; + + // Manually seed roles since seed_roles is not directly callable as a repository method + let db = &app_state.surrealdb_ws; + let roles_to_seed = vec![ + (RolesEnum::Admin.to_string(), Vec::new()), + (RolesEnum::User.to_string(), Vec::new()), + (RolesEnum::Staff.to_string(), Vec::new()), + (RolesEnum::Mentor.to_string(), Vec::new()), + ]; + + for (name, permissions) in roles_to_seed { + // Check if role already exists to avoid errors on rerun + let existing_role: Option = db.query(format!("SELECT * FROM ONLY role WHERE name = '{}'", name)).await.unwrap().take(0).unwrap_or(None); + if existing_role.is_none() { + let role_id = Uuid::new_v4().to_string(); + let role = RolesSchema { + id: make_thing(&ResourceEnum::Roles.to_string(), &role_id), + name: name.clone(), + is_deleted: false, + permissions, + created_at: Some(get_iso_date()), + updated_at: Some(get_iso_date()), + }; + db.create::((ResourceEnum::Roles.to_string().as_str(), role_id)).content(role).await.unwrap(); // Corrected syntax + } + } + + app_state +} + +// Helper function to create a test application (router) +fn app(app_state: AppState) -> Router { + Router::new() + .route("/v1/mentors/register", post(mentors_controller::post_register_mentor)) + .with_state(app_state) +} + +#[tokio::test] +async fn test_register_new_user_as_mentor_success() { + let app_state = setup_all_test_environment().await; // Use the new setup function + let app = app(app_state.clone()); + + let test_email = "newmentor@example.com"; + let test_password = "Password123!"; + let test_fullname = "New Mentor User"; + let test_phone = "1234567890"; + + // Clean up before test + let user_repo = UsersRepository::new(&app_state); + let _ = user_repo.query_delete_user(test_email.to_string()).await; + + let dto = MentorUserRegisterRequestDto { + email: test_email.to_string(), + password: test_password.to_string(), + fullname: test_fullname.to_string(), + phone_number: test_phone.to_string(), + identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification { + legal_name: "Legal Name".to_string(), + gender: Some("Laki-laki".to_string()), + domicile: Some("Jakarta Selatan".to_string()), + identity_document_url: "http://example.com/id.pdf".to_string(), + phone_for_verification: "0987654321".to_string(), + }, + professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile { + bio: "Experienced professional seeking to mentor others in software development.".to_string(), + last_education: Some("S1".to_string()), + linkedin_url: Some("http://linkedin.com/in/mentor".to_string()), + github_url: None, + cv_url: None, + portfolio_url: Some("http://example.com/portfolio".to_string()), + industries: vec!["Technology".to_string()], + expertise: vec!["Rust".to_string(), "Debugging".to_string()], + languages: vec!["English".to_string()], + current_company: "Acme Corp".to_string(), + current_role: "Senior Engineer".to_string(), + years_of_experience: 5, + }, + mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics { + topics_of_interest: vec!["Career Development".to_string()], + preferred_mentee_level: vec!["Beginner".to_string()], + preferred_mentoring_formats: vec!["Online".to_string()], + availability_commitment: "5 hours/week".to_string(), + mentoring_rate_amount: 100, + }, + }; + + let response = app + .oneshot( + Request::builder() + .method(Method::POST) + .uri("/v1/mentors/register") + .header("Content-Type", "application/json") + .body(Body::from(serde_json::to_string(&dto).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let mentor_register_response: MentorRegisterResponseDto = serde_json::from_slice(&body).unwrap(); + + assert!(!mentor_register_response.id.is_empty()); + assert!(!mentor_register_response.user_id.is_empty()); + assert_eq!(mentor_register_response.email, Some(test_email.to_string())); + assert_eq!(mentor_register_response.status, "pending".to_string()); // New mentors should be pending verification + + // Verify user created and has mentor role, and is inactive + let user = user_repo.query_user_by_email(test_email.to_string()).await.unwrap(); + assert_eq!(user.email, test_email); + assert_eq!(user.is_active, false); // Should be inactive awaiting OTP + let role_repo = RolesRepository::new(&app_state); // Use RolesRepository to get role + let mentor_role = role_repo.query_role_by_name(RolesEnum::Mentor.to_string()).await.unwrap(); + assert_eq!(user.role.to_raw(), mentor_role.id.to_raw()); // Compare raw IDs + + // Clean up after test + let _ = user_repo.query_delete_user(test_email.to_string()).await; +} + +#[tokio::test] +async fn test_register_existing_user_as_mentor_success() { + let app_state = setup_all_test_environment().await; // Use the new setup function + let app = app(app_state.clone()); + + let test_email = "existinguser_mentor@example.com"; + let test_password = "Password123!"; + let test_fullname = "Existing User Becoming Mentor"; + let test_phone = "1122334455"; + + // Clean up and create an existing user + let user_repo = UsersRepository::new(&app_state); + let _ = user_repo.query_delete_user(test_email.to_string()).await; + + // Manually create a user with 'User' role first + let role_repo = RolesRepository::new(&app_state); // Use RolesRepository to get role + let user_role_id_item = role_repo.query_role_by_name(RolesEnum::User.to_string()).await.unwrap(); // Get RolesDetailItemDto + let user_role_id_thing = make_thing(&ResourceEnum::Roles.to_string(), &user_role_id_item.id); // Convert to Thing + let hashed_password = hash_password(test_password).unwrap(); + user_repo.query_create_user(imphnen_iam::UsersSchema { + id: make_thing(&ResourceEnum::Users.to_string(), &Uuid::new_v4().to_string()), + email: test_email.to_string(), + password: hashed_password, + fullname: "Original User Name".to_string(), + phone_number: "0000000000".to_string(), + is_active: true, // Initially active + role: user_role_id_thing, + created_at: get_iso_date(), + updated_at: get_iso_date(), + ..Default::default() + }).await.unwrap(); + + let dto = MentorUserRegisterRequestDto { + email: test_email.to_string(), + password: test_password.to_string(), // Keep password same for simplicity in test + fullname: test_fullname.to_string(), + phone_number: test_phone.to_string(), + identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification { + legal_name: "Legal Name Updated".to_string(), + gender: Some("Perempuan".to_string()), + domicile: Some("Surabaya".to_string()), + identity_document_url: "http://example.com/id_updated.pdf".to_string(), + phone_for_verification: "0987654322".to_string(), + }, + professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile { + bio: "Existing user now a mentor.".to_string(), + last_education: Some("S2".to_string()), + linkedin_url: Some("http://linkedin.com/in/mentor_existing".to_string()), + github_url: None, + cv_url: None, + portfolio_url: Some("http://example.com/portfolio_existing".to_string()), + industries: vec!["Education".to_string()], + expertise: vec!["Marketing".to_string()], + languages: vec!["Indonesian".to_string()], + current_company: "New Company".to_string(), + current_role: "Manager".to_string(), + years_of_experience: 10, + }, + mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics { + topics_of_interest: vec!["Business Strategy".to_string()], + preferred_mentee_level: vec!["Experienced".to_string()], + preferred_mentoring_formats: vec!["Offline".to_string()], + availability_commitment: "10 hours/month".to_string(), + mentoring_rate_amount: 200, + }, + }; + + let response = app + .oneshot( + Request::builder() + .method(Method::POST) + .uri("/v1/mentors/register") + .header("Content-Type", "application/json") + .body(Body::from(serde_json::to_string(&dto).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let mentor_register_response: MentorRegisterResponseDto = serde_json::from_slice(&body).unwrap(); + + assert!(!mentor_register_response.id.is_empty()); + assert!(!mentor_register_response.user_id.is_empty()); + assert_eq!(mentor_register_response.email, Some(test_email.to_string())); + assert_eq!(mentor_register_response.status, "pending".to_string()); // Should be pending verification + + // Verify user updated and has mentor role, and is inactive + let user = user_repo.query_user_by_email(test_email.to_string()).await.unwrap(); + assert_eq!(user.email, test_email); + assert_eq!(user.fullname, test_fullname); + assert_eq!(user.phone_number, test_phone); + assert_eq!(user.is_active, false); // Should be inactive awaiting OTP + let role_repo = RolesRepository::new(&app_state); // Use RolesRepository to get role + let mentor_role = role_repo.query_role_by_name(RolesEnum::Mentor.to_string()).await.unwrap(); + assert_eq!(user.role.to_raw(), mentor_role.id.to_raw()); // Compare raw IDs + + // Clean up after test + let _ = user_repo.query_delete_user(test_email.to_string()).await; +} + +#[tokio::test] +async fn test_register_mentor_already_has_profile() { + let app_state = setup_all_test_environment().await; // Use the new setup function + let app = app(app_state.clone()); + + let test_email = "existing_mentor_profile@example.com"; + let test_password = "Password123!"; + let test_fullname = "Existing Mentor Profile"; + let test_phone = "5551234567"; + + // Clean up and create a user that is already a mentor + let user_repo = UsersRepository::new(&app_state); + let mentor_repo = imphnen_dimentorin::mentors_repository::MentorsRepository::new(&app_state); + let _ = user_repo.query_delete_user(test_email.to_string()).await; + + // Manually create a user with 'Mentor' role + let role_repo = RolesRepository::new(&app_state); // Use RolesRepository to get role + let mentor_role_thing = role_repo.query_role_by_name(RolesEnum::Mentor.to_string()).await.unwrap().id; + let hashed_password = imphnen_utils::hash_password(test_password).unwrap(); + let user_id = imphnen_utils::make_thing(&ResourceEnum::Users.to_string(), &Uuid::new_v4().to_string()); + user_repo.query_create_user(imphnen_iam::UsersSchema { + id: user_id.clone(), + email: test_email.to_string(), + password: hashed_password, + fullname: test_fullname.to_string(), + phone_number: test_phone.to_string(), + is_active: true, + role: mentor_role_thing.clone(), + created_at: imphnen_utils::get_iso_date(), + updated_at: imphnen_utils::get_iso_date(), + // Removed mentor_user_id + }).await.unwrap(); + + // Manually create a mentor profile for this user + let existing_mentor_profile_id = imphnen_utils::make_thing(&ResourceEnum::Mentors.to_string(), &Uuid::new_v4().to_string()); + mentor_repo.query_create_mentor(imphnen_dimentorin::mentors_schema::MentorSchema { + id: existing_mentor_profile_id.clone(), + user_id: Some(user_id.clone()), + email: Some(test_email.to_string()), + legal_name: "Existing Mentor Legal Name".to_string(), + gender: Some("Laki-laki".to_string()), + domicile: Some("Bandung".to_string()), + identity_document_url: "http://example.com/existing_id.pdf".to_string(), + phone_for_verification: "1234567890".to_string(), + bio: "Already an existing mentor in the system.".to_string(), + last_education: Some("S3".to_string()), + linkedin_url: None, + github_url: None, + cv_url: None, + portfolio_url: Some("http://example.com/existing_portfolio".to_string()), + industries: vec!["Finance".to_string()], + expertise: vec!["Investments".to_string()], + languages: vec!["English".to_string()], + current_company: "Finance Corp".to_string(), + current_role: "Analyst".to_string(), + years_of_experience: 7, + topics_of_interest: vec!["Stocks".to_string()], + preferred_mentee_level: vec!["All".to_string()], + preferred_mentoring_formats: vec!["Any".to_string()], + availability_commitment: "Flexible".to_string(), + mentoring_rate: imphnen_dimentorin::mentors_dto::MentoringRate::default(), + status: "Approved".to_string(), + is_deleted: false, + created_at: imphnen_utils::get_iso_date(), + updated_at: imphnen_utils::get_iso_date(), + }).await.unwrap(); + + // Update user to link mentor profile + let user_after_mentor_creation_dto = user_repo.query_user_by_email(test_email.to_string()).await.unwrap(); + let mut user_after_mentor_creation_schema = UsersSchema::from(user_after_mentor_creation_dto); + user_after_mentor_creation_schema = user_after_mentor_creation_schema.update_mentor_id(Some(existing_mentor_profile_id.clone().to_raw())); + user_repo.query_update_user(user_after_mentor_creation_schema).await.unwrap(); + + let dto = MentorUserRegisterRequestDto { + email: test_email.to_string(), + password: test_password.to_string(), + fullname: test_fullname.to_string(), + phone_number: test_phone.to_string(), + identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification { + legal_name: "Legal Name".to_string(), + gender: Some("Perempuan".to_string()), + domicile: Some("Yogyakarta".to_string()), + identity_document_url: "http://example.com/id.pdf".to_string(), + phone_for_verification: "0987654321".to_string(), + }, + professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile { + bio: "Experienced professional seeking to mentor others in software development.".to_string(), + last_education: Some("SMA".to_string()), + linkedin_url: Some("http://linkedin.com/in/mentor".to_string()), + github_url: None, + cv_url: None, + portfolio_url: Some("http://example.com/portfolio_new".to_string()), + industries: vec!["Technology".to_string()], + expertise: vec!["Rust".to_string(), "Debugging".to_string()], + languages: vec!["English".to_string()], + current_company: "Acme Corp".to_string(), + current_role: "Senior Engineer".to_string(), + years_of_experience: 5, + }, + mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics { + topics_of_interest: vec!["Career Development".to_string()], + preferred_mentee_level: vec!["Beginner".to_string()], + preferred_mentoring_formats: vec!["Online".to_string()], + availability_commitment: "5 hours/week".to_string(), + mentoring_rate_amount: 100, + }, + }; + + let response = app + .oneshot( + Request::builder() + .method(Method::POST) + .uri("/v1/mentors/register") + .header("Content-Type", "application/json") + .body(Body::from(serde_json::to_string(&dto).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::CONFLICT); // Expecting conflict + + // Clean up after test + let _ = user_repo.query_delete_user(test_email.to_string()).await; + let _ = mentor_repo.query_delete_mentor(&existing_mentor_profile_id.to_raw()).await; +} + +#[tokio::test] +async fn test_register_mentor_invalid_email_format() { + let app_state = setup_all_test_environment().await; + let app = app(app_state.clone()); + + let test_email = "invalid-email"; // Invalid email format + let test_password = "Password123!"; + let test_fullname = "Test User"; + let test_phone = "1234567890"; + + let dto = MentorUserRegisterRequestDto { + email: test_email.to_string(), + password: test_password.to_string(), + fullname: test_fullname.to_string(), + phone_number: test_phone.to_string(), + identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification { + legal_name: "Legal Name".to_string(), + gender: Some("Laki-laki".to_string()), + domicile: Some("Jakarta Selatan".to_string()), + identity_document_url: "http://example.com/id.pdf".to_string(), + phone_for_verification: "0987654321".to_string(), + }, + professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile { + bio: "Experienced professional seeking to mentor others in software development.".to_string(), + last_education: Some("S1".to_string()), + linkedin_url: Some("http://linkedin.com/in/mentor".to_string()), + github_url: None, + cv_url: None, + portfolio_url: Some("http://example.com/portfolio".to_string()), + industries: vec!["Technology".to_string()], + expertise: vec!["Rust".to_string(), "Debugging".to_string()], + languages: vec!["English".to_string()], + current_company: "Acme Corp".to_string(), + current_role: "Senior Engineer".to_string(), + years_of_experience: 5, + }, + mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics { + topics_of_interest: vec!["Career Development".to_string()], + preferred_mentee_level: vec!["Beginner".to_string()], + preferred_mentoring_formats: vec!["Online".to_string()], + availability_commitment: "5 hours/week".to_string(), + mentoring_rate_amount: 100, + }, + }; + + let response = app + .oneshot( + Request::builder() + .method(Method::POST) + .uri("/v1/mentors/register") + .header("Content-Type", "application/json") + .body(Body::from(serde_json::to_string(&dto).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let body = response.into_body().collect().await.unwrap().to_bytes(); + let error_response: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert!(error_response["message"].as_str().unwrap().contains("email")); +} + +#[tokio::test] +async fn test_register_mentor_weak_password() { + let app_state = setup_all_test_environment().await; + let app = app(app_state.clone()); + + let test_email = "weakpass@example.com"; + let test_password = "weak"; // Weak password + let test_fullname = "Test User"; + let test_phone = "1234567890"; + + let dto = MentorUserRegisterRequestDto { + email: test_email.to_string(), + password: test_password.to_string(), + fullname: test_fullname.to_string(), + phone_number: test_phone.to_string(), + identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification { + legal_name: "Legal Name".to_string(), + gender: Some("Laki-laki".to_string()), + domicile: Some("Jakarta Selatan".to_string()), + identity_document_url: "http://example.com/id.pdf".to_string(), + phone_for_verification: "0987654321".to_string(), + }, + professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile { + bio: "Experienced professional seeking to mentor others in software development.".to_string(), + last_education: Some("S1".to_string()), + linkedin_url: Some("http://linkedin.com/in/mentor".to_string()), + github_url: None, + cv_url: None, + portfolio_url: Some("http://example.com/portfolio".to_string()), + industries: vec!["Technology".to_string()], + expertise: vec!["Rust".to_string(), "Debugging".to_string()], + languages: vec!["English".to_string()], + current_company: "Acme Corp".to_string(), + current_role: "Senior Engineer".to_string(), + years_of_experience: 5, + }, + mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics { + topics_of_interest: vec!["Career Development".to_string()], + preferred_mentee_level: vec!["Beginner".to_string()], + preferred_mentoring_formats: vec!["Online".to_string()], + availability_commitment: "5 hours/week".to_string(), + mentoring_rate_amount: 100, + }, + }; + + let response = app + .oneshot( + Request::builder() + .method(Method::POST) + .uri("/v1/mentors/register") + .header("Content-Type", "application/json") + .body(Body::from(serde_json::to_string(&dto).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let body = response.into_body().collect().await.unwrap().to_bytes(); + let error_response: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert!(error_response["message"].as_str().unwrap().contains("password")); +} + +#[tokio::test] +async fn test_register_mentor_missing_fullname() { + let app_state = setup_all_test_environment().await; + let app = app(app_state.clone()); + + let test_email = "missingfullname@example.com"; + let test_password = "Password123!"; + let test_phone = "1234567890"; + + let dto = MentorUserRegisterRequestDto { + email: test_email.to_string(), + password: test_password.to_string(), + fullname: "".to_string(), // Missing fullname + phone_number: test_phone.to_string(), + identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification { + legal_name: "Legal Name".to_string(), + gender: Some("Laki-laki".to_string()), + domicile: Some("Jakarta Selatan".to_string()), + identity_document_url: "http://example.com/id.pdf".to_string(), + phone_for_verification: "0987654321".to_string(), + }, + professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile { + bio: "Experienced professional seeking to mentor others in software development.".to_string(), + last_education: Some("S1".to_string()), + linkedin_url: Some("http://linkedin.com/in/mentor".to_string()), + github_url: None, + cv_url: None, + portfolio_url: Some("http://example.com/portfolio".to_string()), + industries: vec!["Technology".to_string()], + expertise: vec!["Rust".to_string(), "Debugging".to_string()], + languages: vec!["English".to_string()], + current_company: "Acme Corp".to_string(), + current_role: "Senior Engineer".to_string(), + years_of_experience: 5, + }, + mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics { + topics_of_interest: vec!["Career Development".to_string()], + preferred_mentee_level: vec!["Beginner".to_string()], + preferred_mentoring_formats: vec!["Online".to_string()], + availability_commitment: "5 hours/week".to_string(), + mentoring_rate_amount: 100, + }, + }; + + let response = app + .oneshot( + Request::builder() + .method(Method::POST) + .uri("/v1/mentors/register") + .header("Content-Type", "application/json") + .body(Body::from(serde_json::to_string(&dto).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let body = response.into_body().collect().await.unwrap().to_bytes(); + let error_response: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert!(error_response["message"].as_str().unwrap().contains("fullname")); +} + +#[tokio::test] +async fn test_register_mentor_missing_phone_number() { + let app_state = setup_all_test_environment().await; + let app = app(app_state.clone()); + + let test_email = "missingphone@example.com"; + let test_password = "Password123!"; + let test_fullname = "Test User"; + + let dto = MentorUserRegisterRequestDto { + email: test_email.to_string(), + password: test_password.to_string(), + fullname: test_fullname.to_string(), + phone_number: "".to_string(), // Missing phone number + identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification { + legal_name: "Legal Name".to_string(), + gender: Some("Laki-laki".to_string()), + domicile: Some("Jakarta Selatan".to_string()), + identity_document_url: "http://example.com/id.pdf".to_string(), + phone_for_verification: "0987654321".to_string(), + }, + professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile { + bio: "Experienced professional seeking to mentor others in software development.".to_string(), + last_education: Some("S1".to_string()), + linkedin_url: Some("http://linkedin.com/in/mentor".to_string()), + github_url: None, + cv_url: None, + portfolio_url: Some("http://example.com/portfolio".to_string()), + industries: vec!["Technology".to_string()], + expertise: vec!["Rust".to_string(), "Debugging".to_string()], + languages: vec!["English".to_string()], + current_company: "Acme Corp".to_string(), + current_role: "Senior Engineer".to_string(), + years_of_experience: 5, + }, + mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics { + topics_of_interest: vec!["Career Development".to_string()], + preferred_mentee_level: vec!["Beginner".to_string()], + preferred_mentoring_formats: vec!["Online".to_string()], + availability_commitment: "5 hours/week".to_string(), + mentoring_rate_amount: 100, + }, + }; + + let response = app + .oneshot( + Request::builder() + .method(Method::POST) + .uri("/v1/mentors/register") + .header("Content-Type", "application/json") + .body(Body::from(serde_json::to_string(&dto).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let body = response.into_body().collect().await.unwrap().to_bytes(); + let error_response: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert!(error_response["message"].as_str().unwrap().contains("phone_number")); +} + +#[tokio::test] +async fn test_register_mentor_missing_identity_document_url() { + let app_state = setup_all_test_environment().await; + let app = app(app_state.clone()); + + let test_email = "missingdocurl@example.com"; + let test_password = "Password123!"; + let test_fullname = "Test User"; + let test_phone = "1234567890"; + + let dto = MentorUserRegisterRequestDto { + email: test_email.to_string(), + password: test_password.to_string(), + fullname: test_fullname.to_string(), + phone_number: test_phone.to_string(), + identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification { + legal_name: "Legal Name".to_string(), + gender: Some("Laki-laki".to_string()), + domicile: Some("Jakarta Selatan".to_string()), + identity_document_url: "".to_string(), // Missing identity document url + phone_for_verification: "0987654321".to_string(), + }, + professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile { + bio: "Experienced professional seeking to mentor others in software development.".to_string(), + last_education: Some("S1".to_string()), + linkedin_url: Some("http://linkedin.com/in/mentor".to_string()), + github_url: None, + cv_url: None, + portfolio_url: Some("http://example.com/portfolio".to_string()), + industries: vec!["Technology".to_string()], + expertise: vec!["Rust".to_string(), "Debugging".to_string()], + languages: vec!["English".to_string()], + current_company: "Acme Corp".to_string(), + current_role: "Senior Engineer".to_string(), + years_of_experience: 5, + }, + mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics { + topics_of_interest: vec!["Career Development".to_string()], + preferred_mentee_level: vec!["Beginner".to_string()], + preferred_mentoring_formats: vec!["Online".to_string()], + availability_commitment: "5 hours/week".to_string(), + mentoring_rate_amount: 100, + }, + }; + + let response = app + .oneshot( + Request::builder() + .method(Method::POST) + .uri("/v1/mentors/register") + .header("Content-Type", "application/json") + .body(Body::from(serde_json::to_string(&dto).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let body = response.into_body().collect().await.unwrap().to_bytes(); + let error_response: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert!(error_response["message"].as_str().unwrap().contains("identity_document_url")); +} + +#[tokio::test] +async fn test_register_mentor_invalid_phone_for_verification_format() { + let app_state = setup_all_test_environment().await; + let app = app(app_state.clone()); + + let test_email = "invalidphoneveri@example.com"; + let test_password = "Password123!"; + let test_fullname = "Test User"; + let test_phone = "1234567890"; + + let dto = MentorUserRegisterRequestDto { + email: test_email.to_string(), + password: test_password.to_string(), + fullname: test_fullname.to_string(), + phone_number: test_phone.to_string(), + identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification { + legal_name: "Legal Name".to_string(), + gender: Some("Laki-laki".to_string()), + domicile: Some("Jakarta Selatan".to_string()), + identity_document_url: "http://example.com/id.pdf".to_string(), + phone_for_verification: "invalid".to_string(), // Invalid phone for verification + }, + professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile { + bio: "Experienced professional seeking to mentor others in software development.".to_string(), + last_education: Some("S1".to_string()), + linkedin_url: Some("http://linkedin.com/in/mentor".to_string()), + github_url: None, + cv_url: None, + portfolio_url: Some("http://example.com/portfolio".to_string()), + industries: vec!["Technology".to_string()], + expertise: vec!["Rust".to_string(), "Debugging".to_string()], + languages: vec!["English".to_string()], + current_company: "Acme Corp".to_string(), + current_role: "Senior Engineer".to_string(), + years_of_experience: 5, + }, + mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics { + topics_of_interest: vec!["Career Development".to_string()], + preferred_mentee_level: vec!["Beginner".to_string()], + preferred_mentoring_formats: vec!["Online".to_string()], + availability_commitment: "5 hours/week".to_string(), + mentoring_rate_amount: 100, + }, + }; + + let response = app + .oneshot( + Request::builder() + .method(Method::POST) + .uri("/v1/mentors/register") + .header("Content-Type", "application/json") + .body(Body::from(serde_json::to_string(&dto).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let body = response.into_body().collect().await.unwrap().to_bytes(); + let error_response: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert!(error_response["message"].as_str().unwrap().contains("phone_for_verification")); +} \ No newline at end of file diff --git a/tests/src/dimentorin/mentors/mentor_repository_test.rs b/tests/src/dimentorin/mentors/mentor_repository_test.rs new file mode 100644 index 0000000..cf1b1e2 --- /dev/null +++ b/tests/src/dimentorin/mentors/mentor_repository_test.rs @@ -0,0 +1,349 @@ +use anyhow::Result; +use imphnen_dimentorin::v1::mentors::{MentorSchema, MentoringRate, MentorsRepository}; +use surrealdb::sql::Thing; +use imphnen_iam::v1::users::UsersRepository; +use tests::{ + create_test_user, get_role_id, ResourceEnum, cleanup_db, + generate_unique_email, setup_all_test_environment, +}; +use surrealdb::Uuid; + +/// Helper to create a full-featured MentorSchema for tests +fn create_full_mentor_schema(id: &str, user_id: &str, email: &str, legal_name: &str) -> MentorSchema { + MentorSchema { + id: Thing::from(("app_mentors", id)), + user_id: Some(Thing::from(("app_users", user_id))), + email: Some(email.to_string()), + legal_name: legal_name.to_string(), + gender: Some("Laki-laki".to_string()), + domicile: Some("Jakarta".to_string()), + identity_document_url: "https://example.com/ktp.jpg".to_string(), + phone_for_verification: "+6281234567890".to_string(), + bio: "Saya adalah mentor backend Rust dengan pengalaman 5 tahun dalam pengembangan aplikasi backend yang scalable dan performant.".to_string(), + last_education: Some("S1".to_string()), + linkedin_url: Some("https://linkedin.com/in/mentor".to_string()), + github_url: Some("http://github.com/test".to_string()), + cv_url: Some("http://example.com/cv.pdf".to_string()), + portfolio_url: Some("http://example.com/portfolio".to_string()), + industries: vec!["Software".to_string(), "Education".to_string()], + expertise: vec!["Rust".to_string(), "Microservices".to_string()], + languages: vec!["Indonesian".to_string(), "English".to_string()], + current_company: "PT Contoh".to_string(), + current_role: "Senior Backend Engineer".to_string(), + years_of_experience: 5, + topics_of_interest: vec!["Rust Programming".to_string(), "Backend Development".to_string()], + preferred_mentee_level: vec!["beginner".to_string(), "intermediate".to_string()], + preferred_mentoring_formats: vec!["Test Format".to_string()], + availability_commitment: "2 jam per minggu untuk mentoring online dan offline".to_string(), + mentoring_rate: MentoringRate { + amount: 100_000, + currency: "IDR".to_string(), + per_duration: "hour".to_string(), + }, + status: "verified".to_string(), + is_deleted: false, + created_at: "2024-01-01T00:00:00Z".to_string(), + updated_at: "2024-01-01T00:00:00Z".to_string(), + ..Default::default() + } +} + +#[tokio::test] +async fn test_create_mentor() -> Result<()> { + cleanup_db().await; // Keep for now as setup_all_test_environment doesn't clean up + let app_state = setup_all_test_environment().await; + let repo = MentorsRepository::new(&app_state); + + let id = Uuid::new_v4().to_string(); + let email = generate_unique_email("test_create_mentor"); + + let user_repo = UsersRepository::new(&app_state); + let mut user = create_test_user(&email, "Mentor User", true, &get_role_id(&app_state).await); + user.id = Thing::from(("app_users", id.as_str())); + user.email = email.to_string(); + user.mentor_id = Some(Thing::from(("app_mentors", id.as_str()))); + let create_result = user_repo.query_create_user(user.clone()).await; + assert!(create_result.is_ok(), "Failed to create user: {:?}", create_result.err()); + + let mentor = create_full_mentor_schema(&id, &id, &email, "Mentor User"); + let create_res = repo.query_create_mentor(mentor.clone()).await; + assert!(create_res.is_ok(), "Failed to create mentor: {:?}", create_res.err()); + + let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id.as_str())); + let mentor_fetched = repo.query_mentor_by_id(&thing_id, false).await; + assert!(mentor_fetched.is_ok(), "Failed to fetch mentor after create: {:?}", mentor_fetched.err()); + let mentor_fetched = mentor_fetched.unwrap(); + assert_eq!(mentor_fetched.legal_name, "Mentor User"); + assert_eq!(mentor_fetched.status, "verified"); + assert_eq!(mentor_fetched.user_id, Thing::from(("app_users", id.as_str()))); + Ok(()) +} + +#[tokio::test] +async fn test_get_all_mentors() -> Result<()> { + cleanup_db().await; + let app_state = setup_all_test_environment().await; + let repo = MentorsRepository::new(&app_state); + + let id = Uuid::new_v4().to_string(); + let email = generate_unique_email("test_get_all_mentors"); + + let user_repo = UsersRepository::new(&app_state); + let mut user = create_test_user(&email, "Test Mentor Get All User", true, &get_role_id(&app_state).await); + user.id = Thing::from(("app_users", id.as_str())); + user.email = email.to_string(); + user.mentor_id = Some(Thing::from(("app_mentors", id.as_str()))); + let create_user_res = user_repo.query_create_user(user.clone()).await; + assert!(create_user_res.is_ok(), "Failed to create user for get_all test: {:?}", create_user_res.err()); + + let mentor = create_full_mentor_schema(&id, &id, &email, "Test Mentor Get All"); + let create_res = repo.query_create_mentor(mentor.clone()).await; + assert!(create_res.is_ok(), "Failed to create mentor: {:?}", create_res.err()); + + let mut meta = imphnen_libs::MetaRequestDto::default(); + meta.search = Some("Test Mentor Get All".to_string()); + + let mentors_res = repo.query_mentor_list(meta).await; + assert!(mentors_res.is_ok(), "Failed to get all mentors: {:?}", mentors_res.err()); + let mentors = mentors_res.unwrap().data; + assert!(!mentors.is_empty(), "Mentors list should not be empty"); + Ok(()) +} + +#[tokio::test] +async fn test_get_mentor_by_id() -> Result<()> { + cleanup_db().await; + let app_state = setup_all_test_environment().await; + let repo = MentorsRepository::new(&app_state); + + let id = Uuid::new_v4().to_string(); + let email = generate_unique_email("test_get_mentor_by_id"); + + let user_repo = UsersRepository::new(&app_state); + let mut user = create_test_user(&email, "Test Mentor Get By Id User", true, &get_role_id(&app_state).await); + user.id = Thing::from(("app_users", id.as_str())); + user.email = email.to_string(); + user.mentor_id = Some(Thing::from(("app_mentors", id.as_str()))); + let create_user_res = user_repo.query_create_user(user.clone()).await; + assert!(create_user_res.is_ok(), "Failed to create user for get_by_id test: {:?}", create_user_res.err()); + + let mentor = create_full_mentor_schema(&id, &id, &email, "Test Mentor Get By Id"); + let create_res = repo.query_create_mentor(mentor.clone()).await; + assert!(create_res.is_ok(), "Failed to create mentor: {:?}", create_res.err()); + + let mentor_res = repo + .query_mentor_by_id(&Thing::from((ResourceEnum::Mentors.to_string().as_str(), id.as_str())), false) + .await; + assert!(mentor_res.is_ok(), "Failed to get mentor by id: {:?}", mentor_res.err()); + let mentor = mentor_res.unwrap(); + assert_eq!(mentor.legal_name, "Test Mentor Get By Id"); + Ok(()) +} + +#[tokio::test] +async fn test_update_mentor() -> Result<()> { + cleanup_db().await; + let app_state = setup_all_test_environment().await; + let repo = MentorsRepository::new(&app_state); + + let id = Uuid::new_v4().to_string(); + let email = generate_unique_email("test_update_mentor"); + + let user_repo = UsersRepository::new(&app_state); + let mut user = create_test_user(&email, "Test Mentor Update User", true, &get_role_id(&app_state).await); + user.id = Thing::from(("app_users", id.as_str())); + user.email = email.to_string(); + user.mentor_id = Some(Thing::from(("app_mentors", id.as_str()))); + let create_user_res = user_repo.query_create_user(user.clone()).await; + assert!(create_user_res.is_ok(), "Failed to create user for update test: {:?}", create_user_res.err()); + + let mentor = create_full_mentor_schema(&id, &id, &email, "Test Mentor Update"); + let create_res = repo.query_create_mentor(mentor.clone()).await; + assert!(create_res.is_ok(), "Failed to create mentor: {:?}", create_res.err()); + + let mut updated_mentor = mentor.clone(); + updated_mentor.legal_name = "Test Mentor Updated".to_string(); + updated_mentor.gender = Some("Perempuan".to_string()); + updated_mentor.domicile = Some("Bandung".to_string()); + updated_mentor.last_education = Some("S2".to_string()); + updated_mentor.portfolio_url = Some("http://example.com/updated_portfolio".to_string()); + updated_mentor.mentoring_rate.amount = 200_000; + + let update_res = repo.query_update_mentor(updated_mentor.clone()).await; + assert!(update_res.is_ok(), "Failed to update mentor: {:?}", update_res.err()); + + let mentor_fetched = repo + .query_mentor_by_id(&Thing::from((ResourceEnum::Mentors.to_string().as_str(), id.as_str())), false) + .await; + assert!(mentor_fetched.is_ok(), "Failed to fetch mentor after update: {:?}", mentor_fetched.err()); + let mentor_fetched = mentor_fetched.unwrap(); + assert_eq!(mentor_fetched.legal_name, "Test Mentor Updated"); + assert_eq!(mentor_fetched.gender, Some("Perempuan".to_string())); + assert_eq!(mentor_fetched.domicile, Some("Bandung".to_string())); + assert_eq!(mentor_fetched.last_education, Some("S2".to_string())); + assert_eq!(mentor_fetched.portfolio_url, Some("http://example.com/updated_portfolio".to_string())); + assert_eq!(mentor_fetched.mentoring_rate.amount, 200_000); + Ok(()) +} + +#[tokio::test] +async fn test_delete_mentor() -> Result<()> { + cleanup_db().await; + let app_state = setup_all_test_environment().await; + let repo = MentorsRepository::new(&app_state); + + let id = Uuid::new_v4().to_string(); + let email = generate_unique_email("test_delete_mentor"); + + let user_repo = UsersRepository::new(&app_state); + let mut user = create_test_user(&email, "Test Mentor Delete User", true, &get_role_id(&app_state).await); + user.id = Thing::from(("app_users", id.as_str())); + user.email = email.to_string(); + user.mentor_id = Some(Thing::from(("app_mentors", id.as_str()))); + let create_user_res = user_repo.query_create_user(user.clone()).await; + assert!(create_user_res.is_ok(), "Failed to create user for delete test: {:?}", create_user_res.err()); + + let mentor = create_full_mentor_schema(&id, &id, &email, "Test Mentor Delete"); + let create_res = repo.query_create_mentor(mentor.clone()).await; + assert!(create_res.is_ok(), "Failed to create mentor: {:?}", create_res.err()); + + let delete_res = repo.query_delete_mentor(id.clone()).await; + assert!(delete_res.is_ok(), "Failed to delete mentor: {:?}", delete_res.err()); + + let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id.as_str())); + let mentor_fetched = repo.query_mentor_by_id(&thing_id, false).await; + assert!(mentor_fetched.is_err(), "Mentor should not be found after soft delete"); + if let Some(err) = mentor_fetched.err() { + assert!( + err.to_string().contains("Mentor not found in database") + || err.to_string().contains("Mentor has been deleted"), + "Expected 'Mentor not found in database' or 'Mentor has been deleted' error, got: {}", + err + ); + } + + let delete_again_res = repo.query_delete_mentor(id.clone()).await; + assert!(delete_again_res.is_err(), "Should not be able to soft delete an already deleted mentor"); + if let Some(err) = delete_again_res.err() { + assert!( + err.to_string().contains("already soft deleted"), + "Expected 'already soft deleted' error, got: {}", + err + ); + } + Ok(()) +} + +#[tokio::test] +async fn test_get_by_user_email() -> Result<()> { + cleanup_db().await; + let app_state = setup_all_test_environment().await; + let repo = MentorsRepository::new(&app_state); + + let id = Uuid::new_v4().to_string(); + let user_email = generate_unique_email("test_mentor_by_email"); + + let user_repo = UsersRepository::new(&app_state); + let mut user = create_test_user(&user_email, "Test User By Email", true, &get_role_id(&app_state).await); + user.id = Thing::from(("app_users", id.as_str())); + user.email = user_email.clone(); + user.mentor_id = Some(Thing::from(("app_mentors", id.as_str()))); + let create_user_res = user_repo.query_create_user(user.clone()).await; + assert!(create_user_res.is_ok(), "Failed to create user for get_by_user_email test: {:?}", create_user_res.err()); + + let mentor = create_full_mentor_schema(&id, &id, &user_email, "Test Mentor By Email"); + let create_res = repo.query_create_mentor(mentor.clone()).await; + assert!(create_res.is_ok(), "Failed to create mentor: {:?}", create_res.err()); + + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + + let mentor_detail = repo + .query_mentor_by_email(user_email.to_string(), false) + .await; + if mentor_detail.is_err() { + let mentor_by_id = repo + .query_mentor_by_id(&Thing::from(("app_mentors", id.as_str())), false) + .await; + assert!( + mentor_by_id.is_ok(), + "Failed to get mentor by user_id fallback: {:?}", + mentor_by_id.err() + ); + let mentor_by_id = mentor_by_id.unwrap(); + assert_eq!(mentor_by_id.legal_name, "Test Mentor By Email"); + return Ok(()); + } + assert!( + mentor_detail.is_ok(), + "Failed to get mentor by user email: {:?}", + mentor_detail.err() + ); + let mentor_detail = mentor_detail.unwrap(); + assert_eq!(mentor_detail.legal_name, "Test Mentor By Email"); + Ok(()) +} + +#[tokio::test] +async fn test_create_mentor_with_duplicate_user_id() -> Result<()> { + cleanup_db().await; + let app_state = setup_all_test_environment().await; + let repo = MentorsRepository::new(&app_state); + + let id = Uuid::new_v4().to_string(); + let email = generate_unique_email("test_duplicate_user_id"); + + let user_repo = UsersRepository::new(&app_state); + let mut user = create_test_user(&email, "Duplicate User", true, &get_role_id(&app_state).await); + user.id = Thing::from(("app_users", id.as_str())); + user.email = email.to_string(); + user.mentor_id = Some(Thing::from(("app_mentors", id.as_str()))); + let _ = user_repo.query_create_user(user.clone()).await; + + let mentor1 = create_full_mentor_schema(&id, &id, &email, "Mentor 1"); + let create_res1 = repo.query_create_mentor(mentor1.clone()).await; + assert!(create_res1.is_ok(), "Failed to create first mentor: {:?}", create_res1.err()); + + // Attempt to create another mentor with the same user_id + let duplicate_mentor_id = Uuid::new_v4().to_string(); + let mentor2 = create_full_mentor_schema(&duplicate_mentor_id, &id, &generate_unique_email("test_duplicate_user_id_2"), "Mentor 2"); + let create_res2 = repo.query_create_mentor(mentor2.clone()).await; + assert!(create_res2.is_err(), "Should not be able to create mentor with duplicate user_id"); + if let Some(err) = create_res2.err() { + assert!(err.to_string().contains("already has a mentor profile"), "Expected 'already has a mentor profile' error, got: {}", err); + } + Ok(()) +} + +#[tokio::test] +async fn test_update_non_existent_mentor() -> Result<()> { + cleanup_db().await; + let app_state = setup_all_test_environment().await; + let repo = MentorsRepository::new(&app_state); + + let non_existent_id = Uuid::new_v4().to_string(); + let non_existent_email = generate_unique_email("test_non_existent_mentor"); + let non_existent_user_id = Uuid::new_v4().to_string(); + + let mentor_to_update = create_full_mentor_schema(&non_existent_id, &non_existent_user_id, &non_existent_email, "Non Existent Mentor"); + let update_res = repo.query_update_mentor(mentor_to_update).await; + assert!(update_res.is_err(), "Should not be able to update a non-existent mentor"); + if let Some(err) = update_res.err() { + assert!(err.to_string().contains("Mentor not found"), "Expected 'Mentor not found' error, got: {}", err); + } + Ok(()) +} + +#[tokio::test] +async fn test_delete_non_existent_mentor() -> Result<()> { + cleanup_db().await; + let app_state = setup_all_test_environment().await; + let repo = MentorsRepository::new(&app_state); + + let non_existent_id = Uuid::new_v4().to_string(); + let delete_res = repo.query_delete_mentor(non_existent_id.clone()).await; + assert!(delete_res.is_err(), "Should not be able to delete a non-existent mentor"); + if let Some(err) = delete_res.err() { + assert!(err.to_string().contains("Mentor not found"), "Expected 'Mentor not found' error, got: {}", err); + } + Ok(()) +} diff --git a/tests/src/dimentorin/mentors/mod.rs b/tests/src/dimentorin/mentors/mod.rs new file mode 100644 index 0000000..5dc6ae6 --- /dev/null +++ b/tests/src/dimentorin/mentors/mod.rs @@ -0,0 +1,4 @@ +#[cfg(test)] +pub mod mentor_repository_test; +#[cfg(test)] +pub mod mentor_registration_tests; diff --git a/tests/src/dimentorin/mod.rs b/tests/src/dimentorin/mod.rs new file mode 100644 index 0000000..8ee3a1d --- /dev/null +++ b/tests/src/dimentorin/mod.rs @@ -0,0 +1 @@ +pub mod mentor; diff --git a/tests/src/iam/auth/auth_login_tests.rs b/tests/src/iam/auth/auth_login_tests.rs new file mode 100644 index 0000000..d11047a --- /dev/null +++ b/tests/src/iam/auth/auth_login_tests.rs @@ -0,0 +1,379 @@ +#[cfg(test)] +mod auth_login_tests { + use crate::generate_unique_email; + use crate::hash_password; + use crate::mock_test::setup_all_test_environment; + use axum::http::StatusCode; + use imphnen_iam::{ + v1::auth::{AuthLoginRequestDto, AuthService}, + AppState, UsersRepository, UsersSchema, + }; + use serde_json::Value; // Import the new setup function + + async fn setup_test_environment() -> AppState { + setup_all_test_environment().await + } + + async fn create_test_user_with_role( + state: &AppState, + email: &str, + password: &str, + role_name: &str, + is_active: bool, + ) -> UsersSchema { + let role_repo = imphnen_iam::RolesRepository::new(state); + + let role = match role_repo.query_role_by_name(role_name.to_string()).await { + Ok(role) => role, + Err(_) => { + let _ = role_repo + .query_create_role(imphnen_iam::RolesRequestCreateDto { + name: role_name.to_string(), + permissions: vec![], + }) + .await + .unwrap(); + + role_repo + .query_role_by_name(role_name.to_string()) + .await + .unwrap_or_else(|_| { + panic!("Failed to create {role_name} role"); + }) + } + }; + + let user = UsersSchema { + id: crate::make_thing("app_users", &uuid::Uuid::new_v4().to_string()), + email: email.to_string(), + fullname: "Test User".to_string(), + password: hash_password(password).unwrap(), + is_deleted: false, + avatar: None, + phone_number: "081234567890".to_string(), + is_active, + gender: None, + birthdate: None, + role: crate::make_thing("app_roles", &role.id), + mentor_id: None, + created_at: imphnen_utils::get_iso_date(), + updated_at: imphnen_utils::get_iso_date(), + }; + let user_repo = UsersRepository::new(state); + user_repo + .query_create_user(user.clone()) + .await + .expect("Failed to create test user"); + + user + } + + #[tokio::test] + async fn test_successful_login_with_valid_credentials() { + let state = setup_test_environment().await; + let email = generate_unique_email("test_login_success"); + let password = "TestPass123!"; + + create_test_user_with_role(&state, &email, password, "User", true).await; + + let login_dto = AuthLoginRequestDto { + email: email.clone(), + password: password.to_string(), + }; + + let response = AuthService::mutation_login(login_dto, &state).await; + let (parts, body) = response.into_parts(); + + assert_eq!(parts.status, StatusCode::OK); + + let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap(); + let response_json: Value = serde_json::from_slice(&body_bytes).unwrap(); + + assert!(response_json.get("data").is_some()); + assert!(response_json["data"].get("token").is_some()); + assert!(response_json["data"]["token"].get("access_token").is_some()); + assert!(response_json["data"]["token"] + .get("refresh_token") + .is_some()); + assert!(response_json["data"].get("user").is_some()); + assert_eq!(response_json["data"]["user"]["email"], email); + } + + #[tokio::test] + async fn test_login_with_invalid_email_format() { + let state = setup_test_environment().await; + + let login_dto = AuthLoginRequestDto { + email: "invalid-email".to_string(), + password: "TestPass123!".to_string(), + }; + + let response = AuthService::mutation_login(login_dto, &state).await; + let (parts, body) = response.into_parts(); + + assert_eq!(parts.status, StatusCode::BAD_REQUEST); + + let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap(); + let response_json: Value = serde_json::from_slice(&body_bytes).unwrap(); + assert_eq!(response_json["message"], "Email not valid"); + } + + #[tokio::test] + async fn test_login_with_empty_email() { + let state = setup_test_environment().await; + + let login_dto = AuthLoginRequestDto { + email: "".to_string(), + password: "TestPass123!".to_string(), + }; + + let response = AuthService::mutation_login(login_dto, &state).await; + let (parts, body) = response.into_parts(); + + assert_eq!(parts.status, StatusCode::BAD_REQUEST); + let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap(); + let response_json: Value = serde_json::from_slice(&body_bytes).unwrap(); + let message = response_json["message"].as_str().unwrap(); + assert!(message.contains("Email cannot be empty")); + assert!(message.contains("Email not valid")); + } + + #[tokio::test] + async fn test_login_with_empty_password() { + let state = setup_test_environment().await; + + let login_dto = AuthLoginRequestDto { + email: generate_unique_email("test_empty_pass"), + password: "".to_string(), + }; + + let response = AuthService::mutation_login(login_dto, &state).await; + let (parts, body) = response.into_parts(); + + assert_eq!(parts.status, StatusCode::BAD_REQUEST); + let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap(); + let response_json: Value = serde_json::from_slice(&body_bytes).unwrap(); + assert_eq!(response_json["message"], "Password cannot be empty"); + } + + #[tokio::test] + async fn test_login_with_wrong_password() { + let state = setup_test_environment().await; + let email = generate_unique_email("test_wrong_pass"); + let correct_password = "TestPass123!"; + + create_test_user_with_role(&state, &email, correct_password, "User", true).await; + + let login_dto = AuthLoginRequestDto { + email: email.clone(), + password: "WrongPassword123!".to_string(), + }; + + let response = AuthService::mutation_login(login_dto, &state).await; + let (parts, body) = response.into_parts(); + + assert_eq!(parts.status, StatusCode::BAD_REQUEST); + + let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap(); + let response_json: Value = serde_json::from_slice(&body_bytes).unwrap(); + + assert_eq!(response_json["message"], "Email or password not correct"); + } + + #[tokio::test] + async fn test_login_with_nonexistent_user() { + let state = setup_test_environment().await; + + let login_dto = AuthLoginRequestDto { + email: generate_unique_email("nonexistent"), + password: "TestPass123!".to_string(), + }; + + let response = AuthService::mutation_login(login_dto, &state).await; + let (parts, body) = response.into_parts(); + + assert_eq!(parts.status, StatusCode::UNAUTHORIZED); + + let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap(); + let response_json: Value = serde_json::from_slice(&body_bytes).unwrap(); + + assert!(response_json["message"] + .to_string() + .contains("User not found")); + } + + #[tokio::test] + async fn test_login_with_inactive_user() { + let state = setup_test_environment().await; + let email = generate_unique_email("test_inactive"); + let password = "TestPass123!"; + + create_test_user_with_role(&state, &email, password, "User", false).await; + + let login_dto = AuthLoginRequestDto { + email: email.clone(), + password: password.to_string(), + }; + + let response = AuthService::mutation_login(login_dto, &state).await; + let (parts, body) = response.into_parts(); + + assert_eq!(parts.status, StatusCode::BAD_REQUEST); + + let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap(); + let response_json: Value = serde_json::from_slice(&body_bytes).unwrap(); + + assert_eq!( + response_json["message"], + "Account not active, please verify your email" + ); + } + + #[tokio::test] + async fn test_successful_mentor_login() { + let state = setup_test_environment().await; + let email = generate_unique_email("test_mentor_login"); + let password = "TestPass123!"; + + create_test_user_with_role(&state, &email, password, "Mentor", true).await; + + let login_dto = AuthLoginRequestDto { + email: email.clone(), + password: password.to_string(), + }; + + let response = AuthService::mutation_mentor_login(login_dto, &state).await; + let (parts, body) = response.into_parts(); + + assert_eq!(parts.status, StatusCode::OK); + + let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap(); + let response_json: Value = serde_json::from_slice(&body_bytes).unwrap(); + + assert!(response_json.get("data").is_some()); + assert_eq!(response_json["data"]["user"]["role"]["name"], "Mentor"); + } + + #[tokio::test] + async fn test_mentor_login_with_non_mentor_user() { + let state = setup_test_environment().await; + let email = generate_unique_email("test_user_not_mentor"); + let password = "TestPass123!"; + + create_test_user_with_role(&state, &email, password, "User", true).await; + + let login_dto = AuthLoginRequestDto { + email: email.clone(), + password: password.to_string(), + }; + + let response = AuthService::mutation_mentor_login(login_dto, &state).await; + let (parts, body) = response.into_parts(); + + assert_eq!(parts.status, StatusCode::FORBIDDEN); + + let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap(); + let response_json: Value = serde_json::from_slice(&body_bytes).unwrap(); + + assert_eq!( + response_json["message"], + "User does not have mentor privileges" + ); + } + + #[tokio::test] + async fn test_mentor_login_with_inactive_mentor() { + let state = setup_test_environment().await; + let email = generate_unique_email("test_inactive_mentor"); + let password = "TestPass123!"; + + create_test_user_with_role(&state, &email, password, "Mentor", false).await; + + let login_dto = AuthLoginRequestDto { + email: email.clone(), + password: password.to_string(), + }; + + let response = AuthService::mutation_mentor_login(login_dto, &state).await; + let (parts, body) = response.into_parts(); + + assert_eq!(parts.status, StatusCode::BAD_REQUEST); + + let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap(); + let response_json: Value = serde_json::from_slice(&body_bytes).unwrap(); + + assert_eq!( + response_json["message"], + "Account not active, please verify your email" + ); + } + + #[tokio::test] + async fn test_login_creates_user_cache() { + let state = setup_test_environment().await; + let email = generate_unique_email("test_cache"); + let password = "TestPass123!"; + + create_test_user_with_role(&state, &email, password, "User", true).await; + + let login_dto = AuthLoginRequestDto { + email: email.clone(), + password: password.to_string(), + }; + + let response = AuthService::mutation_login(login_dto, &state).await; + let (parts, _) = response.into_parts(); + + assert_eq!(parts.status, StatusCode::OK); + + // Verify user was cached + let auth_repo = imphnen_iam::AuthRepository::new(&state); + let cached_user = auth_repo.query_get_stored_user(email.clone()).await; + assert!(cached_user.is_ok()); + assert_eq!(cached_user.unwrap().email, email); + } + + #[tokio::test] + async fn test_login_with_special_characters_in_email() { + let state = setup_test_environment().await; + let email = generate_unique_email("test+special"); + let password = "TestPass123!"; + + create_test_user_with_role(&state, &email, password, "User", true).await; + + let login_dto = AuthLoginRequestDto { + email: email.clone(), + password: password.to_string(), + }; + + let response = AuthService::mutation_login(login_dto, &state).await; + let (parts, _) = response.into_parts(); + + assert_eq!(parts.status, StatusCode::OK); + } + + #[tokio::test] + async fn test_login_with_case_sensitive_email() { + let state = setup_test_environment().await; + let email = generate_unique_email("test_case"); + let password = "TestPass123!"; + + create_test_user_with_role(&state, &email, password, "User", true).await; + + let login_dto = AuthLoginRequestDto { + email: email.to_uppercase(), + password: password.to_string(), + }; + + let response = AuthService::mutation_login(login_dto, &state).await; + let (parts, body) = response.into_parts(); + + // Email should be case-sensitive + assert_eq!(parts.status, StatusCode::UNAUTHORIZED); + let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap(); + let response_json: Value = serde_json::from_slice(&body_bytes).unwrap(); + assert!(response_json["message"] + .to_string() + .contains("User not found")); + } +} diff --git a/tests/src/iam/auth/auth_repository_test.rs b/tests/src/iam/auth/auth_repository_test.rs index 803955b..f2cccbe 100644 --- a/tests/src/iam/auth/auth_repository_test.rs +++ b/tests/src/iam/auth/auth_repository_test.rs @@ -1,9 +1,16 @@ #[cfg(test)] mod auth_repository_test { use crate::{ - AuthOtpSchema, AuthRepository, ResourceEnum, UsersRepository, UsersSchema, - create_mock_app_state, generate_unique_email, get_iso_date, get_role_id, + generate_unique_email, + get_iso_date, + get_role_id, make_thing, + setup_all_test_environment, // Import the new setup function + AuthOtpSchema, + AuthRepository, + ResourceEnum, + UsersRepository, + UsersSchema, }; use chrono::{Duration, Utc}; use imphnen_iam::{AppState, RolesDetailQueryDto, UsersDetailQueryDto}; @@ -22,6 +29,7 @@ mod auth_repository_test { gender: None, birthdate: None, role: make_thing("app_roles", &get_role_id(state).await), + mentor_id: None, created_at: get_iso_date(), updated_at: get_iso_date(), } @@ -29,27 +37,35 @@ mod auth_repository_test { #[tokio::test] async fn test_store_and_get_user() { - let app_state = create_mock_app_state().await; + let app_state = setup_all_test_environment().await; // Use the new setup function let repo = AuthRepository::new(&app_state); let email = generate_unique_email("forgot"); - let user = create_mock_user(&app_state, &email).await; + let mut user = create_mock_user(&app_state, &email).await; + user.role = make_thing("app_roles", &get_role_id(&app_state).await); let user_repo = UsersRepository::new(&app_state); let create_user = user_repo.query_create_user(user.clone()).await; assert!(create_user.is_ok()); - let user_data = user_repo - .query_user_by_email(email.to_string()) - .await - .unwrap(); + let user_data = user_repo.query_user_by_email(email).await; + assert!( + user_data.is_ok(), + "Failed to get user by email: {:?}", + user_data.err() + ); + let user_data = user_data.unwrap(); let store = repo.query_store_user(user_data.clone()).await; - assert!(store.is_ok()); + assert!(store.is_ok(), "Failed to store user: {:?}", store.err()); let fetched = repo.query_get_stored_user(user.email.clone()).await; - assert!(fetched.is_ok()); + assert!( + fetched.is_ok(), + "Failed to fetch stored user: {:?}", + fetched.err() + ); assert_eq!(fetched.unwrap().email, user.email); } #[tokio::test] async fn test_delete_stored_user() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; // Use the new setup function let auth_repo = AuthRepository::new(&state); let email = "delete_me@example.com".to_string(); let mock_user = UsersDetailQueryDto { @@ -71,15 +87,20 @@ mod auth_repository_test { }, is_deleted: false, password: "".into(), + mentor_id: None, created_at: get_iso_date(), updated_at: get_iso_date(), }; - let _: Option = state + let created: Result, surrealdb::Error> = state .surrealdb_mem .create((ResourceEnum::UsersCache.to_string(), email.clone())) .content(mock_user) - .await - .unwrap(); + .await; + assert!( + created.is_ok(), + "Failed to create mock user: {:?}", + created.err() + ); let result = auth_repo.query_delete_stored_user(email.clone()).await; assert!( result.is_ok(), @@ -91,101 +112,120 @@ mod auth_repository_test { #[tokio::test] async fn test_store_and_get_otp() { - let app_state = create_mock_app_state().await; + let app_state = setup_all_test_environment().await; // Use the new setup function let repo = AuthRepository::new(&app_state); let email = "otp_user@example.com".to_string(); let otp = 123456; let stored = repo.query_store_otp(email.clone(), otp).await; - assert!(stored.is_ok()); + assert!(stored.is_ok(), "Failed to store OTP: {:?}", stored.err()); let fetched = repo.query_get_stored_otp(email.clone()).await; - assert!(fetched.is_ok()); + assert!(fetched.is_ok(), "Failed to fetch OTP: {:?}", fetched.err()); assert_eq!(fetched.unwrap(), otp); } #[tokio::test] async fn test_delete_stored_otp() { - let app_state = create_mock_app_state().await; + let app_state = setup_all_test_environment().await; // Use the new setup function let repo = AuthRepository::new(&app_state); let email = "otp_del@example.com".to_string(); let otp = 654321; - repo.query_store_otp(email.clone(), otp).await.unwrap(); + let store_res = repo.query_store_otp(email.clone(), otp).await; + assert!( + store_res.is_ok(), + "Failed to store OTP: {:?}", + store_res.err() + ); let deleted = repo.query_delete_stored_otp(email.clone()).await; - assert!(deleted.is_ok()); + assert!(deleted.is_ok(), "Failed to delete OTP: {:?}", deleted.err()); let fetched = repo.query_get_stored_otp(email.clone()).await; - assert!(fetched.is_err()); + assert!( + fetched.is_err(), + "OTP should be deleted, but got: {fetched:?}" + ); } #[tokio::test] async fn test_expired_otp() { - let app_state = create_mock_app_state().await; + let app_state = setup_all_test_environment().await; // Use the new setup function let repo = AuthRepository::new(&app_state); let email = "expired_otp@example.com".to_string(); let otp = 789012; let table = ResourceEnum::OtpCache.to_string(); let expires_at = Utc::now() - Duration::seconds(1); - let _: Option = repo + let created: Result, surrealdb::Error> = repo .state .surrealdb_mem .create((table.clone(), email.as_str())) .content(AuthOtpSchema { otp, expires_at }) - .await - .unwrap(); + .await; + assert!( + created.is_ok(), + "Failed to create expired OTP: {:?}", + created.err() + ); let result = repo.query_get_stored_otp(email.clone()).await; - assert!(result.is_err()); + assert!( + result.is_err(), + "Expired OTP should not be retrievable, got: {result:?}" + ); + if let Some(err) = result.err() { + assert!( + err.to_string().contains("OTP expired"), + "Expected 'OTP expired' error, got: {err}" + ); + } } #[tokio::test] async fn test_get_non_existent_stored_user_should_fail() { - let app_state = create_mock_app_state().await; + let app_state = setup_all_test_environment().await; // Use the new setup function let repo = AuthRepository::new(&app_state); let result = repo .query_get_stored_user("not_found@example.com".into()) .await; assert!(result.is_err()); + if let Some(err) = result.err() { + assert!( + err.to_string().contains("No stored user data found"), + "Expected 'No stored user data found' error, got: {err}" + ); + } } #[tokio::test] async fn test_delete_non_existent_user_should_fail() { - let app_state = create_mock_app_state().await; + let app_state = setup_all_test_environment().await; // Use the new setup function let repo = AuthRepository::new(&app_state); let result = repo .query_delete_stored_user("ghost@example.com".into()) .await; assert!(result.is_err()); - } - - #[tokio::test] - async fn test_get_expired_otp_should_fail() { - use chrono::Duration; - let app_state = create_mock_app_state().await; - let repo = AuthRepository::new(&app_state); - let email = "expired_otp@example.com"; - let expired_time = chrono::Utc::now() - Duration::seconds(10); - let otp = 123456; - let _: Option = repo - .state - .surrealdb_mem - .create((ResourceEnum::OtpCache.to_string(), email)) - .content(AuthOtpSchema { - otp, - expires_at: expired_time, - }) - .await - .unwrap(); - - let result = repo.query_get_stored_otp(email.into()).await; - assert!(result.is_err()); + if let Some(err) = result.err() { + assert!( + err.to_string().contains("Failed delete stored user"), + "Expected 'Failed delete stored user' error, got: {err}" + ); + } } #[tokio::test] async fn test_store_and_get_valid_otp() { - let app_state = create_mock_app_state().await; + let app_state = setup_all_test_environment().await; // Use the new setup function let repo = AuthRepository::new(&app_state); let email = "valid_otp@example.com"; let otp = 654321; let store_result = repo.query_store_otp(email.into(), otp).await; - assert!(store_result.is_ok()); + assert!( + store_result.is_ok(), + "Failed to store valid OTP: {:?}", + store_result.err() + ); let get_result = repo.query_get_stored_otp(email.into()).await; + assert!( + get_result.is_ok(), + "Failed to get valid OTP: {:?}", + get_result.err() + ); assert_eq!(get_result.unwrap(), otp); } } diff --git a/tests/src/iam/auth/mod.rs b/tests/src/iam/auth/mod.rs index 4d30d1c..a1ebde8 100644 --- a/tests/src/iam/auth/mod.rs +++ b/tests/src/iam/auth/mod.rs @@ -1,2 +1,4 @@ #[cfg(test)] +pub mod auth_login_tests; +#[cfg(test)] pub mod auth_repository_test; diff --git a/tests/src/iam/permissions/permissions_repository_test.rs b/tests/src/iam/permissions/permissions_repository_test.rs index b07befd..73bdd80 100644 --- a/tests/src/iam/permissions/permissions_repository_test.rs +++ b/tests/src/iam/permissions/permissions_repository_test.rs @@ -1,19 +1,22 @@ use crate::{ - create_mock_app_state, + get_iso_date, // Import the new setup function and get_iso_date permissions::{PermissionsRepository, PermissionsSchema}, + setup_all_test_environment, }; use chrono::Utc; fn create_dummy_permission(name: &str) -> PermissionsSchema { PermissionsSchema { name: name.to_string(), + created_at: Some(get_iso_date()), // Ensure created_at is always set + updated_at: Some(get_iso_date()), // Ensure updated_at is always set ..Default::default() } } #[tokio::test] async fn test_create_permission_should_succeed() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; // Use the new setup function let repo = PermissionsRepository::new(&state); let permission = create_dummy_permission("Test Permission"); let result = repo.query_create_permission(permission).await; @@ -22,7 +25,7 @@ async fn test_create_permission_should_succeed() { #[tokio::test] async fn test_query_permission_list_should_return_data() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; // Use the new setup function let repo = PermissionsRepository::new(&state); let _ = repo @@ -45,7 +48,7 @@ async fn test_query_permission_list_should_return_data() { #[tokio::test] async fn test_query_permission_by_id_should_succeed() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; // Use the new setup function let repo = PermissionsRepository::new(&state); let permission = create_dummy_permission("Detail"); let _ = repo.query_create_permission(permission.clone()).await; @@ -56,7 +59,7 @@ async fn test_query_permission_by_id_should_succeed() { #[tokio::test] async fn test_update_permission_should_succeed() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; // Use the new setup function let repo = PermissionsRepository::new(&state); let mut permission = create_dummy_permission("Update This"); let _ = repo.query_create_permission(permission.clone()).await; @@ -68,7 +71,7 @@ async fn test_update_permission_should_succeed() { #[tokio::test] async fn test_delete_permission_should_succeed() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; // Use the new setup function let repo = PermissionsRepository::new(&state); let permission = create_dummy_permission("To Be Deleted"); let _ = repo.query_create_permission(permission.clone()).await; @@ -79,33 +82,64 @@ async fn test_delete_permission_should_succeed() { #[tokio::test] async fn test_delete_permission_should_fail_if_already_deleted() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; // Use the new setup function let repo = PermissionsRepository::new(&state); let permission = create_dummy_permission("Delete Twice"); let _ = repo.query_create_permission(permission.clone()).await; let id = permission.id.id.to_raw(); - let _ = repo.query_delete_permission(id.clone()).await; - let second = repo.query_delete_permission(id).await; - assert!(second.is_err(), "Should fail on second delete"); + let delete_result = repo.query_delete_permission(id.clone()).await; + assert!( + delete_result.is_ok(), + "Initial delete failed: {:?}", + delete_result.err() + ); + let second_delete_result = repo.query_delete_permission(id).await; + assert!( + second_delete_result.is_err(), + "Should fail on second delete" + ); + if let Some(err) = second_delete_result.err() { + assert!( + err.to_string().contains("Permission not found"), + "Expected 'Permission not found' error, got: {err}" + ); + } } #[tokio::test] async fn test_update_permission_should_fail_if_deleted() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; // Use the new setup function let repo = PermissionsRepository::new(&state); let mut permission = create_dummy_permission("To Be Updated Then Deleted"); let _ = repo.query_create_permission(permission.clone()).await; let id = permission.id.id.to_raw(); - let _ = repo.query_delete_permission(id.clone()).await; + let delete_result = repo.query_delete_permission(id.clone()).await; + assert!( + delete_result.is_ok(), + "Initial delete failed: {:?}", + delete_result.err() + ); permission.name = "Try Update".into(); let result = repo.query_update_permission(permission).await; assert!(result.is_err(), "Update on deleted should fail"); + if let Some(err) = result.err() { + assert!( + err.to_string().contains("Permission not found"), + "Expected 'Permission not found' error, got: {err}" + ); + } } #[tokio::test] async fn test_query_permission_by_id_should_fail_if_not_found() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; // Use the new setup function let repo = PermissionsRepository::new(&state); let result = repo.query_permission_by_id("non-existent-id".into()).await; assert!(result.is_err(), "Expected error for not found id"); + if let Some(err) = result.err() { + assert!( + err.to_string().contains("Permission not found"), + "Expected 'Permission not found' error, got: {err}" + ); + } } diff --git a/tests/src/iam/roles/roles_repository_test.rs b/tests/src/iam/roles/roles_repository_test.rs index 34d4c25..738a421 100644 --- a/tests/src/iam/roles/roles_repository_test.rs +++ b/tests/src/iam/roles/roles_repository_test.rs @@ -1,5 +1,5 @@ use crate::{ - ResourceEnum, create_mock_app_state, get_iso_date, make_thing, + get_iso_date, make_thing, permissions::{ permissions_repository::PermissionsRepository, permissions_schema::PermissionsSchema, @@ -8,6 +8,7 @@ use crate::{ roles_dto::{RolesRequestCreateDto, RolesRequestUpdateDto}, roles_repository::RolesRepository, }, + setup_all_test_environment, ResourceEnum, }; use surrealdb::Uuid; @@ -17,58 +18,92 @@ fn generate_unique_name(prefix: &str) -> String { #[tokio::test] async fn test_query_create_role_should_succeed() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; let perm_repo = PermissionsRepository::new(&state); let role_repo = RolesRepository::new(&state); let perm_id = Uuid::new_v4().to_string(); let permission = PermissionsSchema { - id: make_thing(&ResourceEnum::Permissions.to_string(), &perm_id.clone()), + id: make_thing(&ResourceEnum::Permissions.to_string(), &perm_id), name: generate_unique_name("read_quiz"), is_deleted: false, created_at: Some(get_iso_date()), updated_at: Some(get_iso_date()), }; - perm_repo.query_create_permission(permission).await.unwrap(); + let perm_res = perm_repo.query_create_permission(permission).await; + assert!( + perm_res.is_ok(), + "Failed to create permission: {:?}", + perm_res.err() + ); let payload = RolesRequestCreateDto { name: generate_unique_name("user"), permissions: vec![perm_id.clone()], }; let result = role_repo.query_create_role(payload).await; - assert!(result.is_ok()); + assert!(result.is_ok(), "Failed to create role: {:?}", result.err()); } #[tokio::test] async fn test_query_role_by_name_should_return_data() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; let role_repo = RolesRepository::new(&state); let name = generate_unique_name("viewer"); let payload = RolesRequestCreateDto { name: name.clone(), permissions: vec![], }; - role_repo.query_create_role(payload.clone()).await.unwrap(); - let role = role_repo.query_role_by_name(name.clone()).await.unwrap(); + let create_res = role_repo.query_create_role(payload.clone()).await; + assert!( + create_res.is_ok(), + "Failed to create role: {:?}", + create_res.err() + ); + let role = role_repo.query_role_by_name(name.clone()).await; + assert!(role.is_ok(), "Failed to get role by name: {:?}", role.err()); + let role = role.unwrap(); assert_eq!(role.name, name.clone()); } #[tokio::test] async fn test_query_role_by_id_should_return_data() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; let role_repo = RolesRepository::new(&state); + let name = generate_unique_name("tester"); + let payload = RolesRequestCreateDto { name: name.clone(), permissions: vec![], }; - role_repo.query_create_role(payload.clone()).await.unwrap(); - let role = role_repo.query_role_by_name(name.clone()).await.unwrap(); - let result = role_repo.query_role_by_id(role.id).await.unwrap(); - assert_eq!(result.name, name.clone()); + + let create_res = role_repo.query_create_role(payload.clone()).await; + + assert!( + create_res.is_ok(), + "Failed to create role: {:?}", + create_res.err() + ); + + let role = role_repo.query_role_by_name(name.clone()).await; + + assert!(role.is_ok(), "Failed to get role by name: {:?}", role.err()); + let role = role.unwrap(); + + let result = role_repo.query_role_by_id(role.id.clone()).await; + + assert!( + result.is_ok(), + "Failed to get role by id: {:?}", + result.err() + ); + let result_role = result.unwrap(); + + assert_eq!(result_role.name, name.clone()); } #[tokio::test] async fn test_query_update_role_should_update_name_and_permissions() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; let repo = RolesRepository::new(&state); let perm_repo = PermissionsRepository::new(&state); let original_perm_id = Uuid::new_v4().to_string(); @@ -77,22 +112,32 @@ async fn test_query_update_role_should_update_name_and_permissions() { name: generate_unique_name("original_permission"), is_deleted: false, created_at: Some(crate::get_iso_date()), - updated_at: None, + updated_at: Some(crate::get_iso_date()), }; - perm_repo - .query_create_permission(original_perm) - .await - .unwrap(); + let perm_res = perm_repo.query_create_permission(original_perm).await; + assert!( + perm_res.is_ok(), + "Failed to create original permission: {:?}", + perm_res.err() + ); let role_upadate_name = generate_unique_name("role_for_update"); let create_payload = RolesRequestCreateDto { name: role_upadate_name.clone(), permissions: vec![original_perm_id.clone()], }; - repo.query_create_role(create_payload).await.unwrap(); - let existing_role = repo - .query_role_by_name(role_upadate_name.clone()) - .await - .unwrap(); + let create_res = repo.query_create_role(create_payload).await; + assert!( + create_res.is_ok(), + "Failed to create role: {:?}", + create_res.err() + ); + let existing_role = repo.query_role_by_name(role_upadate_name.clone()).await; + assert!( + existing_role.is_ok(), + "Failed to get role by name: {:?}", + existing_role.err() + ); + let existing_role = existing_role.unwrap(); let existing_role_id = existing_role.id.clone(); let new_perm_id = Uuid::new_v4().to_string(); let new_perm = PermissionsSchema { @@ -100,10 +145,15 @@ async fn test_query_update_role_should_update_name_and_permissions() { name: "New Permission".into(), is_deleted: false, created_at: Some(crate::get_iso_date()), - updated_at: None, + updated_at: Some(crate::get_iso_date()), }; let new_role_name = generate_unique_name("updated_role_name"); - perm_repo.query_create_permission(new_perm).await.unwrap(); + let perm_res = perm_repo.query_create_permission(new_perm).await; + assert!( + perm_res.is_ok(), + "Failed to create new permission: {:?}", + perm_res.err() + ); let update_payload = RolesRequestUpdateDto { name: Some(new_role_name.clone()), permissions: Some(vec![new_perm_id.clone()]), @@ -112,34 +162,57 @@ async fn test_query_update_role_should_update_name_and_permissions() { let update_result = repo .query_update_role(existing_role_id.clone(), update_payload) .await; - assert!(update_result.is_ok()); - let updated = repo - .query_role_by_id(existing_role_id.clone()) - .await - .unwrap(); - assert_eq!(updated.name, new_role_name.clone()); + assert!( + update_result.is_ok(), + "Failed to update role: {:?}", + update_result.err() + ); + let updated = repo.query_role_by_id(existing_role_id.clone()).await; + assert!( + updated.is_ok(), + "Failed to get updated role: {:?}", + updated.err() + ); + assert_eq!(updated.unwrap().name, new_role_name.clone()); } #[tokio::test] async fn test_query_delete_role_should_soft_delete() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; let role_repo = RolesRepository::new(&state); let name = generate_unique_name("temporary"); let payload = RolesRequestCreateDto { name: name.clone(), permissions: vec![], }; - role_repo.query_create_role(payload.clone()).await.unwrap(); - let role = role_repo.query_role_by_name(name.clone()).await.unwrap(); + let create_res = role_repo.query_create_role(payload.clone()).await; + assert!( + create_res.is_ok(), + "Failed to create role: {:?}", + create_res.err() + ); + let role = role_repo.query_role_by_name(name.clone()).await; + assert!(role.is_ok(), "Failed to get role by name: {:?}", role.err()); + let role = role.unwrap(); let result = role_repo.query_delete_role(role.id.clone()).await; - assert!(result.is_ok()); + assert!(result.is_ok(), "Failed to delete role: {:?}", result.err()); let deleted = role_repo.query_role_by_id(role.id).await; - assert!(deleted.is_err()); + assert!( + deleted.is_err(), + "Role should be deleted, but got: {deleted:?}" + ); + if let Some(err) = deleted.err() { + assert!( + err.to_string().contains("Role not found"), + "Expected 'Role not found' error, got: {err}" + ); + } } + #[tokio::test] -async fn test_query_update_role_should_fallback_to_existing_permissions_if_none_provided() - { - let state = create_mock_app_state().await; +async fn test_query_update_role_should_fallback_to_existing_permissions_if_none_provided( +) { + let state = setup_all_test_environment().await; let repo = RolesRepository::new(&state); let perm_repo = PermissionsRepository::new(&state); let perm_id = Uuid::new_v4().to_string(); @@ -148,18 +221,31 @@ async fn test_query_update_role_should_fallback_to_existing_permissions_if_none_ name: "Permission for Fallback".into(), is_deleted: false, created_at: Some(crate::get_iso_date()), - updated_at: None, + updated_at: Some(crate::get_iso_date()), }; - perm_repo.query_create_permission(permission).await.unwrap(); + let perm_res = perm_repo.query_create_permission(permission).await; + assert!( + perm_res.is_ok(), + "Failed to create permission: {:?}", + perm_res.err() + ); let create_payload = RolesRequestCreateDto { name: "Role With Permission".into(), permissions: vec![perm_id.clone()], }; - repo.query_create_role(create_payload).await.unwrap(); - let existing = repo - .query_role_by_name("Role With Permission".into()) - .await - .unwrap(); + let create_res = repo.query_create_role(create_payload).await; + assert!( + create_res.is_ok(), + "Failed to create role: {:?}", + create_res.err() + ); + let existing = repo.query_role_by_name("Role With Permission".into()).await; + assert!( + existing.is_ok(), + "Failed to get role by name: {:?}", + existing.err() + ); + let existing = existing.unwrap(); let existing_id = existing.id.clone(); let update_payload = RolesRequestUpdateDto { name: Some("Updated Role Name".into()), @@ -169,29 +255,61 @@ async fn test_query_update_role_should_fallback_to_existing_permissions_if_none_ let update_res = repo .query_update_role(existing_id.clone(), update_payload) .await; - assert!(update_res.is_ok()); + assert!( + update_res.is_ok(), + "Failed to update role (fallback): {:?}", + update_res.err() + ); } #[tokio::test] async fn test_query_role_by_name_should_fail_if_not_found() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; let role_repo = RolesRepository::new(&state); let result = role_repo.query_role_by_name("ghost-role".into()).await; assert!(result.is_err()); + if let Some(err) = result.err() { + assert!( + err.to_string().contains("Role not found"), + "Expected 'Role not found' error, got: {err}" + ); + } } #[tokio::test] async fn test_query_delete_role_should_fail_if_already_deleted() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; let role_repo = RolesRepository::new(&state); let name = generate_unique_name("soft_delete_test"); let payload = RolesRequestCreateDto { name: name.clone(), permissions: vec![], }; - role_repo.query_create_role(payload.clone()).await.unwrap(); - let role = role_repo.query_role_by_name(name.clone()).await.unwrap(); - role_repo.query_delete_role(role.id.clone()).await.unwrap(); - let result = role_repo.query_delete_role(role.id); - assert!(result.await.is_err()); + let create_res = role_repo.query_create_role(payload.clone()).await; + assert!( + create_res.is_ok(), + "Failed to create role: {:?}", + create_res.err() + ); + let role = role_repo.query_role_by_name(name.clone()).await; + assert!(role.is_ok(), "Failed to get role by name: {:?}", role.err()); + let role = role.unwrap(); + let del_res = role_repo.query_delete_role(role.id.clone()).await; + assert!( + del_res.is_ok(), + "Failed to delete role: {:?}", + del_res.err() + ); + let result_fut = role_repo.query_delete_role(role.id); + let result_val = result_fut.await; + assert!( + result_val.is_err(), + "Role should already be deleted, but got: {result_val:?}" + ); + if let Some(err) = result_val.err() { + assert!( + err.to_string().contains("Role not found"), + "Expected 'Role not found' error, got: {err}" + ); + } } diff --git a/tests/src/iam/users/users_repository_test.rs b/tests/src/iam/users/users_repository_test.rs index a95f6bd..4158c6f 100644 --- a/tests/src/iam/users/users_repository_test.rs +++ b/tests/src/iam/users/users_repository_test.rs @@ -1,31 +1,44 @@ -use crate::{MetaRequestDto, UsersRepository, generate_unique_email, get_role_id}; -use crate::{create_mock_app_state, create_test_user}; +use crate::{create_test_user, mock_test::setup_all_test_environment}; +use crate::{generate_unique_email, get_role_id, MetaRequestDto, UsersRepository}; // Import setup_all_test_environment from mock_test #[tokio::test] async fn test_create_and_get_user() { - let app_state = create_mock_app_state().await; + let app_state = setup_all_test_environment().await; // Use centralized setup let repo = UsersRepository::new(&app_state); let email = generate_unique_email("testuser"); let user = create_test_user(&email, "Test User", true, &get_role_id(&app_state).await); let create_result = repo.query_create_user(user.clone()).await; - assert!(create_result.is_ok()); - let fetched = repo.query_user_by_email(email.clone().into()).await; - assert!(fetched.is_ok()); + assert!( + create_result.is_ok(), + "Failed to create user: {:?}", + create_result.err() + ); + let fetched = repo.query_user_by_email(email.clone()).await; + assert!( + fetched.is_ok(), + "Failed to fetch user by email: {:?}", + fetched.err() + ); assert_eq!(fetched.unwrap().email, email.clone()); } #[tokio::test] async fn test_query_user_list_with_pagination_and_filter() { - let app_state = create_mock_app_state().await; + let app_state = setup_all_test_environment().await; // Use centralized setup let repo = UsersRepository::new(&app_state); for i in 0..10 { - let email = format!("user{}@example.com", i); - let fullname = format!("User {}", i); + let email = format!("user{i}@example.com"); + let fullname = format!("User {i}"); let is_active = i % 2 == 0; let user = create_test_user(&email, &fullname, is_active, &get_role_id(&app_state).await); - repo.query_create_user(user).await.unwrap(); + let create_res = repo.query_create_user(user).await; + assert!( + create_res.is_ok(), + "Failed to create user: {:?}", + create_res.err() + ); } let meta = MetaRequestDto { page: Some(1), @@ -36,25 +49,43 @@ async fn test_query_user_list_with_pagination_and_filter() { filter: Some("true".into()), filter_by: Some("is_active".into()), }; - let result = repo.query_user_list(meta).await.unwrap(); + let result = repo.query_user_list(meta).await; + assert!( + result.is_ok(), + "Failed to query user list: {:?}", + result.err() + ); + let result = result.unwrap(); assert!(result.data.len() <= 5); assert!(result.data.iter().all(|u| u.is_active)); - assert!(result.meta.as_ref().unwrap().total.is_some()); + assert!( + result + .meta + .as_ref() + .map(|m| m.total.is_some()) + .unwrap_or(false), + "Meta total should be Some" + ); } #[tokio::test] async fn test_query_user_list_basic() { - let app_state = create_mock_app_state().await; + let app_state = setup_all_test_environment().await; // Use centralized setup let repo = UsersRepository::new(&app_state); for i in 0..10 { - let email = format!("basic{}@example.com", i); + let email = format!("basic{i}@example.com"); let user = create_test_user( &email, - &format!("Basic User {}", i), + &format!("Basic User {i}"), true, &get_role_id(&app_state).await, ); - repo.query_create_user(user).await.unwrap(); + let create_res = repo.query_create_user(user).await; + assert!( + create_res.is_ok(), + "Failed to create user: {:?}", + create_res.err() + ); } let meta = MetaRequestDto { page: Some(1), @@ -65,59 +96,103 @@ async fn test_query_user_list_basic() { filter: None, filter_by: None, }; - let result = repo.query_user_list(meta).await.unwrap(); - assert!(result.meta.as_ref().unwrap().total.unwrap() >= 1); - assert_eq!(result.meta.as_ref().unwrap().page.unwrap(), 1); - assert_eq!(result.meta.as_ref().unwrap().per_page.unwrap(), 5); + let result = repo.query_user_list(meta).await; + assert!( + result.is_ok(), + "Failed to query user list: {:?}", + result.err() + ); + let result = result.unwrap(); + assert!( + result.meta.as_ref().and_then(|m| m.total).unwrap_or(0) >= 1, + "Meta total should be >= 1" + ); + assert_eq!( + result.meta.as_ref().and_then(|m| m.page).unwrap_or(0), + 1, + "Meta page should be 1" + ); + assert_eq!( + result.meta.as_ref().and_then(|m| m.per_page).unwrap_or(0), + 5, + "Meta per_page should be 5" + ); } #[tokio::test] async fn test_query_delete_user() { - let app_state = create_mock_app_state().await; + let app_state = setup_all_test_environment().await; // Use centralized setup let repo = UsersRepository::new(&app_state); let email = &generate_unique_email("deleteuser"); let user = create_test_user(email, "Delete User", true, &get_role_id(&app_state).await); - repo.query_create_user(user.clone()).await.unwrap(); - let user_detail = repo - .query_user_by_email(email.to_string().clone()) - .await - .unwrap(); + let create_res = repo.query_create_user(user.clone()).await; + assert!( + create_res.is_ok(), + "Failed to create user: {:?}", + create_res.err() + ); + let user_detail = repo.query_user_by_email(email.to_string().clone()).await; + assert!( + user_detail.is_ok(), + "Failed to fetch user by email: {:?}", + user_detail.err() + ); + let user_detail = user_detail.unwrap(); let delete_result = repo .query_delete_user(user_detail.id.id.to_raw().clone()) .await; - assert!(delete_result.is_ok()); + assert!( + delete_result.is_ok(), + "Failed to delete user: {:?}", + delete_result.err() + ); let fetch_result = repo.query_user_by_email(user_detail.email.clone()).await; - assert!(fetch_result.is_err()); + assert!( + fetch_result.is_err(), + "User should be deleted, but got: {fetch_result:?}" + ); } #[tokio::test] async fn test_delete_non_existent_user_should_fail() { - let app_state = create_mock_app_state().await; + let app_state = setup_all_test_environment().await; // Use centralized setup let repo = UsersRepository::new(&app_state); let result = repo.query_delete_user("lklklklk".to_string()).await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err().to_string(), "User not found"); + assert!(result.is_err(), "Delete non-existent user should fail"); + assert_eq!( + result.unwrap_err().to_string(), + "User not found in database" + ); } #[tokio::test] async fn test_delete_user_twice_should_fail_on_second_attempt() { - let app_state = create_mock_app_state().await; + let app_state = setup_all_test_environment().await; // Use centralized setup let repo = UsersRepository::new(&app_state); let email = "twice@example.com"; let user = create_test_user(email, "Delete Twice", true, &get_role_id(&app_state).await); - repo.query_create_user(user.clone()).await.unwrap(); + let create_res = repo.query_create_user(user.clone()).await; + assert!( + create_res.is_ok(), + "Failed to create user: {:?}", + create_res.err() + ); let first = repo.query_delete_user(user.id.id.to_raw()).await; - assert!(first.is_ok()); + assert!( + first.is_ok(), + "First delete should succeed: {:?}", + first.err() + ); let second = repo.query_delete_user(user.id.id.to_raw()).await; - assert!(second.is_err()); + assert!(second.is_err(), "Second delete should fail"); assert_eq!(second.unwrap_err().to_string(), "User not found"); } #[tokio::test] async fn test_query_update_user_should_succeed() { - let state = create_mock_app_state().await; + let state = setup_all_test_environment().await; // Use centralized setup let repo = UsersRepository::new(&state); let mut user = create_test_user( "update@example.com", @@ -125,12 +200,23 @@ async fn test_query_update_user_should_succeed() { true, &get_role_id(&state).await, ); - repo.query_create_user(user.clone()).await.unwrap(); + let create_res = repo.query_create_user(user.clone()).await; + assert!( + create_res.is_ok(), + "Failed to create user: {:?}", + create_res.err() + ); user.fullname = "Updated Name".into(); user.phone_number = "089876543210".into(); let result = repo.query_update_user(user.clone()).await; assert!(result.is_ok(), "Update failed: {:?}", result.err()); - let updated = repo.query_user_by_id(user.id.id.to_raw()).await.unwrap(); + let updated = repo.query_user_by_id(&user.id).await; + assert!( + updated.is_ok(), + "Failed to fetch updated user: {:?}", + updated.err() + ); + let updated = updated.unwrap(); assert_eq!(updated.fullname, "Updated Name"); assert_eq!(updated.phone_number, "089876543210"); } diff --git a/tests/src/lib.rs b/tests/src/lib.rs index f999392..ce7a91c 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -1,9 +1,49 @@ use ::surrealdb::Uuid; pub use imphnen_entities::*; pub use imphnen_iam::*; - +pub fn create_test_mentor( + email: &str, + fullname: &str, + is_active: bool, + role_id: &str, +) -> UsersSchema { + let mut user = create_test_user(email, fullname, is_active, role_id); + user.mentor_id = Some(user.id.clone()); + user +} +pub fn create_test_user( + email: &str, + fullname: &str, + is_active: bool, + role_id: &str, +) -> UsersSchema { + UsersSchema { + id: make_thing("app_users", &Uuid::new_v4().to_string()), + email: email.to_string(), + fullname: format!("Randomize {} {}", fullname, rand::random::()), + password: hash_password("secret").unwrap(), + is_deleted: false, + avatar: None, + phone_number: "081234567890".to_string(), + is_active, + gender: None, + birthdate: None, + role: make_thing("app_roles", role_id), + created_at: get_iso_date(), + updated_at: get_iso_date(), + mentor_id: None, + } +} #[cfg(test)] pub mod iam; +pub mod mock_test; + +pub use mock_test::{ + cleanup_db, create_mock_app_state, seed_permissions_and_roles_for_test, + seed_users_for_test, setup_all_test_environment, +}; + +pub use imphnen_utils::{get_iso_date, hash_password, make_thing, Env}; pub fn generate_unique_email(prefix: &str) -> String { format!("{}_{}@example.com", prefix, Uuid::new_v4()) @@ -26,3 +66,12 @@ pub async fn get_role_id(state: &crate::AppState) -> String { .expect("Role not found after creation") .id } + +pub async fn setup() { + cleanup_db().await; + let app_state = create_mock_app_state().await; + seed_permissions_and_roles_for_test(&app_state.surrealdb_ws) + .await + .unwrap(); + seed_users_for_test(&app_state.surrealdb_ws).await.unwrap(); +} diff --git a/tests/src/mock_test.rs b/tests/src/mock_test.rs new file mode 100644 index 0000000..ec85a06 --- /dev/null +++ b/tests/src/mock_test.rs @@ -0,0 +1,197 @@ +use crate::{get_iso_date, hash_password}; +use imphnen_entities::AppState; +use imphnen_iam::{PermissionsEnum, UsersSchema}; +use imphnen_libs::enviroment::load_env; +use serde::{Deserialize, Serialize}; +use strum::IntoEnumIterator; +use surrealdb::engine::{any, local}; +use surrealdb::{opt::auth::Root, sql::Thing, Connection, Surreal}; +use tracing::debug; +use uuid::Uuid; + +#[derive(Serialize, Deserialize, Debug)] +struct PermissionSeedData { + id: String, + name: String, + is_deleted: bool, + created_at: String, + updated_at: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct RoleSeedData { + id: String, + name: String, + is_deleted: bool, + created_at: String, + updated_at: String, + permissions: Vec, +} + +pub async fn create_mock_app_state() -> AppState { + load_env(); + + + + let db_ws = any::connect("ws://127.00.1:8000/rpc").await.unwrap(); + + + + let db_mem = Surreal::new::(()).await.unwrap(); + + + let unique_id = Uuid::new_v4().to_string(); + let ns = format!("test_ns_{unique_id}"); + let db = format!("test_db_{unique_id}"); + + + + db_ws + .signin(Root { + username: "root", + password: "root", + }) + .await + .unwrap(); + + + + db_ws.use_ns(&ns).use_db(&db).await.unwrap(); + + + AppState { + surrealdb_ws: db_ws, + surrealdb_mem: db_mem, + } +} +pub async fn cleanup_db() { + let app_state = create_mock_app_state().await; + let _ = app_state + .surrealdb_ws + .query( + r#"REMOVE TABLE app_users; REMOVE TABLE app_roles; REMOVE TABLE app_permissions; REMOVE TABLE app_roles_permissions; REMOVE TABLE app_users_cache; REMOVE TABLE app_otp_cache; REMOVE TABLE app_gacha_items; REMOVE TABLE app_gacha_claims; REMOVE TABLE app_gacha_rolls; REMOVE TABLE app_gacha_credits; REMOVE TABLE app_events; REMOVE TABLE app_testimonials; REMOVE TABLE app_mentors;"#, + ) + .await; +} + +pub async fn seed_permissions_and_roles_for_test( + db: &Surreal, +) -> Result<(), Box> { + db.query("DEFINE TABLE app_permissions;").await?; + db.query("DEFINE FIELD name ON app_permissions TYPE string;") + .await?; + db.query("DEFINE FIELD is_deleted ON app_permissions TYPE bool;") + .await?; + db.query("DEFINE FIELD created_at ON app_permissions TYPE string;") + .await?; + db.query("DEFINE FIELD updated_at ON app_permissions TYPE string;") + .await?; + + db.query("DEFINE TABLE app_roles;").await?; + db.query("DEFINE FIELD name ON app_roles TYPE string;") + .await?; + db.query("DEFINE FIELD is_deleted ON app_roles TYPE bool;") + .await?; + // Changed from array to array> + db.query( + "DEFINE FIELD permissions ON app_roles TYPE array>;", + ) + .await?; + db.query("DEFINE FIELD created_at ON app_roles TYPE string;") + .await?; + db.query("DEFINE FIELD updated_at ON app_roles TYPE string;") + .await?; + + for perm_enum in PermissionsEnum::iter() { + db.query("INSERT INTO app_permissions (id, name, is_deleted, created_at, updated_at) VALUES ($id, $name, $is_deleted, $created_at, $updated_at);") + .bind(("id", perm_enum.id())) + .bind(("name", perm_enum.to_string())) + .bind(("is_deleted", false)) + .bind(("created_at", get_iso_date())) + .bind(("updated_at", get_iso_date())) + .await?; + } + + let roles = vec![ + ("f6b03f25-e416-4893-ac88-caaa690afb07".to_string(), "Admin"), + ("3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a".to_string(), "Mentor"), + ("50133429-f4b1-4249-9f97-7b86e6ee9d86".to_string(), "Staff"), + ("5713cb37-dc02-4e87-8048-d7a41d352059".to_string(), "User"), + ]; + + for (id, name) in roles { + db.query("INSERT INTO app_roles (id, name, is_deleted, created_at, updated_at, permissions) VALUES ($id, $name, $is_deleted, $created_at, $updated_at, $permissions);") + .bind(("id", id)) + .bind(("name", name)) + .bind(("is_deleted", false)) + .bind(("created_at", get_iso_date())) + .bind(("updated_at", get_iso_date())) + .bind(("permissions", Vec::::new())) // Use Vec for permissions + .await?; + } + Ok(()) +} + +pub async fn seed_users_for_test( + db: &Surreal, +) -> Result<(), Box> { + let users_data = vec![ + ( + "c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2", + "admin@example.com", + "Admin", + "f6b03f25-e416-4893-ac88-caaa690afb07", + ), + ( + "a4d23fb5-9e31-423c-9842-fbd6e75a5298", + "staff@example.com", + "Staff", + "50133429-f4b1-4249-9f97-7b86e6ee9d86", + ), + ( + "d5e89c12-72af-4b1a-abc3-ff1234567890", + "user@example.com", + "User", + "5713cb37-dc02-4e87-8048-d7a41d352059", + ), + ]; + + for (id, email, fullname, role_id) in users_data { + let user = UsersSchema { + id: Thing::from(("app_users", id)), + fullname: fullname.into(), + email: email.into(), + password: hash_password("password").unwrap(), + avatar: None, + phone_number: "081234567890".into(), + is_active: true, + is_deleted: false, + mentor_id: None, + gender: None, + birthdate: None, + role: Thing::from(("app_roles", role_id)), + created_at: get_iso_date(), + updated_at: get_iso_date(), + }; + db.create::>(("app_users", id)) + .content(user) + .await?; + + println!("✅ Inserted user: {fullname} ({email})"); + } + Ok(()) +} + +pub async fn setup_all_test_environment() -> AppState { + let app_state = create_mock_app_state().await; + app_state + .surrealdb_mem + .use_ns("test_namespace") + .use_db("test_db") + .await + .unwrap(); + seed_permissions_and_roles_for_test(&app_state.surrealdb_ws) + .await + .unwrap(); + app_state +}