From 7cc2435ef24544bfd3cece6811751af13e6f8132 Mon Sep 17 00:00:00 2001 From: levish0 Date: Wed, 4 Mar 2026 17:24:53 +0900 Subject: [PATCH 01/19] refactoredd --- .env.example | 7 +- Cargo.lock | 668 +++++++++++++++--- Cargo.toml | 22 +- charts/README.md | 1 - charts/axumkit-server/values.yaml | 7 +- charts/axumkit-worker/values.yaml | 7 +- charts/axumkit/Chart.yaml | 5 - charts/axumkit/values-local.yaml.example | 8 +- charts/axumkit/values.yaml | 29 +- crates/axumkit-config/src/server_config.rs | 75 +- crates/axumkit-constants/src/cache_keys.rs | 27 + crates/axumkit-constants/src/lib.rs | 9 +- crates/axumkit-constants/src/storage_keys.rs | 3 - crates/axumkit-dto/src/lib.rs | 1 - crates/axumkit-dto/src/posts/mod.rs | 7 - .../src/posts/request/create_post.rs | 22 - .../axumkit-dto/src/posts/request/get_post.rs | 10 - .../src/posts/request/list_posts.rs | 8 - crates/axumkit-dto/src/posts/request/mod.rs | 9 - .../src/posts/request/update_post.rs | 22 - .../src/posts/response/create_post.rs | 18 - .../src/posts/response/delete_post.rs | 18 - .../src/posts/response/list_posts.rs | 28 - crates/axumkit-dto/src/posts/response/mod.rs | 9 - crates/axumkit-dto/src/posts/response/post.rs | 24 - crates/axumkit-dto/src/search/mod.rs | 4 +- crates/axumkit-dto/src/search/request/mod.rs | 2 - .../axumkit-dto/src/search/request/posts.rs | 22 - crates/axumkit-dto/src/search/response/mod.rs | 2 - .../axumkit-dto/src/search/response/posts.rs | 29 - crates/axumkit-entity/src/lib.rs | 1 - crates/axumkit-entity/src/posts.rs | 34 - crates/axumkit-errors/src/errors.rs | 12 +- .../src/handlers/general_handler.rs | 18 +- crates/axumkit-errors/src/handlers/mod.rs | 1 - .../src/handlers/post_handler.rs | 22 - .../src/handlers/user_handler.rs | 7 +- crates/axumkit-errors/src/protocol.rs | 8 +- .../src/api/v0/routes/auth/change_email.rs | 3 +- .../src/api/v0/routes/auth/change_password.rs | 3 +- .../src/api/v0/routes/auth/complete_signup.rs | 9 +- .../v0/routes/auth/confirm_email_change.rs | 3 +- .../src/api/v0/routes/auth/forgot_password.rs | 3 +- .../src/api/v0/routes/auth/login.rs | 8 +- .../src/api/v0/routes/auth/logout.rs | 5 +- .../src/api/v0/routes/auth/mod.rs | 3 +- .../auth/oauth/github/github_authorize.rs | 4 +- .../routes/auth/oauth/github/github_link.rs | 4 +- .../routes/auth/oauth/github/github_login.rs | 10 +- .../api/v0/routes/auth/oauth/github/mod.rs | 3 +- .../auth/oauth/google/google_authorize.rs | 4 +- .../routes/auth/oauth/google/google_link.rs | 4 +- .../routes/auth/oauth/google/google_login.rs | 10 +- .../api/v0/routes/auth/oauth/google/mod.rs | 3 +- .../auth/oauth/list_oauth_connections.rs | 4 +- .../src/api/v0/routes/auth/oauth/mod.rs | 3 +- .../auth/oauth/unlink_oauth_connection.rs | 4 +- .../src/api/v0/routes/auth/openapi.rs | 7 +- .../routes/auth/resend_verification_email.rs | 3 +- .../src/api/v0/routes/auth/reset_password.rs | 3 +- .../src/api/v0/routes/auth/routes.rs | 3 +- .../src/api/v0/routes/auth/totp/disable.rs | 7 +- .../src/api/v0/routes/auth/totp/enable.rs | 7 +- .../src/api/v0/routes/auth/totp/mod.rs | 3 +- .../auth/totp/regenerate_backup_codes.rs | 8 +- .../src/api/v0/routes/auth/totp/setup.rs | 7 +- .../src/api/v0/routes/auth/totp/status.rs | 5 +- .../src/api/v0/routes/auth/totp/verify.rs | 3 +- .../src/api/v0/routes/auth/verify_email.rs | 3 +- .../axumkit-server/src/api/v0/routes/mod.rs | 1 - .../src/api/v0/routes/openapi.rs | 2 - .../src/api/v0/routes/posts/create_post.rs | 37 - .../src/api/v0/routes/posts/delete_post.rs | 27 - .../src/api/v0/routes/posts/get_post.rs | 24 - .../src/api/v0/routes/posts/list_posts.rs | 29 - .../src/api/v0/routes/posts/mod.rs | 7 - .../src/api/v0/routes/posts/openapi.rs | 33 - .../src/api/v0/routes/posts/routes.rs | 19 - .../src/api/v0/routes/posts/update_post.rs | 40 -- .../src/api/v0/routes/routes.rs | 2 - .../src/api/v0/routes/search/mod.rs | 1 - .../src/api/v0/routes/search/openapi.rs | 7 +- .../src/api/v0/routes/search/routes.rs | 5 +- .../src/api/v0/routes/search/search_posts.rs | 26 - .../src/bridge/worker_client/index.rs | 45 +- .../src/bridge/worker_client/mod.rs | 2 - .../src/bridge/worker_client/reindex.rs | 30 +- .../src/bridge/worker_client/storage.rs | 25 - crates/axumkit-server/src/connection/mod.rs | 2 - .../axumkit-server/src/connection/r2_conn.rs | 4 +- .../src/connection/seaweedfs_conn.rs | 184 ----- crates/axumkit-server/src/main.rs | 8 +- crates/axumkit-server/src/repository/mod.rs | 1 - .../src/repository/posts/create.rs | 27 - .../src/repository/posts/delete.rs | 18 - .../src/repository/posts/find_by_author.rs | 20 - .../src/repository/posts/find_by_id.rs | 12 - .../src/repository/posts/get_by_id.rs | 15 - .../src/repository/posts/list.rs | 21 - .../src/repository/posts/mod.rs | 15 - .../src/repository/posts/update.rs | 34 - .../src/service/auth/change_email.rs | 35 +- .../src/service/auth/change_password.rs | 44 +- .../src/service/auth/confirm_email_change.rs | 56 +- .../src/service/auth/forgot_password.rs | 38 +- .../axumkit-server/src/service/auth/login.rs | 21 +- .../axumkit-server/src/service/auth/logout.rs | 9 +- crates/axumkit-server/src/service/auth/mod.rs | 3 +- .../service/auth/resend_verification_email.rs | 35 +- .../src/service/auth/reset_password.rs | 42 +- .../src/service/auth/session.rs | 14 +- .../src/service/auth/session_types.rs | 5 +- .../src/service/auth/totp/backup_codes.rs | 37 +- .../src/service/auth/totp/common.rs | 7 +- .../src/service/auth/totp/disable.rs | 38 +- .../src/service/auth/totp/enable.rs | 43 +- .../src/service/auth/totp/mod.rs | 3 +- .../src/service/auth/totp/setup.rs | 41 +- .../src/service/auth/totp/status.rs | 9 +- .../src/service/auth/totp/temp_token.rs | 36 +- .../src/service/auth/totp/verify.rs | 38 +- .../src/service/auth/verify_email.rs | 60 +- crates/axumkit-server/src/service/mod.rs | 1 - .../src/service/posts/create_post.rs | 44 -- .../src/service/posts/delete_post.rs | 39 - .../src/service/posts/get_post.rs | 33 - .../src/service/posts/list_posts.rs | 24 - .../axumkit-server/src/service/posts/mod.rs | 11 - .../src/service/posts/update_post.rs | 71 -- .../axumkit-server/src/service/search/mod.rs | 2 - .../src/service/search/search_posts.rs | 90 --- crates/axumkit-server/src/state.rs | 3 +- .../axumkit-server/src/utils/crypto/token.rs | 2 +- crates/axumkit-server/src/utils/r2_url.rs | 2 +- .../src/config/worker_config.rs | 25 +- crates/axumkit-worker/src/connection/mod.rs | 2 - .../axumkit-worker/src/connection/r2_conn.rs | 4 +- .../src/connection/seaweedfs_conn.rs | 130 ---- .../src/jobs/cron/cleanup_orphaned_blobs.rs | 118 ---- .../src/jobs/cron/lua/extend_lock.lua | 5 + .../src/jobs/cron/lua/release_lock.lua | 5 + crates/axumkit-worker/src/jobs/cron/mod.rs | 210 ++++-- .../axumkit-worker/src/jobs/cron/sitemap.rs | 201 +----- crates/axumkit-worker/src/jobs/index/mod.rs | 8 - crates/axumkit-worker/src/jobs/index/post.rs | 153 ---- crates/axumkit-worker/src/jobs/mod.rs | 5 +- crates/axumkit-worker/src/jobs/reindex/mod.rs | 10 - .../axumkit-worker/src/jobs/reindex/posts.rs | 193 ----- .../src/jobs/storage/delete_content.rs | 66 -- crates/axumkit-worker/src/jobs/storage/mod.rs | 3 - crates/axumkit-worker/src/lib.rs | 5 +- crates/axumkit-worker/src/main.rs | 28 +- crates/axumkit-worker/src/nats/streams.rs | 12 - crates/e2e/src/helpers/mod.rs | 4 +- crates/e2e/src/helpers/posts.rs | 135 ---- crates/e2e/src/lib.rs | 2 +- crates/migration/src/lib.rs | 2 - .../src/m20250825_033642_create_posts.rs | 79 --- crates/xtask/README.md | 1 - crates/xtask/src/main.rs | 8 - docker-compose.e2e.yml | 62 +- docs/.vitepress/config.mts | 1 - docs/deploy/docker.md | 132 +--- docs/features/posts.md | 91 --- docs/features/search.md | 42 +- docs/features/worker.md | 125 +--- docs/guide/architecture.md | 292 ++------ docs/guide/getting-started.md | 67 +- docs/guide/study-guide.md | 240 ++----- docs/index.md | 8 +- docs/reference/api-endpoints.md | 49 +- docs/reference/environment.md | 287 +++----- docs/reference/error-codes.md | 216 +++--- 173 files changed, 1771 insertions(+), 4269 deletions(-) delete mode 100644 crates/axumkit-dto/src/posts/mod.rs delete mode 100644 crates/axumkit-dto/src/posts/request/create_post.rs delete mode 100644 crates/axumkit-dto/src/posts/request/get_post.rs delete mode 100644 crates/axumkit-dto/src/posts/request/list_posts.rs delete mode 100644 crates/axumkit-dto/src/posts/request/mod.rs delete mode 100644 crates/axumkit-dto/src/posts/request/update_post.rs delete mode 100644 crates/axumkit-dto/src/posts/response/create_post.rs delete mode 100644 crates/axumkit-dto/src/posts/response/delete_post.rs delete mode 100644 crates/axumkit-dto/src/posts/response/list_posts.rs delete mode 100644 crates/axumkit-dto/src/posts/response/mod.rs delete mode 100644 crates/axumkit-dto/src/posts/response/post.rs delete mode 100644 crates/axumkit-dto/src/search/request/posts.rs delete mode 100644 crates/axumkit-dto/src/search/response/posts.rs delete mode 100644 crates/axumkit-entity/src/posts.rs delete mode 100644 crates/axumkit-errors/src/handlers/post_handler.rs delete mode 100644 crates/axumkit-server/src/api/v0/routes/posts/create_post.rs delete mode 100644 crates/axumkit-server/src/api/v0/routes/posts/delete_post.rs delete mode 100644 crates/axumkit-server/src/api/v0/routes/posts/get_post.rs delete mode 100644 crates/axumkit-server/src/api/v0/routes/posts/list_posts.rs delete mode 100644 crates/axumkit-server/src/api/v0/routes/posts/mod.rs delete mode 100644 crates/axumkit-server/src/api/v0/routes/posts/openapi.rs delete mode 100644 crates/axumkit-server/src/api/v0/routes/posts/routes.rs delete mode 100644 crates/axumkit-server/src/api/v0/routes/posts/update_post.rs delete mode 100644 crates/axumkit-server/src/api/v0/routes/search/search_posts.rs delete mode 100644 crates/axumkit-server/src/bridge/worker_client/storage.rs delete mode 100644 crates/axumkit-server/src/connection/seaweedfs_conn.rs delete mode 100644 crates/axumkit-server/src/repository/posts/create.rs delete mode 100644 crates/axumkit-server/src/repository/posts/delete.rs delete mode 100644 crates/axumkit-server/src/repository/posts/find_by_author.rs delete mode 100644 crates/axumkit-server/src/repository/posts/find_by_id.rs delete mode 100644 crates/axumkit-server/src/repository/posts/get_by_id.rs delete mode 100644 crates/axumkit-server/src/repository/posts/list.rs delete mode 100644 crates/axumkit-server/src/repository/posts/mod.rs delete mode 100644 crates/axumkit-server/src/repository/posts/update.rs delete mode 100644 crates/axumkit-server/src/service/posts/create_post.rs delete mode 100644 crates/axumkit-server/src/service/posts/delete_post.rs delete mode 100644 crates/axumkit-server/src/service/posts/get_post.rs delete mode 100644 crates/axumkit-server/src/service/posts/list_posts.rs delete mode 100644 crates/axumkit-server/src/service/posts/mod.rs delete mode 100644 crates/axumkit-server/src/service/posts/update_post.rs delete mode 100644 crates/axumkit-server/src/service/search/search_posts.rs delete mode 100644 crates/axumkit-worker/src/connection/seaweedfs_conn.rs delete mode 100644 crates/axumkit-worker/src/jobs/cron/cleanup_orphaned_blobs.rs create mode 100644 crates/axumkit-worker/src/jobs/cron/lua/extend_lock.lua create mode 100644 crates/axumkit-worker/src/jobs/cron/lua/release_lock.lua delete mode 100644 crates/axumkit-worker/src/jobs/index/post.rs delete mode 100644 crates/axumkit-worker/src/jobs/reindex/posts.rs delete mode 100644 crates/axumkit-worker/src/jobs/storage/delete_content.rs delete mode 100644 crates/axumkit-worker/src/jobs/storage/mod.rs delete mode 100644 crates/e2e/src/helpers/posts.rs delete mode 100644 crates/migration/src/m20250825_033642_create_posts.rs delete mode 100644 docs/features/posts.md diff --git a/.env.example b/.env.example index 16d14fa..7519f4e 100644 --- a/.env.example +++ b/.env.example @@ -62,15 +62,12 @@ GITHUB_REDIRECT_URI= # Cloudflare R2 R2_ENDPOINT= R2_REGION=auto -R2_PUBLIC_DOMAIN= -R2_BUCKET_NAME= +R2_ASSETS_PUBLIC_DOMAIN= +R2_ASSETS_BUCKET_NAME= R2_ACCESS_KEY_ID= R2_SECRET_ACCESS_KEY= TURNSTILE_SECRET_KEY= -# SeaweedFS -SEAWEEDFS_ENDPOINT=http://localhost:8888 - # NATS NATS_URL=nats://localhost:4222 diff --git a/Cargo.lock b/Cargo.lock index ce70719..93443b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "const-random", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -131,9 +133,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "ar_archive_writer" @@ -187,7 +189,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", - "cpufeatures", + "cpufeatures 0.2.17", "password-hash", ] @@ -203,6 +205,165 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "arrow" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4754a624e5ae42081f464514be454b39711daae0458906dacde5f4c632f33a8" +dependencies = [ + "arrow-arith", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-ord", + "arrow-row", + "arrow-schema", + "arrow-select", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7b3141e0ec5145a22d8694ea8b6d6f69305971c4fa1c1a13ef0195aef2d678b" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "num-traits", +] + +[[package]] +name = "arrow-array" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8955af33b25f3b175ee10af580577280b4bd01f7e823d94c7cdef7cf8c9aef" +dependencies = [ + "ahash 0.8.12", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "hashbrown 0.16.1", + "num-complex", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-buffer" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c697ddca96183182f35b3a18e50b9110b11e916d7b7799cbfd4d34662f2c56c2" +dependencies = [ + "bytes", + "half", + "num-bigint", + "num-traits", +] + +[[package]] +name = "arrow-cast" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "646bbb821e86fd57189c10b4fcdaa941deaf4181924917b0daa92735baa6ada5" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-ord", + "arrow-schema", + "arrow-select", + "atoi", + "base64", + "chrono", + "half", + "lexical-core", + "num-traits", + "ryu", +] + +[[package]] +name = "arrow-data" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fdd994a9d28e6365aa78e15da3f3950c0fdcea6b963a12fa1c391afb637b304" +dependencies = [ + "arrow-buffer", + "arrow-schema", + "half", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-ord" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d8f1870e03d4cbed632959498bcc84083b5a24bded52905ae1695bd29da45b" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", +] + +[[package]] +name = "arrow-row" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18228633bad92bff92a95746bbeb16e5fc318e8382b75619dec26db79e4de4c0" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "half", +] + +[[package]] +name = "arrow-schema" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c872d36b7bf2a6a6a2b40de9156265f0242910791db366a2c17476ba8330d68" + +[[package]] +name = "arrow-select" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68bf3e3efbd1278f770d67e5dc410257300b161b93baedb3aae836144edcaf4b" +dependencies = [ + "ahash 0.8.12", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "num-traits", +] + +[[package]] +name = "arrow-string" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e968097061b3c0e9fe3079cf2e703e487890700546b5b0647f60fca1b5a8d8" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "memchr", + "num-traits", + "regex", + "regex-syntax", +] + [[package]] name = "as-slice" version = "0.2.1" @@ -473,9 +634,9 @@ dependencies = [ [[package]] name = "aws-config" -version = "1.8.12" +version = "1.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96571e6996817bf3d58f6b569e4b9fd2e9d2fcf9f7424eed07b2ce9bb87535e5" +checksum = "8a8fc176d53d6fe85017f230405e3255cedb4a02221cb55ed6d76dccbbb099b2" dependencies = [ "aws-credential-types", "aws-runtime", @@ -503,9 +664,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.11" +version = "1.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd362783681b15d136480ad555a099e82ecd8e2d10a841e14dfd0078d67fee3" +checksum = "6d203b0bf2626dcba8665f5cd0871d7c2c0930223d6b6be9097592fea21242d0" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -538,9 +699,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.18" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "959dab27ce613e6c9658eb3621064d0e2027e5f2acb65bc526a43577facea557" +checksum = "ede2ddc593e6c8acc6ce3358c28d6677a6dc49b65ba4b37a2befe14a11297e75" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -552,9 +713,12 @@ dependencies = [ "aws-smithy-types", "aws-types", "bytes", + "bytes-utils", "fastrand", "http 0.2.12", + "http 1.4.0", "http-body 0.4.6", + "http-body 1.0.1", "percent-encoding", "pin-project-lite", "tracing", @@ -563,9 +727,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.121.0" +version = "1.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61948728b681f88a1e49b9500469cf9e36575a424e745e2c5a651a42386e7d9c" +checksum = "744c09d75dfec039a05cf8e117c995ded3b0baffa6eb83f3ed7075a01d8d8947" dependencies = [ "aws-credential-types", "aws-runtime", @@ -587,7 +751,7 @@ dependencies = [ "hmac", "http 0.2.12", "http 1.4.0", - "http-body 0.4.6", + "http-body 1.0.1", "lru", "percent-encoding", "regex-lite", @@ -598,9 +762,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.92.0" +version = "1.95.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d63bd2bdeeb49aa3f9b00c15e18583503b778b2e792fc06284d54e7d5b6566" +checksum = "00c5ff27c6ba2cbd95e6e26e2e736676fdf6bcf96495b187733f521cfe4ce448" dependencies = [ "aws-credential-types", "aws-runtime", @@ -615,15 +779,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ssooidc" -version = "1.94.0" +version = "1.97.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532d93574bf731f311bafb761366f9ece345a0416dbcc273d81d6d1a1205239b" +checksum = "4d186f1e5a3694a188e5a0640b3115ccc6e084d104e16fd6ba968dca072ffef8" dependencies = [ "aws-credential-types", "aws-runtime", @@ -638,15 +803,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "1.96.0" +version = "1.99.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357e9a029c7524db6a0099cd77fbd5da165540339e7296cca603531bc783b56c" +checksum = "9acba7c62f3d4e2408fa998a3a8caacd8b9a5b5549cf36e2372fbdae329d5449" dependencies = [ "aws-credential-types", "aws-runtime", @@ -662,15 +828,16 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sigv4" -version = "1.3.7" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e523e1c4e8e7e8ff219d732988e22bfeae8a1cafdbe6d9eca1546fa080be7c" +checksum = "37411f8e0f4bea0c3ca0958ce7f18f6439db24d555dbd809787262cd00926aa9" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -696,9 +863,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.10" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e39f47abe8641e434de98e047e85ced629862e7ab719b6914a846796ceb289e2" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" dependencies = [ "futures-util", "pin-project-lite", @@ -707,17 +874,18 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.63.13" +version = "0.64.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23374b9170cbbcc6f5df8dc5ebb9b6c5c28a3c8f599f0e8b8b10eb6f4a5c6e74" +checksum = "6750f3dd509b0694a4377f0293ed2f9630d710b1cebe281fa8bac8f099f88bc6" dependencies = [ "aws-smithy-http", "aws-smithy-types", "bytes", "crc-fast", "hex", - "http 0.2.12", - "http-body 0.4.6", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", "md-5", "pin-project-lite", "sha1", @@ -727,9 +895,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.17" +version = "0.60.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6a1db93c9aaf8c8e5d97f055e5fa01a782bd5bdc9c4042b7e18090503b12a7" +checksum = "faf09d74e5e32f76b8762da505a3cd59303e367a664ca67295387baa8c1d7548" dependencies = [ "aws-smithy-types", "bytes", @@ -738,9 +906,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.62.6" +version = "0.63.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b" +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -749,9 +917,9 @@ dependencies = [ "bytes-utils", "futures-core", "futures-util", - "http 0.2.12", "http 1.4.0", - "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", "percent-encoding", "pin-project-lite", "pin-utils", @@ -760,9 +928,9 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.1.8" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a395c914b1ff95db3cb7003ea4fd19432343af698a9b5028a7d35f8e712240a1" +checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -790,27 +958,27 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.61.9" +version = "0.62.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551" +checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a" dependencies = [ "aws-smithy-types", ] [[package]] name = "aws-smithy-observability" -version = "0.2.3" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7764bc1dfdb71157bc481528a649e617ed8c9c8aa93c0e8b01087133677cfc8e" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-query" -version = "0.60.12" +version = "0.60.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786bbf4434ed3e3413c5a1741abf53a0372dd48124ddb2091e6bff1b1b2582b5" +checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" dependencies = [ "aws-smithy-types", "urlencoding", @@ -818,9 +986,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.9.8" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb5b6167fcdf47399024e81ac08e795180c576a20e4d4ce67949f9a88ae37dc1" +checksum = "028999056d2d2fd58a697232f9eec4a643cf73a71cf327690a7edad1d2af2110" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -834,6 +1002,7 @@ dependencies = [ "http 1.4.0", "http-body 0.4.6", "http-body 1.0.1", + "http-body-util", "pin-project-lite", "pin-utils", "tokio", @@ -842,9 +1011,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.11.2" +version = "1.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b743e9aab0b8d50a9a40eebedf974fcfe3621032e07c6388d1c7821b155b7b0" +checksum = "876ab3c9c29791ba4ba02b780a3049e21ec63dabda09268b175272c3733a79e6" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -859,9 +1028,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0828575b70da70406b4cdb5d4afe0afe725f72245f04d34f02e0fb5ebd6fc872" +checksum = "d2b1117b3b2bbe166d11199b540ceed0d0f7676e36e7b962b5a437a9971eac75" dependencies = [ "base64-simd", "bytes", @@ -885,18 +1054,18 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.13" +version = "0.60.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11b2f670422ff42bf7065031e72b45bc52a3508bd089f743ea90731ca2b6ea57" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.11" +version = "1.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d980627d2dd7bfc32a3c025685a033eeab8d365cc840c631ef59d1b8f428164" +checksum = "0470cc047657c6e286346bdf10a8719d26efd6a91626992e0e64481e44323e96" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -1085,7 +1254,7 @@ dependencies = [ "infer", "meilisearch-sdk", "oauth2", - "rand 0.9.2", + "rand 0.10.0", "redis", "reqwest", "sea-orm", @@ -1253,7 +1422,7 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq 0.4.2", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -1355,9 +1524,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -1407,11 +1576,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -1531,6 +1711,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -1625,6 +1825,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.3.0" @@ -1760,7 +1969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -2316,9 +2525,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -2331,9 +2540,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -2341,15 +2550,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -2369,9 +2578,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -2388,9 +2597,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -2399,21 +2608,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -2423,7 +2632,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -2459,11 +2667,25 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + [[package]] name = "gif" version = "0.14.1" @@ -2549,6 +2771,7 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "num-traits", "zerocopy", ] @@ -2988,6 +3211,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -3215,6 +3444,12 @@ dependencies = [ "spin 0.9.8", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lebe" version = "0.5.3" @@ -3249,6 +3484,63 @@ dependencies = [ "url", ] +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + [[package]] name = "libc" version = "0.2.180" @@ -3470,9 +3762,9 @@ dependencies = [ [[package]] name = "minijinja" -version = "2.15.1" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b479616bb6f0779fb0f3964246beda02d4b01144e1b0d5519616e012ccc2a245" +checksum = "65ab6f50e4e8fb40bd21f527066bd019f5b029035b4e5ac9b9f9ba526c6bd87b" dependencies = [ "memo-map", "self_cell", @@ -3641,6 +3933,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.0" @@ -4081,6 +4382,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.114", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -4316,6 +4627,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -4343,6 +4660,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.0", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -4381,6 +4709,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "rav1e" version = "0.8.1" @@ -4453,9 +4787,9 @@ dependencies = [ [[package]] name = "redis" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e969d1d702793536d5fda739a82b88ad7cbe7d04f8386ee8cd16ad3eff4854a5" +checksum = "dbe7f6e08ce1c6a9b21684e643926f6fc3b683bc006cb89afd72a5e0eb16e3a2" dependencies = [ "arc-swap", "arcstr", @@ -4914,9 +5248,9 @@ dependencies = [ [[package]] name = "sea-orm" -version = "2.0.0-rc.30" +version = "2.0.0-rc.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4bb965a287ae073c738851c5d38037ac6da66c9841ac1de7c13c8d08862180a" +checksum = "41c847ddcc05ba1c4a8ad6bf781ac8d3750123f6e95642c2268b0551101931de" dependencies = [ "async-stream", "async-trait", @@ -4930,6 +5264,7 @@ dependencies = [ "ouroboros", "pgvector", "rust_decimal", + "sea-orm-arrow", "sea-orm-macros", "sea-query", "sea-query-sqlx", @@ -4945,6 +5280,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "sea-orm-arrow" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2eee8405f16c1f337fe3a83389361caea83c928d14dbd666a480407072c365" +dependencies = [ + "arrow", + "sea-query", + "thiserror 2.0.18", +] + [[package]] name = "sea-orm-cli" version = "2.0.0-rc.30" @@ -4967,9 +5313,9 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "2.0.0-rc.30" +version = "2.0.0-rc.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e208f041129ad7962b6951f0b392e9ff97a8337bd8c7022c61e7b02ab29fe0" +checksum = "a172f9ceaceafa0fe40f6f15df0231750755284c80bd8ba98d10d5ae611ecaf0" dependencies = [ "heck 0.5.0", "itertools", @@ -4999,9 +5345,9 @@ dependencies = [ [[package]] name = "sea-query" -version = "1.0.0-rc.30" +version = "1.0.0-rc.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6a067a2f6f13250f615f0bedb5bc3a6c872fec70776d0b43b43caeaa699e232" +checksum = "58decdaaaf2a698170af2fa1b2e8f7b43a970e7768bf18aebaab113bada46354" dependencies = [ "chrono", "inherent", @@ -5237,7 +5583,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -5254,7 +5600,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -5904,6 +6250,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -5931,9 +6286,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -6404,11 +6759,11 @@ dependencies = [ [[package]] name = "uuid" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -6519,6 +6874,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasite" version = "0.1.0" @@ -6584,6 +6948,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -6597,6 +6983,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" version = "0.3.85" @@ -6975,6 +7373,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.114", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.114", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" diff --git a/Cargo.toml b/Cargo.toml index a86d658..47400ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ axumkit_worker = { path = "crates/axumkit-worker" } axumkit_server = { path = "crates/axumkit-server" } # External dependencies -tokio = { version = "1.49.0", features = ["full"] } +tokio = { version = "1.50.0", features = ["full"] } axum = { version = "0.8.8", features = ["multipart", "macros"] } axum-extra = { version = "0.12.5", features = ["typed-header", "query"] } tower = { version = "0.5.3", features = ["timeout", "limit", "buffer"] } @@ -29,15 +29,15 @@ tower-http = { version = "0.6.8", features = ["cors", "auth", "trace", "request- tower-cookies = "0.11.0" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" -sea-orm = { version = "2.0.0-rc.30", features = ["postgres-array", "sqlx-postgres", "runtime-tokio-native-tls", "with-ipnetwork"] } -redis = { version = "1.0.3", features = ["tokio-comp", "tokio-native-tls-comp", "connection-manager"] } +sea-orm = { version = "2.0.0-rc.35", features = ["postgres-array", "sqlx-postgres", "runtime-tokio-native-tls", "with-ipnetwork"] } +redis = { version = "1.0.4", features = ["tokio-comp", "tokio-native-tls-comp", "connection-manager"] } tokio-cron-scheduler = "0.15.1" argon2 = "0.5.3" oauth2 = { version = "5.0.0", features = ["reqwest"] } totp-rs = { version = "5.7.0", features = ["qr"] } cookie = "0.18.1" -aws-sdk-s3 = "1.121.0" -aws-config = { version = "1.8.12", features = ["behavior-version-latest"] } +aws-sdk-s3 = "1.124.0" +aws-config = { version = "1.8.14", features = ["behavior-version-latest"] } meilisearch-sdk = { version = "0.32.0" } utoipa = { version = "5.4.0", features = ["axum_extras", "openapi_extensions", "time", "uuid", "chrono"] } utoipa-swagger-ui = { version = "9.0.2", features = ["axum", "debug-embed"] } @@ -46,12 +46,12 @@ tracing = "0.1.44" tracing-subscriber = { version = "0.3.22", features = ["json", "env-filter"] } tracing-appender = "0.2.4" reqwest = { version = "0.12.28", features = ["json", "multipart"] } -anyhow = "1.0.100" -chrono = { version = "0.4.43", features = ["serde"] } +anyhow = "1.0.102" +chrono = { version = "0.4.44", features = ["serde"] } chrono-tz = "0.10.4" -uuid = { version = "1.20.0", features = ["v7", "v4"] } +uuid = { version = "1.21.0", features = ["v7", "v4"] } dotenvy = "0.15.7" -rand = "0.9.2" +rand = "0.10.0" hex = "0.4.3" base64 = "0.22.1" image = { version = "0.25.9", features = ["webp"] } @@ -60,9 +60,9 @@ blake3 = "1.8.3" zstd = "0.13.3" lettre = { version = "0.11.19", features = ["tokio1-native-tls"] } mrml = "5.1.0" -minijinja = { version = "2.15.1", features = ["loader"] } +minijinja = { version = "2.17.0", features = ["loader"] } similar = "2.7.0" -futures = "0.3.31" +futures = "0.3.32" async-nats = "0.46.0" tokio-stream = { version = "0.1.18", features = ["sync"] } sitemap-rs = "0.4.0" diff --git a/charts/README.md b/charts/README.md index fd8599e..7b8845e 100644 --- a/charts/README.md +++ b/charts/README.md @@ -129,7 +129,6 @@ appVersion: "0.4.0" | redis | 24.1.2 | bitnami | | nats | 2.12.3 | nats-io | | meilisearch | 0.21.0 | meilisearch | -| seaweedfs | 4.0.407 | seaweedfs | ## Troubleshooting diff --git a/charts/axumkit-server/values.yaml b/charts/axumkit-server/values.yaml index e1ec2fd..dc16c0f 100644 --- a/charts/axumkit-server/values.yaml +++ b/charts/axumkit-server/values.yaml @@ -145,9 +145,6 @@ secret: # NATS (optional, default: nats://localhost:4222) NATS_URL: "" - # SeaweedFS (required) - SEAWEEDFS_ENDPOINT: "" - # Google OAuth (required) GOOGLE_CLIENT_ID: "" GOOGLE_CLIENT_SECRET: "" @@ -161,8 +158,8 @@ secret: # Cloudflare R2 (required) R2_ENDPOINT: "" R2_REGION: "auto" - R2_PUBLIC_DOMAIN: "" - R2_BUCKET_NAME: "" + R2_ASSETS_PUBLIC_DOMAIN: "" + R2_ASSETS_BUCKET_NAME: "" R2_ACCESS_KEY_ID: "" R2_SECRET_ACCESS_KEY: "" diff --git a/charts/axumkit-worker/values.yaml b/charts/axumkit-worker/values.yaml index 8f3f714..8b7f5c3 100644 --- a/charts/axumkit-worker/values.yaml +++ b/charts/axumkit-worker/values.yaml @@ -104,14 +104,11 @@ secret: # NATS (optional, default: nats://localhost:4222) NATS_URL: "" - # SeaweedFS (required) - SEAWEEDFS_ENDPOINT: "" - # Cloudflare R2 (required for sitemap) R2_ENDPOINT: "" R2_REGION: "auto" - R2_PUBLIC_DOMAIN: "" - R2_BUCKET_NAME: "" + R2_ASSETS_PUBLIC_DOMAIN: "" + R2_ASSETS_BUCKET_NAME: "" R2_ACCESS_KEY_ID: "" R2_SECRET_ACCESS_KEY: "" diff --git a/charts/axumkit/Chart.yaml b/charts/axumkit/Chart.yaml index 2013fe1..9849980 100644 --- a/charts/axumkit/Chart.yaml +++ b/charts/axumkit/Chart.yaml @@ -62,8 +62,3 @@ dependencies: version: "0.21.0" repository: "https://meilisearch.github.io/meilisearch-kubernetes" condition: meilisearch.enabled - - - name: seaweedfs - version: "4.0.407" - repository: "https://seaweedfs.github.io/seaweedfs/helm" - condition: seaweedfs.enabled diff --git a/charts/axumkit/values-local.yaml.example b/charts/axumkit/values-local.yaml.example index 819f61e..581ca96 100644 --- a/charts/axumkit/values-local.yaml.example +++ b/charts/axumkit/values-local.yaml.example @@ -29,8 +29,8 @@ axumkit-server: GITHUB_REDIRECT_URI: "http://localhost:8000/v0/auth/github/callback" R2_ENDPOINT: "" - R2_PUBLIC_DOMAIN: "" - R2_BUCKET_NAME: "" + R2_ASSETS_PUBLIC_DOMAIN: "" + R2_ASSETS_BUCKET_NAME: "" R2_ACCESS_KEY_ID: "" R2_SECRET_ACCESS_KEY: "" @@ -55,8 +55,8 @@ axumkit-worker: POSTGRES_WRITE_PASSWORD: "axumkit_secret" R2_ENDPOINT: "" - R2_PUBLIC_DOMAIN: "" - R2_BUCKET_NAME: "" + R2_ASSETS_PUBLIC_DOMAIN: "" + R2_ASSETS_BUCKET_NAME: "" R2_ACCESS_KEY_ID: "" R2_SECRET_ACCESS_KEY: "" diff --git a/charts/axumkit/values.yaml b/charts/axumkit/values.yaml index 1160396..b2d32d9 100644 --- a/charts/axumkit/values.yaml +++ b/charts/axumkit/values.yaml @@ -38,7 +38,6 @@ axumkit-server: POSTGRES_READ_PASSWORD: "" NATS_URL: "nats://axumkit-nats:4222" - SEAWEEDFS_ENDPOINT: "http://axumkit-seaweedfs-filer:8888" GOOGLE_CLIENT_ID: "" GOOGLE_CLIENT_SECRET: "" @@ -50,8 +49,8 @@ axumkit-server: R2_ENDPOINT: "" R2_REGION: "auto" - R2_PUBLIC_DOMAIN: "" - R2_BUCKET_NAME: "" + R2_ASSETS_PUBLIC_DOMAIN: "" + R2_ASSETS_BUCKET_NAME: "" R2_ACCESS_KEY_ID: "" R2_SECRET_ACCESS_KEY: "" @@ -92,12 +91,11 @@ axumkit-worker: POSTGRES_WRITE_PASSWORD: "" NATS_URL: "nats://axumkit-nats:4222" - SEAWEEDFS_ENDPOINT: "http://axumkit-seaweedfs-filer:8888" R2_ENDPOINT: "" R2_REGION: "auto" - R2_PUBLIC_DOMAIN: "" - R2_BUCKET_NAME: "" + R2_ASSETS_PUBLIC_DOMAIN: "" + R2_ASSETS_BUCKET_NAME: "" R2_ACCESS_KEY_ID: "" R2_SECRET_ACCESS_KEY: "" @@ -183,22 +181,3 @@ meilisearch: enabled: true size: 10Gi -# ============================================================================= -# SeaweedFS -# ============================================================================= -seaweedfs: - enabled: true - - master: - replicas: 1 - storage: 1Gi - - volume: - replicas: 1 - storage: 50Gi - - filer: - replicas: 1 - storage: 1Gi - s3: - enabled: true diff --git a/crates/axumkit-config/src/server_config.rs b/crates/axumkit-config/src/server_config.rs index 44cba34..42cd82f 100644 --- a/crates/axumkit-config/src/server_config.rs +++ b/crates/axumkit-config/src/server_config.rs @@ -27,30 +27,33 @@ pub struct ServerConfig { pub github_client_secret: String, pub github_redirect_uri: String, - // Cloudflare + // Cloudflare R2 (shared credentials) pub r2_endpoint: String, pub r2_region: String, - pub r2_public_domain: String, - pub r2_bucket_name: String, pub r2_access_key_id: String, pub r2_secret_access_key: String, + // R2 Assets (public bucket - images, sitemap) + pub r2_assets_public_domain: String, + pub r2_assets_bucket_name: String, + + // Cloudflare Turnstile pub turnstile_secret_key: String, // Write DB (Primary) + pub db_write_user: String, + pub db_write_password: String, pub db_write_host: String, pub db_write_port: String, pub db_write_name: String, - pub db_write_user: String, - pub db_write_password: String, pub db_write_max_connection: u32, pub db_write_min_connection: u32, - // Read DB (Replica) + // Read DB (Replica) - defaults to write DB if not set + pub db_read_user: String, + pub db_read_password: String, pub db_read_host: String, pub db_read_port: String, pub db_read_name: String, - pub db_read_user: String, - pub db_read_password: String, pub db_read_max_connection: u32, pub db_read_min_connection: u32, @@ -73,14 +76,11 @@ pub struct ServerConfig { pub meilisearch_host: String, pub meilisearch_api_key: Option, - // SeaweedFS (revision content storage) - pub seaweedfs_endpoint: String, - pub cors_allowed_origins: Vec, pub cors_allowed_headers: Vec, pub cors_max_age: Option, - // Cookie Domain (e.g., ".example.com" for cross-subdomain cookies) + // Cookie Domain (e.g., ".seven.wiki" for cross-subdomain cookies) pub cookie_domain: Option, // Stability Layer (protect DB pool from overload) @@ -95,6 +95,7 @@ static CONFIG: LazyLock = LazyLock::new(|| { let mut errors: Vec = Vec::new(); + // 필수 환경변수 (누락 시 에러 수집, panic하지 않음) macro_rules! require { ($name:expr) => { env::var($name).unwrap_or_else(|_| { @@ -104,6 +105,7 @@ static CONFIG: LazyLock = LazyLock::new(|| { }; } + // 필수 환경변수 + 파싱 (누락/파싱 실패 시 에러 수집) macro_rules! require_parse { ($name:expr, $ty:ty) => {{ match env::var($name) { @@ -187,10 +189,10 @@ static CONFIG: LazyLock = LazyLock::new(|| { let github_redirect_uri = require!("GITHUB_REDIRECT_URI"); let r2_endpoint = require!("R2_ENDPOINT"); let r2_region = require!("R2_REGION"); - let r2_public_domain = require!("R2_PUBLIC_DOMAIN"); - let r2_bucket_name = require!("R2_BUCKET_NAME"); let r2_access_key_id = require!("R2_ACCESS_KEY_ID"); let r2_secret_access_key = require!("R2_SECRET_ACCESS_KEY"); + let r2_assets_public_domain = require!("R2_ASSETS_PUBLIC_DOMAIN"); + let r2_assets_bucket_name = require!("R2_ASSETS_BUCKET_NAME"); let turnstile_secret_key = require!("TURNSTILE_SECRET_KEY"); let db_write_host = require!("POSTGRES_WRITE_HOST"); let db_write_port = require!("POSTGRES_WRITE_PORT"); @@ -204,7 +206,6 @@ static CONFIG: LazyLock = LazyLock::new(|| { let db_read_password = require!("POSTGRES_READ_PASSWORD"); let server_host = require!("HOST"); let server_port = require!("PORT"); - let seaweedfs_endpoint = require!("SEAWEEDFS_ENDPOINT"); // Required parsed vars let auth_session_max_lifetime_hours = require_parse!("AUTH_SESSION_MAX_LIFETIME_HOURS", i64); @@ -224,28 +225,32 @@ static CONFIG: LazyLock = LazyLock::new(|| { is_dev, totp_secret, - auth_session_max_lifetime_hours, - auth_session_sliding_ttl_hours, + auth_session_max_lifetime_hours: auth_session_max_lifetime_hours.max(0), + auth_session_sliding_ttl_hours: auth_session_sliding_ttl_hours.max(0), auth_session_refresh_threshold, auth_email_verification_token_expire_time: env::var( "AUTH_EMAIL_VERIFICATION_TOKEN_EXPIRE_TIME", ) .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(1), // 기본값 1시간 (minutes) + .and_then(|v| v.parse::().ok()) + .unwrap_or(1) + .max(0), // 기본값 1시간 (minutes) auth_password_reset_token_expire_time: env::var("AUTH_PASSWORD_RESET_TOKEN_EXPIRE_TIME") .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(15), // 기본값 15분 + .and_then(|v| v.parse::().ok()) + .unwrap_or(15) + .max(0), // 기본값 15분 auth_email_change_token_expire_time: env::var("AUTH_EMAIL_CHANGE_TOKEN_EXPIRE_TIME") .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(15), // 기본값 15분 + .and_then(|v| v.parse::().ok()) + .unwrap_or(15) + .max(0), // 기본값 15분 oauth_pending_signup_ttl_minutes: env::var("OAUTH_PENDING_SIGNUP_TTL_MINUTES") .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(10), // 기본값 10분 + .and_then(|v| v.parse::().ok()) + .unwrap_or(10) + .max(0), // 기본값 10분 // Google google_client_id, @@ -257,13 +262,16 @@ static CONFIG: LazyLock = LazyLock::new(|| { github_client_secret, github_redirect_uri, - // Cloudflare + // Cloudflare R2 (shared credentials) r2_endpoint, r2_region, - r2_public_domain, - r2_bucket_name, r2_access_key_id, r2_secret_access_key, + // R2 Assets (public bucket) + r2_assets_public_domain, + r2_assets_bucket_name, + + // Cloudflare Turnstile turnstile_secret_key, // Write DB (Primary) @@ -275,11 +283,11 @@ static CONFIG: LazyLock = LazyLock::new(|| { db_write_max_connection: env::var("POSTGRES_WRITE_MAX_CONNECTION") .ok() .and_then(|v| v.parse().ok()) - .unwrap_or(100), + .unwrap_or(20), db_write_min_connection: env::var("POSTGRES_WRITE_MIN_CONNECTION") .ok() .and_then(|v| v.parse().ok()) - .unwrap_or(10), + .unwrap_or(5), // Read DB (Replica) db_read_host, @@ -290,11 +298,11 @@ static CONFIG: LazyLock = LazyLock::new(|| { db_read_max_connection: env::var("POSTGRES_READ_MAX_CONNECTION") .ok() .and_then(|v| v.parse().ok()) - .unwrap_or(100), + .unwrap_or(30), db_read_min_connection: env::var("POSTGRES_READ_MIN_CONNECTION") .ok() .and_then(|v| v.parse().ok()) - .unwrap_or(10), + .unwrap_or(5), // Redis Session redis_session_host: env::var("REDIS_SESSION_HOST") @@ -329,9 +337,6 @@ static CONFIG: LazyLock = LazyLock::new(|| { cookie_domain: env::var("COOKIE_DOMAIN").ok().filter(|d| !d.is_empty()), - // SeaweedFS - seaweedfs_endpoint, - // Stability Layer stability_concurrency_limit: env::var("STABILITY_CONCURRENCY_LIMIT") .ok() diff --git a/crates/axumkit-constants/src/cache_keys.rs b/crates/axumkit-constants/src/cache_keys.rs index 6c8c875..738791e 100644 --- a/crates/axumkit-constants/src/cache_keys.rs +++ b/crates/axumkit-constants/src/cache_keys.rs @@ -30,3 +30,30 @@ pub fn oauth_pending_key(token: &str) -> String { pub fn oauth_pending_lock_key(token: &str) -> String { format!("{}{}", OAUTH_PENDING_LOCK_PREFIX, token) } + +/// Email verification token prefix. +/// Format: "email_verification:{token}" +pub const EMAIL_VERIFICATION_PREFIX: &str = "email_verification:"; + +/// Password reset token prefix. +/// Format: "password_reset:{token}" +pub const PASSWORD_RESET_PREFIX: &str = "password_reset:"; + +/// Email change token prefix. +/// Format: "email_change:{token}" +pub const EMAIL_CHANGE_PREFIX: &str = "email_change:"; + +/// Build email verification key. +pub fn email_verification_key(token: &str) -> String { + format!("{}{}", EMAIL_VERIFICATION_PREFIX, token) +} + +/// Build password reset key. +pub fn password_reset_key(token: &str) -> String { + format!("{}{}", PASSWORD_RESET_PREFIX, token) +} + +/// Build email change key. +pub fn email_change_key(token: &str) -> String { + format!("{}{}", EMAIL_CHANGE_PREFIX, token) +} diff --git a/crates/axumkit-constants/src/lib.rs b/crates/axumkit-constants/src/lib.rs index 979468f..4709348 100644 --- a/crates/axumkit-constants/src/lib.rs +++ b/crates/axumkit-constants/src/lib.rs @@ -7,11 +7,12 @@ pub use action_log_actions::{ action_log_action_to_string, string_to_action_log_action, ActionLogAction, }; pub use cache_keys::{ - oauth_pending_key, oauth_pending_lock_key, oauth_state_key, OAUTH_PENDING_LOCK_PREFIX, - OAUTH_PENDING_PREFIX, OAUTH_STATE_PREFIX, OAUTH_STATE_TTL_SECONDS, + email_change_key, email_verification_key, oauth_pending_key, oauth_pending_lock_key, + oauth_state_key, password_reset_key, EMAIL_CHANGE_PREFIX, EMAIL_VERIFICATION_PREFIX, + OAUTH_PENDING_LOCK_PREFIX, OAUTH_PENDING_PREFIX, OAUTH_STATE_PREFIX, OAUTH_STATE_TTL_SECONDS, + PASSWORD_RESET_PREFIX, }; pub use nats_subjects::REALTIME_EVENTS_SUBJECT; pub use storage_keys::{ - user_image_key, BANNER_IMAGE_MAX_SIZE, POST_CONTENT_PREFIX, PROFILE_IMAGE_MAX_SIZE, - USER_IMAGES_PREFIX, + user_image_key, BANNER_IMAGE_MAX_SIZE, PROFILE_IMAGE_MAX_SIZE, USER_IMAGES_PREFIX, }; diff --git a/crates/axumkit-constants/src/storage_keys.rs b/crates/axumkit-constants/src/storage_keys.rs index 20344c3..d648548 100644 --- a/crates/axumkit-constants/src/storage_keys.rs +++ b/crates/axumkit-constants/src/storage_keys.rs @@ -1,8 +1,5 @@ //! R2 storage key prefixes and image size limits -/// Prefix for post content in SeaweedFS -pub const POST_CONTENT_PREFIX: &str = "posts"; - /// Prefix for user profile/banner images pub const USER_IMAGES_PREFIX: &str = "user-images"; diff --git a/crates/axumkit-dto/src/lib.rs b/crates/axumkit-dto/src/lib.rs index 8e23892..fb145a6 100644 --- a/crates/axumkit-dto/src/lib.rs +++ b/crates/axumkit-dto/src/lib.rs @@ -6,7 +6,6 @@ pub mod action_logs; pub mod auth; pub mod oauth; pub mod pagination; -pub mod posts; pub mod search; pub mod user; pub mod validator; diff --git a/crates/axumkit-dto/src/posts/mod.rs b/crates/axumkit-dto/src/posts/mod.rs deleted file mode 100644 index ab5f0b0..0000000 --- a/crates/axumkit-dto/src/posts/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod request; -pub mod response; - -pub use request::{CreatePostRequest, GetPostPath, ListPostsQuery, UpdatePostRequest}; -pub use response::{ - CreatePostResponse, DeletePostResponse, ListPostsResponse, PostListItem, PostResponse, -}; diff --git a/crates/axumkit-dto/src/posts/request/create_post.rs b/crates/axumkit-dto/src/posts/request/create_post.rs deleted file mode 100644 index 599377c..0000000 --- a/crates/axumkit-dto/src/posts/request/create_post.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::validator::string_validator::validate_not_blank; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; -use validator::Validate; - -#[derive(Debug, Deserialize, Serialize, Validate, ToSchema)] -pub struct CreatePostRequest { - #[validate(length( - min = 1, - max = 200, - message = "Title must be between 1 and 200 characters" - ))] - #[validate(custom(function = "validate_not_blank"))] - pub title: String, - #[validate(length( - min = 1, - max = 40000, - message = "Content must be between 1 and 40000 characters" - ))] - #[validate(custom(function = "validate_not_blank"))] - pub content: String, -} diff --git a/crates/axumkit-dto/src/posts/request/get_post.rs b/crates/axumkit-dto/src/posts/request/get_post.rs deleted file mode 100644 index 77a65c3..0000000 --- a/crates/axumkit-dto/src/posts/request/get_post.rs +++ /dev/null @@ -1,10 +0,0 @@ -use serde::Deserialize; -use utoipa::{IntoParams, ToSchema}; -use uuid::Uuid; -use validator::Validate; - -#[derive(Debug, Deserialize, ToSchema, IntoParams, Validate)] -#[into_params(parameter_in = Path)] -pub struct GetPostPath { - pub id: Uuid, -} diff --git a/crates/axumkit-dto/src/posts/request/list_posts.rs b/crates/axumkit-dto/src/posts/request/list_posts.rs deleted file mode 100644 index 435d886..0000000 --- a/crates/axumkit-dto/src/posts/request/list_posts.rs +++ /dev/null @@ -1,8 +0,0 @@ -use serde::Deserialize; -use utoipa::{IntoParams, ToSchema}; - -#[derive(Debug, Deserialize, ToSchema, IntoParams)] -pub struct ListPostsQuery { - pub limit: u64, - pub offset: u64, -} diff --git a/crates/axumkit-dto/src/posts/request/mod.rs b/crates/axumkit-dto/src/posts/request/mod.rs deleted file mode 100644 index 6d7e71a..0000000 --- a/crates/axumkit-dto/src/posts/request/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod create_post; -mod get_post; -mod list_posts; -mod update_post; - -pub use create_post::CreatePostRequest; -pub use get_post::GetPostPath; -pub use list_posts::ListPostsQuery; -pub use update_post::UpdatePostRequest; diff --git a/crates/axumkit-dto/src/posts/request/update_post.rs b/crates/axumkit-dto/src/posts/request/update_post.rs deleted file mode 100644 index 4bd008f..0000000 --- a/crates/axumkit-dto/src/posts/request/update_post.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::validator::string_validator::validate_not_blank; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; -use validator::Validate; - -#[derive(Debug, Deserialize, Serialize, Validate, ToSchema)] -pub struct UpdatePostRequest { - #[validate(length( - min = 1, - max = 200, - message = "Title must be between 1 and 200 characters" - ))] - #[validate(custom(function = "validate_not_blank"))] - pub title: Option, - #[validate(length( - min = 1, - max = 40000, - message = "Content must be between 1 and 40000 characters" - ))] - #[validate(custom(function = "validate_not_blank"))] - pub content: Option, -} diff --git a/crates/axumkit-dto/src/posts/response/create_post.rs b/crates/axumkit-dto/src/posts/response/create_post.rs deleted file mode 100644 index 4e35064..0000000 --- a/crates/axumkit-dto/src/posts/response/create_post.rs +++ /dev/null @@ -1,18 +0,0 @@ -use axum::{ - Json, - http::StatusCode, - response::{IntoResponse, Response}, -}; -use serde::Serialize; -use utoipa::ToSchema; - -#[derive(Debug, Serialize, ToSchema)] -pub struct CreatePostResponse { - pub id: String, -} - -impl IntoResponse for CreatePostResponse { - fn into_response(self) -> Response { - (StatusCode::CREATED, Json(self)).into_response() - } -} diff --git a/crates/axumkit-dto/src/posts/response/delete_post.rs b/crates/axumkit-dto/src/posts/response/delete_post.rs deleted file mode 100644 index 6c6cda9..0000000 --- a/crates/axumkit-dto/src/posts/response/delete_post.rs +++ /dev/null @@ -1,18 +0,0 @@ -use axum::{ - Json, - http::StatusCode, - response::{IntoResponse, Response}, -}; -use serde::Serialize; -use utoipa::ToSchema; - -#[derive(Debug, Serialize, ToSchema)] -pub struct DeletePostResponse { - pub success: bool, -} - -impl IntoResponse for DeletePostResponse { - fn into_response(self) -> Response { - (StatusCode::OK, Json(self)).into_response() - } -} diff --git a/crates/axumkit-dto/src/posts/response/list_posts.rs b/crates/axumkit-dto/src/posts/response/list_posts.rs deleted file mode 100644 index a7066e4..0000000 --- a/crates/axumkit-dto/src/posts/response/list_posts.rs +++ /dev/null @@ -1,28 +0,0 @@ -use axum::{ - Json, - http::StatusCode, - response::{IntoResponse, Response}, -}; -use chrono::{DateTime, Utc}; -use serde::Serialize; -use utoipa::ToSchema; - -#[derive(Debug, Serialize, ToSchema)] -pub struct PostListItem { - pub id: String, - pub author_id: String, - pub title: String, - pub created_at: DateTime, - pub updated_at: DateTime, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct ListPostsResponse { - pub posts: Vec, -} - -impl IntoResponse for ListPostsResponse { - fn into_response(self) -> Response { - (StatusCode::OK, Json(self)).into_response() - } -} diff --git a/crates/axumkit-dto/src/posts/response/mod.rs b/crates/axumkit-dto/src/posts/response/mod.rs deleted file mode 100644 index 0f9340e..0000000 --- a/crates/axumkit-dto/src/posts/response/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod create_post; -mod delete_post; -mod list_posts; -mod post; - -pub use create_post::CreatePostResponse; -pub use delete_post::DeletePostResponse; -pub use list_posts::{ListPostsResponse, PostListItem}; -pub use post::PostResponse; diff --git a/crates/axumkit-dto/src/posts/response/post.rs b/crates/axumkit-dto/src/posts/response/post.rs deleted file mode 100644 index e38e312..0000000 --- a/crates/axumkit-dto/src/posts/response/post.rs +++ /dev/null @@ -1,24 +0,0 @@ -use axum::{ - Json, - http::StatusCode, - response::{IntoResponse, Response}, -}; -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; - -#[derive(Debug, Deserialize, Serialize, ToSchema)] -pub struct PostResponse { - pub id: String, - pub author_id: String, - pub title: String, - pub content: String, - pub created_at: DateTime, - pub updated_at: DateTime, -} - -impl IntoResponse for PostResponse { - fn into_response(self) -> Response { - (StatusCode::OK, Json(self)).into_response() - } -} diff --git a/crates/axumkit-dto/src/search/mod.rs b/crates/axumkit-dto/src/search/mod.rs index 68a6164..be4bcb9 100644 --- a/crates/axumkit-dto/src/search/mod.rs +++ b/crates/axumkit-dto/src/search/mod.rs @@ -1,5 +1,5 @@ pub mod request; pub mod response; -pub use request::{SearchPostsRequest, SearchUsersRequest, SortOrder}; -pub use response::{PostSearchHit, SearchPostsResponse, SearchUsersResponse, UserSearchItem}; +pub use request::{SearchUsersRequest, SortOrder}; +pub use response::{SearchUsersResponse, UserSearchItem}; diff --git a/crates/axumkit-dto/src/search/request/mod.rs b/crates/axumkit-dto/src/search/request/mod.rs index aedf972..9ddb000 100644 --- a/crates/axumkit-dto/src/search/request/mod.rs +++ b/crates/axumkit-dto/src/search/request/mod.rs @@ -1,7 +1,5 @@ pub mod common; -pub mod posts; pub mod users; pub use common::SortOrder; -pub use posts::SearchPostsRequest; pub use users::SearchUsersRequest; diff --git a/crates/axumkit-dto/src/search/request/posts.rs b/crates/axumkit-dto/src/search/request/posts.rs deleted file mode 100644 index 4a70e16..0000000 --- a/crates/axumkit-dto/src/search/request/posts.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::validator::string_validator::validate_not_blank; -use serde::Deserialize; -use utoipa::{IntoParams, ToSchema}; -use validator::Validate; - -#[derive(Debug, Deserialize, ToSchema, Validate, IntoParams)] -#[into_params(parameter_in = Query)] -pub struct SearchPostsRequest { - #[validate(length( - min = 1, - max = 200, - message = "Query must be between 1 and 200 characters" - ))] - #[validate(custom(function = "validate_not_blank"))] - pub query: String, - - #[validate(range(min = 1, message = "Page must be greater than 0"))] - pub page: u32, - - #[validate(range(min = 1, max = 20, message = "Page size must be between 1 and 20"))] - pub page_size: u32, -} diff --git a/crates/axumkit-dto/src/search/response/mod.rs b/crates/axumkit-dto/src/search/response/mod.rs index c7ae9ca..4c17b5e 100644 --- a/crates/axumkit-dto/src/search/response/mod.rs +++ b/crates/axumkit-dto/src/search/response/mod.rs @@ -1,5 +1,3 @@ -pub mod posts; pub mod users; -pub use posts::{PostSearchHit, SearchPostsResponse}; pub use users::{SearchUsersResponse, UserSearchItem}; diff --git a/crates/axumkit-dto/src/search/response/posts.rs b/crates/axumkit-dto/src/search/response/posts.rs deleted file mode 100644 index 345ffa3..0000000 --- a/crates/axumkit-dto/src/search/response/posts.rs +++ /dev/null @@ -1,29 +0,0 @@ -use axum::Json; -use axum::http::StatusCode; -use axum::response::IntoResponse; -use serde::Serialize; -use utoipa::ToSchema; -use uuid::Uuid; - -#[derive(Debug, Serialize, ToSchema)] -pub struct PostSearchHit { - pub id: Uuid, - pub author_id: Uuid, - pub title: String, - pub content_snippet: String, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct SearchPostsResponse { - pub hits: Vec, - pub page: u32, - pub page_size: u32, - pub total_hits: u64, - pub total_pages: u32, -} - -impl IntoResponse for SearchPostsResponse { - fn into_response(self) -> axum::response::Response { - (StatusCode::OK, Json(self)).into_response() - } -} diff --git a/crates/axumkit-entity/src/lib.rs b/crates/axumkit-entity/src/lib.rs index e757fbb..78aa6e5 100644 --- a/crates/axumkit-entity/src/lib.rs +++ b/crates/axumkit-entity/src/lib.rs @@ -1,5 +1,4 @@ pub mod action_logs; pub mod common; -pub mod posts; pub mod user_oauth_connections; pub mod users; diff --git a/crates/axumkit-entity/src/posts.rs b/crates/axumkit-entity/src/posts.rs deleted file mode 100644 index 54dffeb..0000000 --- a/crates/axumkit-entity/src/posts.rs +++ /dev/null @@ -1,34 +0,0 @@ -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "posts")] -pub struct Model { - #[sea_orm(primary_key, auto_increment = false)] - pub id: Uuid, - pub author_id: Uuid, - pub title: String, - #[sea_orm(column_type = "Text", not_null)] - pub storage_key: String, - #[sea_orm(column_type = "TimestampWithTimeZone", not_null)] - pub created_at: DateTimeUtc, - #[sea_orm(column_type = "TimestampWithTimeZone", not_null)] - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::users::Entity", - from = "Column::AuthorId", - to = "super::users::Column::Id" - )] - Author, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Author.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/axumkit-errors/src/errors.rs b/crates/axumkit-errors/src/errors.rs index e3dbdc2..5650874 100644 --- a/crates/axumkit-errors/src/errors.rs +++ b/crates/axumkit-errors/src/errors.rs @@ -1,7 +1,7 @@ use crate::handlers::{ email_handler, eventstream_handler, file_handler, general_handler, meilisearch_handler, - oauth_handler, password_handler, post_handler, rate_limit_handler, session_handler, - system_handler, token_handler, totp_handler, turnstile_handler, user_handler, worker_handler, + oauth_handler, password_handler, rate_limit_handler, session_handler, system_handler, + token_handler, totp_handler, turnstile_handler, user_handler, worker_handler, }; use axum::Json; use axum::http::StatusCode; @@ -42,6 +42,9 @@ impl From> for Errors { #[derive(Debug)] pub enum Errors { + // Auth errors + InvalidCredentials, + // User errors UserInvalidPassword, UserPasswordNotSet, @@ -71,9 +74,6 @@ pub enum Errors { // Permission errors ForbiddenError(String), - // Post - PostNotFound, - // Document DocumentNotFound, DocumentAlreadyExists, @@ -187,7 +187,6 @@ impl IntoResponse for Errors { totp_handler::log_error(&self); email_handler::log_error(&self); file_handler::log_error(&self); - post_handler::log_error(&self); worker_handler::log_error(&self); eventstream_handler::log_error(&self); rate_limit_handler::log_error(&self); @@ -205,7 +204,6 @@ impl IntoResponse for Errors { .or_else(|| totp_handler::map_response(&self)) .or_else(|| email_handler::map_response(&self)) .or_else(|| file_handler::map_response(&self)) - .or_else(|| post_handler::map_response(&self)) .or_else(|| worker_handler::map_response(&self)) .or_else(|| eventstream_handler::map_response(&self)) .or_else(|| rate_limit_handler::map_response(&self)) diff --git a/crates/axumkit-errors/src/handlers/general_handler.rs b/crates/axumkit-errors/src/handlers/general_handler.rs index c642d3c..c89759f 100644 --- a/crates/axumkit-errors/src/handlers/general_handler.rs +++ b/crates/axumkit-errors/src/handlers/general_handler.rs @@ -1,26 +1,18 @@ use crate::errors::Errors; use crate::protocol::general::*; -use crate::protocol::post::*; use axum::http::StatusCode; -use tracing::{debug, warn}; +use tracing::debug; -/// 일반 에러 로깅 처리 +/// General domain error logging. pub fn log_error(error: &Errors) { match error { - // 리소스 찾을 수 없음 - warn! 레벨 - Errors::PostNotFound => { - warn!("Resource not found: {:?}", error); - } - - // 비즈니스 로직 에러 - debug! 레벨 (클라이언트 실수) Errors::ForbiddenError(_) | Errors::BadRequestError(_) | Errors::ValidationError(_) | Errors::FileTooLargeError(_) | Errors::InvalidIpAddress => { - debug!("Client error: {:?}", error); + debug!(error = ?error, "Client error"); } - _ => {} } } @@ -29,7 +21,6 @@ pub fn log_error(error: &Errors) { pub fn map_response(error: &Errors) -> Option<(StatusCode, &'static str, Option)> { match error { Errors::ForbiddenError(msg) => Some((StatusCode::FORBIDDEN, FORBIDDEN, Some(msg.clone()))), - Errors::PostNotFound => Some((StatusCode::NOT_FOUND, POST_NOT_FOUND, None)), Errors::BadRequestError(msg) => { Some((StatusCode::BAD_REQUEST, BAD_REQUEST, Some(msg.clone()))) } @@ -42,7 +33,6 @@ pub fn map_response(error: &Errors) -> Option<(StatusCode, &'static str, Option< Some(msg.clone()), )), Errors::InvalidIpAddress => Some((StatusCode::BAD_REQUEST, INVALID_IP_ADDRESS, None)), - - _ => None, // 다른 도메인의 에러는 None 반환 + _ => None, } } diff --git a/crates/axumkit-errors/src/handlers/mod.rs b/crates/axumkit-errors/src/handlers/mod.rs index 44252d8..cd13124 100644 --- a/crates/axumkit-errors/src/handlers/mod.rs +++ b/crates/axumkit-errors/src/handlers/mod.rs @@ -5,7 +5,6 @@ pub mod general_handler; pub mod meilisearch_handler; pub mod oauth_handler; pub mod password_handler; -pub mod post_handler; pub mod rate_limit_handler; pub mod session_handler; pub mod system_handler; diff --git a/crates/axumkit-errors/src/handlers/post_handler.rs b/crates/axumkit-errors/src/handlers/post_handler.rs deleted file mode 100644 index 7d3ba34..0000000 --- a/crates/axumkit-errors/src/handlers/post_handler.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::errors::Errors; -use crate::protocol::post::*; -use axum::http::StatusCode; -use tracing::warn; - -/// Post 관련 에러 로깅 처리 -pub fn log_error(error: &Errors) { - match error { - Errors::PostNotFound => { - warn!("Post error: {:?}", error); - } - _ => {} - } -} - -/// Returns: (StatusCode, error_code, details) -pub fn map_response(error: &Errors) -> Option<(StatusCode, &'static str, Option)> { - match error { - Errors::PostNotFound => Some((StatusCode::NOT_FOUND, POST_NOT_FOUND, None)), - _ => None, - } -} diff --git a/crates/axumkit-errors/src/handlers/user_handler.rs b/crates/axumkit-errors/src/handlers/user_handler.rs index 088d5e3..99a5a2f 100644 --- a/crates/axumkit-errors/src/handlers/user_handler.rs +++ b/crates/axumkit-errors/src/handlers/user_handler.rs @@ -1,4 +1,5 @@ use crate::errors::Errors; +use crate::protocol::auth::*; use crate::protocol::user::*; use axum::http::StatusCode; use tracing::{debug, warn}; @@ -12,7 +13,8 @@ pub fn log_error(error: &Errors) { } // 비즈니스 로직 에러 - debug! 레벨 (클라이언트 실수) - Errors::UserInvalidPassword + Errors::InvalidCredentials + | Errors::UserInvalidPassword | Errors::UserPasswordNotSet | Errors::UserInvalidSession | Errors::UserNotVerified @@ -44,6 +46,9 @@ pub fn log_error(error: &Errors) { /// Returns: (StatusCode, error_code, details) pub fn map_response(error: &Errors) -> Option<(StatusCode, &'static str, Option)> { match error { + Errors::InvalidCredentials => { + Some((StatusCode::UNAUTHORIZED, AUTH_INVALID_CREDENTIALS, None)) + } Errors::UserInvalidPassword => { Some((StatusCode::UNAUTHORIZED, USER_INVALID_PASSWORD, None)) } diff --git a/crates/axumkit-errors/src/protocol.rs b/crates/axumkit-errors/src/protocol.rs index 4914771..72e342b 100644 --- a/crates/axumkit-errors/src/protocol.rs +++ b/crates/axumkit-errors/src/protocol.rs @@ -1,5 +1,9 @@ //! Error code constants +pub mod auth { + pub const AUTH_INVALID_CREDENTIALS: &str = "auth:invalid_credentials"; +} + pub mod user { pub const USER_INVALID_PASSWORD: &str = "user:invalid_password"; pub const USER_PASSWORD_NOT_SET: &str = "user:password_not_set"; @@ -16,10 +20,6 @@ pub mod user { pub const USER_INVALID_TOKEN: &str = "user:invalid_token"; } -pub mod post { - pub const POST_NOT_FOUND: &str = "post:not_found"; -} - pub mod oauth { pub const OAUTH_INVALID_AUTH_URL: &str = "oauth:invalid_auth_url"; pub const OAUTH_INVALID_TOKEN_URL: &str = "oauth:invalid_token_url"; diff --git a/crates/axumkit-server/src/api/v0/routes/auth/change_email.rs b/crates/axumkit-server/src/api/v0/routes/auth/change_email.rs index 9c35810..b51314f 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/change_email.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/change_email.rs @@ -1,4 +1,4 @@ -use crate::extractors::RequiredSession; +use crate::extractors::RequiredSession; use crate::service::auth::change_email::service_change_email; use crate::state::AppState; use axum::extract::State; @@ -39,3 +39,4 @@ pub async fn auth_change_email( Ok(StatusCode::NO_CONTENT) } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/change_password.rs b/crates/axumkit-server/src/api/v0/routes/auth/change_password.rs index 1b21cd4..1112a4f 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/change_password.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/change_password.rs @@ -1,4 +1,4 @@ -use crate::extractors::RequiredSession; +use crate::extractors::RequiredSession; use crate::service::auth::change_password::service_change_password; use crate::state::AppState; use axum::extract::State; @@ -39,3 +39,4 @@ pub async fn auth_change_password( Ok(StatusCode::NO_CONTENT) } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/complete_signup.rs b/crates/axumkit-server/src/api/v0/routes/auth/complete_signup.rs index f94b620..23ea5ad 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/complete_signup.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/complete_signup.rs @@ -1,4 +1,4 @@ -use crate::middleware::anonymous_user::AnonymousUserContext; +use crate::middleware::anonymous_user::AnonymousUserContext; use crate::service::oauth::complete_signup::service_complete_signup; use crate::state::AppState; use crate::utils::extract::extract_ip_address::extract_ip_address; @@ -10,16 +10,13 @@ use axum::{ response::Response, }; use axum_extra::{TypedHeader, headers::UserAgent}; +use std::net::SocketAddr; use axumkit_dto::auth::request::CompleteSignupRequest; use axumkit_dto::auth::response::create_login_response; use axumkit_dto::validator::json_validator::ValidatedJson; use axumkit_errors::errors::Errors; -use std::net::SocketAddr; -/// OAuth pending signup을 완료합니다. /// -/// OAuth 로그인 시 신규 사용자인 경우 반환된 pending_token과 함께 -/// handle을 제출하여 가입을 완료합니다. #[utoipa::path( post, path = "/v0/auth/complete-signup", @@ -44,7 +41,6 @@ pub async fn auth_complete_signup( let user_agent_str = extract_user_agent(user_agent); let ip_address = extract_ip_address(&headers, addr); - // OAuth pending signup 완료 let session_id = service_complete_signup( &state.write_db, &state.redis_session, @@ -62,3 +58,4 @@ pub async fn auth_complete_signup( // Return 204 with login cookie (session max lifetime is server-configured). create_login_response(session_id, true) } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/confirm_email_change.rs b/crates/axumkit-server/src/api/v0/routes/auth/confirm_email_change.rs index 535985c..98ceaab 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/confirm_email_change.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/confirm_email_change.rs @@ -1,4 +1,4 @@ -use crate::service::auth::confirm_email_change::service_confirm_email_change; +use crate::service::auth::confirm_email_change::service_confirm_email_change; use crate::state::AppState; use axum::extract::State; use axum::http::StatusCode; @@ -26,3 +26,4 @@ pub async fn auth_confirm_email_change( Ok(StatusCode::NO_CONTENT) } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/forgot_password.rs b/crates/axumkit-server/src/api/v0/routes/auth/forgot_password.rs index a93758a..f4248f4 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/forgot_password.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/forgot_password.rs @@ -1,4 +1,4 @@ -use crate::service::auth::forgot_password::service_forgot_password; +use crate::service::auth::forgot_password::service_forgot_password; use crate::state::AppState; use axum::extract::State; use axum::http::StatusCode; @@ -32,3 +32,4 @@ pub async fn auth_forgot_password( Ok(StatusCode::NO_CONTENT) } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/login.rs b/crates/axumkit-server/src/api/v0/routes/auth/login.rs index 66630bb..22afd20 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/login.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/login.rs @@ -1,4 +1,4 @@ -use crate::service::auth::LoginResult; +use crate::service::auth::LoginResult; use crate::service::auth::login::service_login; use crate::state::AppState; use crate::utils::extract::extract_ip_address::extract_ip_address; @@ -10,12 +10,12 @@ use axum::{ response::Response, }; use axum_extra::{TypedHeader, headers::UserAgent}; +use std::net::SocketAddr; use axumkit_dto::auth::request::LoginRequest; use axumkit_dto::auth::response::TotpRequiredResponse; use axumkit_dto::auth::response::create_login_response; use axumkit_dto::validator::json_validator::ValidatedJson; use axumkit_errors::errors::Errors; -use std::net::SocketAddr; #[utoipa::path( post, @@ -41,7 +41,6 @@ pub async fn auth_login( let user_agent = extract_user_agent(user_agent); let ip_address = extract_ip_address(&headers, addr); - // 로그인 처리 let result = service_login( &state.write_db, &state.redis_session, @@ -56,12 +55,11 @@ pub async fn auth_login( session_id, remember_me, } => { - // 쿠키 설정하는 204 응답 반환 create_login_response(session_id, remember_me) } LoginResult::TotpRequired(temp_token) => { - // TOTP 필요: 202 + temp_token 반환 Ok(TotpRequiredResponse { temp_token }.into_response()) } } } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/logout.rs b/crates/axumkit-server/src/api/v0/routes/auth/logout.rs index 5f7bedd..1df976f 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/logout.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/logout.rs @@ -1,4 +1,4 @@ -use crate::extractors::RequiredSession; +use crate::extractors::RequiredSession; use crate::service::auth::logout::service_logout; use crate::state::AppState; use axum::{extract::State, response::Response}; @@ -22,9 +22,8 @@ pub async fn auth_logout( State(state): State, RequiredSession(session_context): RequiredSession, ) -> Result { - // 로그아웃 처리 service_logout(&state.redis_session, &session_context.session_id).await?; - // 쿠키 클리어하는 204 응답 반환 create_logout_response() } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/mod.rs b/crates/axumkit-server/src/api/v0/routes/auth/mod.rs index f1afe9c..6bd56c9 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/mod.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/mod.rs @@ -1,4 +1,4 @@ -pub mod change_email; +pub mod change_email; pub mod change_password; pub mod complete_signup; pub mod confirm_email_change; @@ -12,3 +12,4 @@ pub mod reset_password; pub mod routes; pub mod totp; pub mod verify_email; + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/oauth/github/github_authorize.rs b/crates/axumkit-server/src/api/v0/routes/auth/oauth/github/github_authorize.rs index 9218d43..372b59d 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/oauth/github/github_authorize.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/oauth/github/github_authorize.rs @@ -1,4 +1,4 @@ -use crate::middleware::anonymous_user::AnonymousUserContext; +use crate::middleware::anonymous_user::AnonymousUserContext; use crate::service::oauth::github::service_generate_github_oauth_url; use crate::state::AppState; use axum::Extension; @@ -8,7 +8,6 @@ use axumkit_dto::oauth::response::OAuthUrlResponse; use axumkit_dto::validator::query_validator::ValidatedQuery; use axumkit_errors::errors::Errors; -/// GitHub OAuth 인증 URL을 생성합니다. #[utoipa::path( get, path = "/v0/auth/oauth/github/authorize", @@ -29,3 +28,4 @@ pub async fn auth_github_authorize( service_generate_github_oauth_url(&state.redis_session, &anonymous.anonymous_user_id, flow) .await } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/oauth/github/github_link.rs b/crates/axumkit-server/src/api/v0/routes/auth/oauth/github/github_link.rs index 896008e..f6afc2d 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/oauth/github/github_link.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/oauth/github/github_link.rs @@ -1,4 +1,4 @@ -use crate::extractors::RequiredSession; +use crate::extractors::RequiredSession; use crate::middleware::anonymous_user::AnonymousUserContext; use crate::service::oauth::github::service_link_github_oauth; use crate::state::AppState; @@ -9,7 +9,6 @@ use axumkit_dto::oauth::request::link::GithubLinkRequest; use axumkit_dto::validator::json_validator::ValidatedJson; use axumkit_errors::errors::Errors; -/// GitHub OAuth를 현재 계정에 연결합니다. #[utoipa::path( post, path = "/v0/auth/oauth/github/link", @@ -45,3 +44,4 @@ pub async fn auth_github_link( Ok(StatusCode::NO_CONTENT) } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/oauth/github/github_login.rs b/crates/axumkit-server/src/api/v0/routes/auth/oauth/github/github_login.rs index d349ade..312c9f7 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/oauth/github/github_login.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/oauth/github/github_login.rs @@ -1,4 +1,4 @@ -use crate::middleware::anonymous_user::AnonymousUserContext; +use crate::middleware::anonymous_user::AnonymousUserContext; use crate::service::oauth::github::service_github_sign_in; use crate::state::AppState; use crate::utils::extract::extract_ip_address::extract_ip_address; @@ -10,16 +10,13 @@ use axum::{ response::Response, }; use axum_extra::{TypedHeader, headers::UserAgent}; +use std::net::SocketAddr; use axumkit_dto::oauth::request::github::GithubLoginRequest; use axumkit_dto::oauth::response::{OAuthPendingSignupResponse, OAuthSignInResponse}; use axumkit_dto::validator::json_validator::ValidatedJson; use axumkit_errors::errors::Errors; -use std::net::SocketAddr; -/// GitHub OAuth 로그인을 처리합니다. /// -/// - 기존 사용자: 204 No Content + Set-Cookie -/// - 신규 사용자: 200 OK + pending signup 정보 (complete-signup 필요) #[utoipa::path( post, path = "/v0/auth/oauth/github/login", @@ -44,7 +41,6 @@ pub async fn auth_github_login( let user_agent_str = extract_user_agent(user_agent); let ip_address = extract_ip_address(&headers, addr); - // GitHub OAuth 로그인 처리 let result = service_github_sign_in( &state.write_db, &state.redis_session, @@ -57,6 +53,6 @@ pub async fn auth_github_login( ) .await?; - // SignInResult를 HTTP 응답으로 변환 OAuthSignInResponse::from_result(result).into_response_result() } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/oauth/github/mod.rs b/crates/axumkit-server/src/api/v0/routes/auth/oauth/github/mod.rs index b237fe4..4810fcd 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/oauth/github/mod.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/oauth/github/mod.rs @@ -1,3 +1,4 @@ -pub mod github_authorize; +pub mod github_authorize; pub mod github_link; pub mod github_login; + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/oauth/google/google_authorize.rs b/crates/axumkit-server/src/api/v0/routes/auth/oauth/google/google_authorize.rs index c1d94bd..e39acb3 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/oauth/google/google_authorize.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/oauth/google/google_authorize.rs @@ -1,4 +1,4 @@ -use crate::middleware::anonymous_user::AnonymousUserContext; +use crate::middleware::anonymous_user::AnonymousUserContext; use crate::service::oauth::google::service_generate_google_oauth_url; use crate::state::AppState; use axum::Extension; @@ -8,7 +8,6 @@ use axumkit_dto::oauth::response::OAuthUrlResponse; use axumkit_dto::validator::query_validator::ValidatedQuery; use axumkit_errors::errors::Errors; -/// Google OAuth 인증 URL을 생성합니다. #[utoipa::path( get, path = "/v0/auth/oauth/google/authorize", @@ -29,3 +28,4 @@ pub async fn auth_google_authorize( service_generate_google_oauth_url(&state.redis_session, &anonymous.anonymous_user_id, flow) .await } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/oauth/google/google_link.rs b/crates/axumkit-server/src/api/v0/routes/auth/oauth/google/google_link.rs index 5dc14db..c1a4715 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/oauth/google/google_link.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/oauth/google/google_link.rs @@ -1,4 +1,4 @@ -use crate::extractors::RequiredSession; +use crate::extractors::RequiredSession; use crate::middleware::anonymous_user::AnonymousUserContext; use crate::service::oauth::google::service_link_google_oauth; use crate::state::AppState; @@ -9,7 +9,6 @@ use axumkit_dto::oauth::request::link::GoogleLinkRequest; use axumkit_dto::validator::json_validator::ValidatedJson; use axumkit_errors::errors::Errors; -/// Google OAuth를 현재 계정에 연결합니다. #[utoipa::path( post, path = "/v0/auth/oauth/google/link", @@ -45,3 +44,4 @@ pub async fn auth_google_link( Ok(StatusCode::NO_CONTENT) } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/oauth/google/google_login.rs b/crates/axumkit-server/src/api/v0/routes/auth/oauth/google/google_login.rs index d38a988..dfbba31 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/oauth/google/google_login.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/oauth/google/google_login.rs @@ -1,4 +1,4 @@ -use crate::middleware::anonymous_user::AnonymousUserContext; +use crate::middleware::anonymous_user::AnonymousUserContext; use crate::service::oauth::google::service_google_sign_in; use crate::state::AppState; use crate::utils::extract::extract_ip_address::extract_ip_address; @@ -10,16 +10,13 @@ use axum::{ response::Response, }; use axum_extra::{TypedHeader, headers::UserAgent}; +use std::net::SocketAddr; use axumkit_dto::oauth::request::google::GoogleLoginRequest; use axumkit_dto::oauth::response::{OAuthPendingSignupResponse, OAuthSignInResponse}; use axumkit_dto::validator::json_validator::ValidatedJson; use axumkit_errors::errors::Errors; -use std::net::SocketAddr; -/// Google OAuth 로그인을 처리합니다. /// -/// - 기존 사용자: 204 No Content + Set-Cookie -/// - 신규 사용자: 200 OK + pending signup 정보 (complete-signup 필요) #[utoipa::path( post, path = "/v0/auth/oauth/google/login", @@ -44,7 +41,6 @@ pub async fn auth_google_login( let user_agent_str = extract_user_agent(user_agent); let ip_address = extract_ip_address(&headers, addr); - // Google OAuth 로그인 처리 let result = service_google_sign_in( &state.write_db, &state.redis_session, @@ -57,6 +53,6 @@ pub async fn auth_google_login( ) .await?; - // SignInResult를 HTTP 응답으로 변환 OAuthSignInResponse::from_result(result).into_response_result() } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/oauth/google/mod.rs b/crates/axumkit-server/src/api/v0/routes/auth/oauth/google/mod.rs index 2e75ec5..5550161 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/oauth/google/mod.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/oauth/google/mod.rs @@ -1,3 +1,4 @@ -pub mod google_authorize; +pub mod google_authorize; pub mod google_link; pub mod google_login; + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/oauth/list_oauth_connections.rs b/crates/axumkit-server/src/api/v0/routes/auth/oauth/list_oauth_connections.rs index 44aa6ea..1d051a4 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/oauth/list_oauth_connections.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/oauth/list_oauth_connections.rs @@ -1,11 +1,10 @@ -use crate::extractors::RequiredSession; +use crate::extractors::RequiredSession; use crate::service::oauth::list_connections::service_list_oauth_connections; use crate::state::AppState; use axum::extract::State; use axumkit_dto::oauth::response::OAuthConnectionListResponse; use axumkit_errors::errors::Errors; -/// 현재 사용자의 OAuth 연결 목록을 조회합니다. #[utoipa::path( get, path = "/v0/auth/oauth/connections", @@ -27,3 +26,4 @@ pub async fn list_oauth_connections( Ok(result) } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/oauth/mod.rs b/crates/axumkit-server/src/api/v0/routes/auth/oauth/mod.rs index 8e67760..2f7e56c 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/oauth/mod.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/oauth/mod.rs @@ -1,4 +1,5 @@ -pub mod github; +pub mod github; pub mod google; pub mod list_oauth_connections; pub mod unlink_oauth_connection; + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/oauth/unlink_oauth_connection.rs b/crates/axumkit-server/src/api/v0/routes/auth/oauth/unlink_oauth_connection.rs index a787d7c..148383c 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/oauth/unlink_oauth_connection.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/oauth/unlink_oauth_connection.rs @@ -1,4 +1,4 @@ -use crate::extractors::RequiredSession; +use crate::extractors::RequiredSession; use crate::service::oauth::unlink_connection::service_unlink_oauth; use crate::state::AppState; use axum::extract::State; @@ -7,7 +7,6 @@ use axumkit_dto::oauth::request::unlink::UnlinkOAuthRequest; use axumkit_dto::validator::json_validator::ValidatedJson; use axumkit_errors::errors::Errors; -/// OAuth 연결을 해제합니다. #[utoipa::path( post, path = "/v0/auth/oauth/connections/unlink", @@ -33,3 +32,4 @@ pub async fn unlink_oauth_connection( Ok(StatusCode::NO_CONTENT) } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/openapi.rs b/crates/axumkit-server/src/api/v0/routes/auth/openapi.rs index 04b8f48..bb3400d 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/openapi.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/openapi.rs @@ -1,3 +1,4 @@ +use utoipa::OpenApi; use axumkit_dto::auth::request::{ ChangeEmailRequest, ChangePasswordRequest, CompleteSignupRequest, ConfirmEmailChangeRequest, ForgotPasswordRequest, LoginRequest, ResetPasswordRequest, TotpDisableRequest, @@ -9,13 +10,12 @@ use axumkit_dto::auth::response::{ }; use axumkit_dto::oauth::request::{ GithubLinkRequest, GithubLoginRequest, GoogleLinkRequest, GoogleLoginRequest, - UnlinkOAuthRequest, + OAuthAuthorizeFlow, OAuthAuthorizeQuery, UnlinkOAuthRequest, }; use axumkit_dto::oauth::response::OAuthPendingSignupResponse; use axumkit_dto::oauth::response::{ OAuthConnectionListResponse, OAuthConnectionResponse, OAuthUrlResponse, }; -use utoipa::OpenApi; #[derive(OpenApi)] #[openapi( @@ -54,6 +54,8 @@ use utoipa::OpenApi; CompleteSignupRequest, OAuthUrlResponse, OAuthPendingSignupResponse, + OAuthAuthorizeFlow, + OAuthAuthorizeQuery, GoogleLoginRequest, GithubLoginRequest, GoogleLinkRequest, @@ -80,3 +82,4 @@ use utoipa::OpenApi; ) )] pub struct AuthApiDoc; + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/resend_verification_email.rs b/crates/axumkit-server/src/api/v0/routes/auth/resend_verification_email.rs index d284c38..f4e4b95 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/resend_verification_email.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/resend_verification_email.rs @@ -1,4 +1,4 @@ -use crate::extractors::RequiredSession; +use crate::extractors::RequiredSession; use crate::service::auth::resend_verification_email::service_resend_verification_email; use crate::state::AppState; use axum::extract::State; @@ -37,3 +37,4 @@ pub async fn auth_resend_verification_email( Ok(StatusCode::NO_CONTENT) } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/reset_password.rs b/crates/axumkit-server/src/api/v0/routes/auth/reset_password.rs index 5381c32..53b0cc7 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/reset_password.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/reset_password.rs @@ -1,4 +1,4 @@ -use crate::service::auth::reset_password::service_reset_password; +use crate::service::auth::reset_password::service_reset_password; use crate::state::AppState; use axum::extract::State; use axum::http::StatusCode; @@ -32,3 +32,4 @@ pub async fn auth_reset_password( Ok(StatusCode::NO_CONTENT) } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/routes.rs b/crates/axumkit-server/src/api/v0/routes/auth/routes.rs index 6932cf6..6ba0ae6 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/routes.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/routes.rs @@ -1,4 +1,4 @@ -use super::change_email::auth_change_email; +use super::change_email::auth_change_email; use super::change_password::auth_change_password; use super::complete_signup::auth_complete_signup; use super::confirm_email_change::auth_confirm_email_change; @@ -77,3 +77,4 @@ pub fn auth_routes(_state: AppState) -> Router { post(auth_confirm_email_change), ) } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/totp/disable.rs b/crates/axumkit-server/src/api/v0/routes/auth/totp/disable.rs index a71f0d6..ee8a5e4 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/totp/disable.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/totp/disable.rs @@ -1,5 +1,4 @@ -use crate::extractors::RequiredSession; -use crate::repository::user::repository_get_user_by_id; +use crate::extractors::RequiredSession; use crate::service::auth::totp::service_totp_disable; use crate::state::AppState; use axum::extract::State; @@ -28,7 +27,7 @@ pub async fn totp_disable( RequiredSession(session): RequiredSession, ValidatedJson(payload): ValidatedJson, ) -> Result { - let user = repository_get_user_by_id(&state.write_db, session.user_id).await?; - service_totp_disable(&state.write_db, session.user_id, &user.email, &payload.code).await?; + service_totp_disable(&state.write_db, session.user_id, &payload.code).await?; Ok(StatusCode::NO_CONTENT) } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/totp/enable.rs b/crates/axumkit-server/src/api/v0/routes/auth/totp/enable.rs index a82c5f9..a2f18b1 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/totp/enable.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/totp/enable.rs @@ -1,5 +1,4 @@ -use crate::extractors::RequiredSession; -use crate::repository::user::repository_get_user_by_id; +use crate::extractors::RequiredSession; use crate::service::auth::totp::service_totp_enable; use crate::state::AppState; use axum::extract::State; @@ -29,6 +28,6 @@ pub async fn totp_enable( RequiredSession(session): RequiredSession, ValidatedJson(payload): ValidatedJson, ) -> Result { - let user = repository_get_user_by_id(&state.write_db, session.user_id).await?; - service_totp_enable(&state.write_db, session.user_id, &user.email, &payload.code).await + service_totp_enable(&state.write_db, session.user_id, &payload.code).await } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/totp/mod.rs b/crates/axumkit-server/src/api/v0/routes/auth/totp/mod.rs index 63cb90c..5a95998 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/totp/mod.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/totp/mod.rs @@ -1,6 +1,7 @@ -pub mod disable; +pub mod disable; pub mod enable; pub mod regenerate_backup_codes; pub mod setup; pub mod status; pub mod verify; + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/totp/regenerate_backup_codes.rs b/crates/axumkit-server/src/api/v0/routes/auth/totp/regenerate_backup_codes.rs index 04ddcd9..73169f5 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/totp/regenerate_backup_codes.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/totp/regenerate_backup_codes.rs @@ -1,5 +1,4 @@ -use crate::extractors::RequiredSession; -use crate::repository::user::repository_get_user_by_id; +use crate::extractors::RequiredSession; use crate::service::auth::totp::service_regenerate_backup_codes; use crate::state::AppState; use axum::extract::State; @@ -28,7 +27,6 @@ pub async fn totp_regenerate_backup_codes( RequiredSession(session): RequiredSession, ValidatedJson(payload): ValidatedJson, ) -> Result { - let user = repository_get_user_by_id(&state.write_db, session.user_id).await?; - service_regenerate_backup_codes(&state.write_db, session.user_id, &user.email, &payload.code) - .await + service_regenerate_backup_codes(&state.write_db, session.user_id, &payload.code).await } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/totp/setup.rs b/crates/axumkit-server/src/api/v0/routes/auth/totp/setup.rs index 6000823..fd39b00 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/totp/setup.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/totp/setup.rs @@ -1,5 +1,4 @@ -use crate::extractors::RequiredSession; -use crate::repository::user::repository_get_user_by_id; +use crate::extractors::RequiredSession; use crate::service::auth::totp::service_totp_setup; use crate::state::AppState; use axum::extract::State; @@ -24,6 +23,6 @@ pub async fn totp_setup( State(state): State, RequiredSession(session): RequiredSession, ) -> Result { - let user = repository_get_user_by_id(&state.write_db, session.user_id).await?; - service_totp_setup(&state.write_db, session.user_id, &user.email).await + service_totp_setup(&state.write_db, session.user_id).await } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/totp/status.rs b/crates/axumkit-server/src/api/v0/routes/auth/totp/status.rs index 58f57cc..41bae4d 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/totp/status.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/totp/status.rs @@ -1,4 +1,4 @@ -use crate::extractors::RequiredSession; +use crate::extractors::RequiredSession; use crate::service::auth::totp::service_totp_status; use crate::state::AppState; use axum::extract::State; @@ -22,5 +22,6 @@ pub async fn totp_status( State(state): State, RequiredSession(session): RequiredSession, ) -> Result { - service_totp_status(&state.read_db, session.user_id).await + service_totp_status(&state.write_db, session.user_id).await } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/totp/verify.rs b/crates/axumkit-server/src/api/v0/routes/auth/totp/verify.rs index 3edbfa3..d0a7701 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/totp/verify.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/totp/verify.rs @@ -1,4 +1,4 @@ -use crate::service::auth::totp::service_totp_verify; +use crate::service::auth::totp::service_totp_verify; use crate::state::AppState; use axum::extract::State; use axum::response::Response; @@ -32,3 +32,4 @@ pub async fn totp_verify( create_login_response(result.session_id, result.remember_me) } + diff --git a/crates/axumkit-server/src/api/v0/routes/auth/verify_email.rs b/crates/axumkit-server/src/api/v0/routes/auth/verify_email.rs index 9de3659..1d1cbea 100644 --- a/crates/axumkit-server/src/api/v0/routes/auth/verify_email.rs +++ b/crates/axumkit-server/src/api/v0/routes/auth/verify_email.rs @@ -1,4 +1,4 @@ -use crate::service::auth::verify_email::service_verify_email; +use crate::service::auth::verify_email::service_verify_email; use crate::state::AppState; use axum::extract::State; use axum::http::StatusCode; @@ -28,3 +28,4 @@ pub async fn auth_verify_email( Ok(StatusCode::NO_CONTENT) } + diff --git a/crates/axumkit-server/src/api/v0/routes/mod.rs b/crates/axumkit-server/src/api/v0/routes/mod.rs index f8f1a19..778dbe9 100644 --- a/crates/axumkit-server/src/api/v0/routes/mod.rs +++ b/crates/axumkit-server/src/api/v0/routes/mod.rs @@ -1,7 +1,6 @@ mod action_logs; mod auth; pub mod openapi; -mod posts; pub mod routes; mod search; mod stream; diff --git a/crates/axumkit-server/src/api/v0/routes/openapi.rs b/crates/axumkit-server/src/api/v0/routes/openapi.rs index 5c4f867..3661d63 100644 --- a/crates/axumkit-server/src/api/v0/routes/openapi.rs +++ b/crates/axumkit-server/src/api/v0/routes/openapi.rs @@ -1,6 +1,5 @@ use super::action_logs::openapi::ActionLogsOpenApi; use super::auth::openapi::AuthApiDoc; -use super::posts::openapi::PostsApiDoc; use super::search::openapi::SearchApiDoc; use super::stream::openapi::StreamOpenApi; use super::user::openapi::UserApiDoc; @@ -15,7 +14,6 @@ impl V0ApiDoc { let mut openapi = Self::openapi(); openapi.merge(AuthApiDoc::openapi()); openapi.merge(UserApiDoc::openapi()); - openapi.merge(PostsApiDoc::openapi()); openapi.merge(SearchApiDoc::openapi()); openapi.merge(ActionLogsOpenApi::openapi()); openapi.merge(StreamOpenApi::openapi()); diff --git a/crates/axumkit-server/src/api/v0/routes/posts/create_post.rs b/crates/axumkit-server/src/api/v0/routes/posts/create_post.rs deleted file mode 100644 index d165b9e..0000000 --- a/crates/axumkit-server/src/api/v0/routes/posts/create_post.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::extractors::session::RequiredSession; -use crate::service::posts::service_create_post; -use crate::state::AppState; -use axum::{extract::State, response::IntoResponse}; -use axumkit_dto::posts::{CreatePostRequest, CreatePostResponse}; -use axumkit_dto::validator::json_validator::ValidatedJson; -use axumkit_errors::errors::Errors; - -#[utoipa::path( - post, - path = "/v0/posts", - request_body = CreatePostRequest, - responses( - (status = 201, description = "Post created successfully", body = CreatePostResponse), - (status = 400, description = "Bad request - Invalid JSON or validation error"), - (status = 401, description = "Unauthorized - User not authenticated"), - (status = 500, description = "Internal Server Error"), - ), - tag = "Posts" -)] -pub async fn create_post( - State(state): State, - RequiredSession(session): RequiredSession, - ValidatedJson(payload): ValidatedJson, -) -> Result { - let response = service_create_post( - &state.write_db, - &state.seaweedfs_client, - &state.worker, - session.user_id, - payload.title, - payload.content, - ) - .await?; - - Ok(response) -} diff --git a/crates/axumkit-server/src/api/v0/routes/posts/delete_post.rs b/crates/axumkit-server/src/api/v0/routes/posts/delete_post.rs deleted file mode 100644 index 1f25728..0000000 --- a/crates/axumkit-server/src/api/v0/routes/posts/delete_post.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::extractors::session::RequiredSession; -use crate::service::posts::service_delete_post; -use crate::state::AppState; -use axum::extract::State; -use axumkit_dto::posts::{DeletePostResponse, GetPostPath}; -use axumkit_dto::validator::path_validator::ValidatedPath; -use axumkit_errors::errors::Errors; - -#[utoipa::path( - delete, - path = "/v0/posts/{id}", - params(GetPostPath), - responses( - (status = 200, description = "Post deleted successfully", body = DeletePostResponse), - (status = 401, description = "Unauthorized - User not authenticated or not the author"), - (status = 404, description = "Post not found"), - (status = 500, description = "Internal Server Error"), - ), - tag = "Posts" -)] -pub async fn delete_post( - State(state): State, - RequiredSession(session): RequiredSession, - ValidatedPath(path): ValidatedPath, -) -> Result { - service_delete_post(&state.write_db, &state.worker, path.id, session.user_id).await -} diff --git a/crates/axumkit-server/src/api/v0/routes/posts/get_post.rs b/crates/axumkit-server/src/api/v0/routes/posts/get_post.rs deleted file mode 100644 index e51b85e..0000000 --- a/crates/axumkit-server/src/api/v0/routes/posts/get_post.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::service::posts::service_get_post; -use crate::state::AppState; -use axum::extract::State; -use axumkit_dto::posts::{GetPostPath, PostResponse}; -use axumkit_dto::validator::path_validator::ValidatedPath; -use axumkit_errors::errors::Errors; - -#[utoipa::path( - get, - path = "/v0/posts/{id}", - params(GetPostPath), - responses( - (status = 200, description = "Post retrieved successfully", body = PostResponse), - (status = 404, description = "Post not found"), - (status = 500, description = "Internal Server Error"), - ), - tag = "Posts" -)] -pub async fn get_post( - State(state): State, - ValidatedPath(path): ValidatedPath, -) -> Result { - service_get_post(&state.read_db, &state.seaweedfs_client, path.id).await -} diff --git a/crates/axumkit-server/src/api/v0/routes/posts/list_posts.rs b/crates/axumkit-server/src/api/v0/routes/posts/list_posts.rs deleted file mode 100644 index aff6267..0000000 --- a/crates/axumkit-server/src/api/v0/routes/posts/list_posts.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::service::posts::service_list_posts; -use crate::state::AppState; -use axum::{ - extract::{Query, State}, - response::IntoResponse, -}; -use axumkit_dto::posts::{ListPostsQuery, ListPostsResponse}; -use axumkit_errors::errors::Errors; - -#[utoipa::path( - get, - path = "/v0/posts", - params( - ("limit" = u64, Query, description = "Number of posts to return"), - ("offset" = u64, Query, description = "Number of posts to skip") - ), - responses( - (status = 200, description = "Posts retrieved successfully", body = ListPostsResponse), - (status = 500, description = "Internal Server Error"), - ), - tag = "Posts" -)] -pub async fn list_posts( - State(state): State, - Query(query): Query, -) -> Result { - let response = service_list_posts(&state.read_db, query.limit, query.offset).await?; - Ok(response) -} diff --git a/crates/axumkit-server/src/api/v0/routes/posts/mod.rs b/crates/axumkit-server/src/api/v0/routes/posts/mod.rs deleted file mode 100644 index 6e3e867..0000000 --- a/crates/axumkit-server/src/api/v0/routes/posts/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod create_post; -pub mod delete_post; -pub mod get_post; -pub mod list_posts; -pub mod openapi; -pub mod routes; -pub mod update_post; diff --git a/crates/axumkit-server/src/api/v0/routes/posts/openapi.rs b/crates/axumkit-server/src/api/v0/routes/posts/openapi.rs deleted file mode 100644 index c6d0ec0..0000000 --- a/crates/axumkit-server/src/api/v0/routes/posts/openapi.rs +++ /dev/null @@ -1,33 +0,0 @@ -use axumkit_dto::posts::{ - CreatePostRequest, CreatePostResponse, DeletePostResponse, GetPostPath, ListPostsQuery, - ListPostsResponse, PostListItem, PostResponse, UpdatePostRequest, -}; -use utoipa::OpenApi; - -#[derive(OpenApi)] -#[openapi( - paths( - super::create_post::create_post, - super::get_post::get_post, - super::list_posts::list_posts, - super::update_post::update_post, - super::delete_post::delete_post, - ), - components( - schemas( - CreatePostRequest, - CreatePostResponse, - GetPostPath, - ListPostsQuery, - ListPostsResponse, - PostListItem, - PostResponse, - UpdatePostRequest, - DeletePostResponse, - ) - ), - tags( - (name = "Posts", description = "Posts endpoints") - ) -)] -pub struct PostsApiDoc; diff --git a/crates/axumkit-server/src/api/v0/routes/posts/routes.rs b/crates/axumkit-server/src/api/v0/routes/posts/routes.rs deleted file mode 100644 index 45a621a..0000000 --- a/crates/axumkit-server/src/api/v0/routes/posts/routes.rs +++ /dev/null @@ -1,19 +0,0 @@ -use super::create_post::create_post; -use super::delete_post::delete_post; -use super::get_post::get_post; -use super::list_posts::list_posts; -use super::update_post::update_post; -use crate::state::AppState; -use axum::{ - Router, - routing::{get, post}, -}; - -pub fn posts_routes() -> Router { - Router::new() - .route("/posts", post(create_post).get(list_posts)) - .route( - "/posts/{id}", - get(get_post).patch(update_post).delete(delete_post), - ) -} diff --git a/crates/axumkit-server/src/api/v0/routes/posts/update_post.rs b/crates/axumkit-server/src/api/v0/routes/posts/update_post.rs deleted file mode 100644 index 1d2d3ae..0000000 --- a/crates/axumkit-server/src/api/v0/routes/posts/update_post.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::extractors::session::RequiredSession; -use crate::service::posts::service_update_post; -use crate::state::AppState; -use axum::extract::State; -use axumkit_dto::posts::{GetPostPath, PostResponse, UpdatePostRequest}; -use axumkit_dto::validator::json_validator::ValidatedJson; -use axumkit_dto::validator::path_validator::ValidatedPath; -use axumkit_errors::errors::Errors; - -#[utoipa::path( - patch, - path = "/v0/posts/{id}", - params(GetPostPath), - request_body = UpdatePostRequest, - responses( - (status = 200, description = "Post updated successfully", body = PostResponse), - (status = 400, description = "Bad request - Invalid JSON or validation error"), - (status = 401, description = "Unauthorized - User not authenticated or not the author"), - (status = 404, description = "Post not found"), - (status = 500, description = "Internal Server Error"), - ), - tag = "Posts" -)] -pub async fn update_post( - State(state): State, - RequiredSession(session): RequiredSession, - ValidatedPath(path): ValidatedPath, - ValidatedJson(payload): ValidatedJson, -) -> Result { - service_update_post( - &state.write_db, - &state.seaweedfs_client, - &state.worker, - path.id, - session.user_id, - payload.title, - payload.content, - ) - .await -} diff --git a/crates/axumkit-server/src/api/v0/routes/routes.rs b/crates/axumkit-server/src/api/v0/routes/routes.rs index 1f131b4..9c4652d 100644 --- a/crates/axumkit-server/src/api/v0/routes/routes.rs +++ b/crates/axumkit-server/src/api/v0/routes/routes.rs @@ -1,6 +1,5 @@ use super::action_logs::routes::action_logs_routes as ActionLogsRoutes; use super::auth::routes::auth_routes as AuthRoutes; -use super::posts::routes::posts_routes as PostsRoutes; use super::search::routes::search_routes as SearchRoutes; use super::stream::routes::stream_routes as StreamRoutes; use super::user::routes::user_routes as UserRoutes; @@ -12,7 +11,6 @@ pub fn v0_routes(state: AppState) -> Router { Router::new() .merge(UserRoutes()) .merge(AuthRoutes(state.clone())) - .merge(PostsRoutes()) .merge(SearchRoutes()) .merge(ActionLogsRoutes()) .merge(StreamRoutes()) diff --git a/crates/axumkit-server/src/api/v0/routes/search/mod.rs b/crates/axumkit-server/src/api/v0/routes/search/mod.rs index 505d934..a8e8179 100644 --- a/crates/axumkit-server/src/api/v0/routes/search/mod.rs +++ b/crates/axumkit-server/src/api/v0/routes/search/mod.rs @@ -1,4 +1,3 @@ pub mod openapi; pub mod routes; -pub mod search_posts; pub mod search_users; diff --git a/crates/axumkit-server/src/api/v0/routes/search/openapi.rs b/crates/axumkit-server/src/api/v0/routes/search/openapi.rs index c95e6f9..a58ee05 100644 --- a/crates/axumkit-server/src/api/v0/routes/search/openapi.rs +++ b/crates/axumkit-server/src/api/v0/routes/search/openapi.rs @@ -1,20 +1,15 @@ use axumkit_dto::search::{ - PostSearchHit, SearchPostsRequest, SearchPostsResponse, SearchUsersRequest, - SearchUsersResponse, SortOrder, UserSearchItem, + SearchUsersRequest, SearchUsersResponse, SortOrder, UserSearchItem, }; use utoipa::OpenApi; #[derive(OpenApi)] #[openapi( paths( - super::search_posts::search_posts, super::search_users::search_users, ), components( schemas( - SearchPostsRequest, - SearchPostsResponse, - PostSearchHit, SortOrder, SearchUsersRequest, SearchUsersResponse, diff --git a/crates/axumkit-server/src/api/v0/routes/search/routes.rs b/crates/axumkit-server/src/api/v0/routes/search/routes.rs index 008fef4..ff1b59f 100644 --- a/crates/axumkit-server/src/api/v0/routes/search/routes.rs +++ b/crates/axumkit-server/src/api/v0/routes/search/routes.rs @@ -1,12 +1,9 @@ use crate::state::AppState; use axum::{Router, routing::get}; -use super::search_posts::search_posts; use super::search_users::search_users; pub fn search_routes() -> Router { // Public routes (no authentication required) - Router::new() - .route("/search/posts", get(search_posts)) - .route("/search/users", get(search_users)) + Router::new().route("/search/users", get(search_users)) } diff --git a/crates/axumkit-server/src/api/v0/routes/search/search_posts.rs b/crates/axumkit-server/src/api/v0/routes/search/search_posts.rs deleted file mode 100644 index 18e1329..0000000 --- a/crates/axumkit-server/src/api/v0/routes/search/search_posts.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::service::search::service_search_posts; -use crate::state::AppState; -use axum::extract::State; -use axumkit_dto::search::{SearchPostsRequest, SearchPostsResponse}; -use axumkit_dto::validator::query_validator::ValidatedQuery; -use axumkit_errors::errors::Errors; - -#[utoipa::path( - get, - path = "/v0/search/posts", - params(SearchPostsRequest), - responses( - (status = 200, description = "Posts found successfully", body = SearchPostsResponse), - (status = 400, description = "Bad request - Invalid query parameters or validation error"), - (status = 500, description = "Internal Server Error - MeiliSearch query failed") - ), - tag = "Search" -)] -pub async fn search_posts( - State(state): State, - ValidatedQuery(payload): ValidatedQuery, -) -> Result { - let result = service_search_posts(&state.meilisearch_client, &payload).await?; - - Ok(result) -} diff --git a/crates/axumkit-server/src/bridge/worker_client/index.rs b/crates/axumkit-server/src/bridge/worker_client/index.rs index f188775..86e2c21 100644 --- a/crates/axumkit-server/src/bridge/worker_client/index.rs +++ b/crates/axumkit-server/src/bridge/worker_client/index.rs @@ -1,47 +1,14 @@ use super::publish_job; use crate::state::WorkerClient; use axumkit_errors::errors::Errors; -use axumkit_worker::jobs::{ - post_index::{IndexAction, IndexPostJob}, - user_index::{IndexUserJob, UserIndexAction}, -}; -use axumkit_worker::nats::streams::{INDEX_POST_SUBJECT, INDEX_USER_SUBJECT}; +use axumkit_worker::jobs::user_index::{IndexUserJob, UserIndexAction}; +use axumkit_worker::nats::streams::INDEX_USER_SUBJECT; use tracing::info; use uuid::Uuid; -/// Push a post indexing job to the worker queue -pub async fn index_post(worker: &WorkerClient, post_id: Uuid) -> Result<(), Errors> { - info!("Queuing post index job for {}", post_id); - - let job = IndexPostJob { - post_id, - action: IndexAction::Index, - }; - - publish_job(worker, INDEX_POST_SUBJECT, &job).await?; - - info!("Post index job queued for {}", post_id); - Ok(()) -} - -/// Push a post deletion job to the worker queue -pub async fn delete_post_from_index(worker: &WorkerClient, post_id: Uuid) -> Result<(), Errors> { - info!("Queuing post delete job for {}", post_id); - - let job = IndexPostJob { - post_id, - action: IndexAction::Delete, - }; - - publish_job(worker, INDEX_POST_SUBJECT, &job).await?; - - info!("Post delete job queued for {}", post_id); - Ok(()) -} - /// Push a user indexing job to the worker queue pub async fn index_user(worker: &WorkerClient, user_id: Uuid) -> Result<(), Errors> { - info!("Queuing user index job for {}", user_id); + info!(user_id = %user_id, "Queuing user index job"); let job = IndexUserJob { user_id, @@ -50,13 +17,13 @@ pub async fn index_user(worker: &WorkerClient, user_id: Uuid) -> Result<(), Erro publish_job(worker, INDEX_USER_SUBJECT, &job).await?; - info!("User index job queued for {}", user_id); + info!(user_id = %user_id, "User index job queued"); Ok(()) } /// Push a user deletion job to the worker queue pub async fn delete_user_from_index(worker: &WorkerClient, user_id: Uuid) -> Result<(), Errors> { - info!("Queuing user delete job for {}", user_id); + info!(user_id = %user_id, "Queuing user delete job"); let job = IndexUserJob { user_id, @@ -65,6 +32,6 @@ pub async fn delete_user_from_index(worker: &WorkerClient, user_id: Uuid) -> Res publish_job(worker, INDEX_USER_SUBJECT, &job).await?; - info!("User delete job queued for {}", user_id); + info!(user_id = %user_id, "User delete job queued"); Ok(()) } diff --git a/crates/axumkit-server/src/bridge/worker_client/mod.rs b/crates/axumkit-server/src/bridge/worker_client/mod.rs index eb114b4..553494f 100644 --- a/crates/axumkit-server/src/bridge/worker_client/mod.rs +++ b/crates/axumkit-server/src/bridge/worker_client/mod.rs @@ -2,13 +2,11 @@ mod cache; mod email; mod index; mod reindex; -mod storage; // Re-export all functions for backwards compatibility pub use email::*; pub use index::*; pub use reindex::*; -pub use storage::*; use crate::state::WorkerClient; use axumkit_errors::errors::Errors; diff --git a/crates/axumkit-server/src/bridge/worker_client/reindex.rs b/crates/axumkit-server/src/bridge/worker_client/reindex.rs index a217f7c..dd04084 100644 --- a/crates/axumkit-server/src/bridge/worker_client/reindex.rs +++ b/crates/axumkit-server/src/bridge/worker_client/reindex.rs @@ -1,30 +1,11 @@ use super::publish_job; use crate::state::WorkerClient; use axumkit_errors::errors::Errors; -use axumkit_worker::jobs::reindex::{create_reindex_posts_job, create_reindex_users_job}; -use axumkit_worker::nats::streams::{REINDEX_POSTS_SUBJECT, REINDEX_USERS_SUBJECT}; +use axumkit_worker::jobs::reindex::create_reindex_users_job; +use axumkit_worker::nats::streams::REINDEX_USERS_SUBJECT; use tracing::info; use uuid::Uuid; -/// Start a full reindex of all posts -pub async fn start_reindex_posts( - worker: &WorkerClient, - batch_size: Option, -) -> Result { - let reindex_id = Uuid::now_v7(); - info!( - "Starting post reindex job: reindex_id={}, batch_size={:?}", - reindex_id, batch_size - ); - - let job = create_reindex_posts_job(reindex_id, batch_size); - - publish_job(worker, REINDEX_POSTS_SUBJECT, &job).await?; - - info!("Post reindex job started: reindex_id={}", reindex_id); - Ok(reindex_id) -} - /// Start a full reindex of all users pub async fn start_reindex_users( worker: &WorkerClient, @@ -32,14 +13,15 @@ pub async fn start_reindex_users( ) -> Result { let reindex_id = Uuid::now_v7(); info!( - "Starting user reindex job: reindex_id={}, batch_size={:?}", - reindex_id, batch_size + reindex_id = %reindex_id, + batch_size = ?batch_size, + "Starting user reindex job" ); let job = create_reindex_users_job(reindex_id, batch_size); publish_job(worker, REINDEX_USERS_SUBJECT, &job).await?; - info!("User reindex job started: reindex_id={}", reindex_id); + info!(reindex_id = %reindex_id, "User reindex job started"); Ok(reindex_id) } diff --git a/crates/axumkit-server/src/bridge/worker_client/storage.rs b/crates/axumkit-server/src/bridge/worker_client/storage.rs deleted file mode 100644 index 46ddc56..0000000 --- a/crates/axumkit-server/src/bridge/worker_client/storage.rs +++ /dev/null @@ -1,25 +0,0 @@ -use super::publish_job; -use crate::state::WorkerClient; -use axumkit_errors::errors::Errors; -use axumkit_worker::jobs::storage::DeleteContentJob; -use axumkit_worker::nats::streams::DELETE_CONTENT_SUBJECT; -use tracing::info; - -/// Push a content deletion job to the worker queue -pub async fn delete_content( - worker: &WorkerClient, - storage_keys: Vec, -) -> Result<(), Errors> { - if storage_keys.is_empty() { - return Ok(()); - } - - info!("Queuing content delete job for {} keys", storage_keys.len()); - - let job = DeleteContentJob { storage_keys }; - - publish_job(worker, DELETE_CONTENT_SUBJECT, &job).await?; - - info!("Content delete job queued"); - Ok(()) -} diff --git a/crates/axumkit-server/src/connection/mod.rs b/crates/axumkit-server/src/connection/mod.rs index b4a4965..860cf2f 100644 --- a/crates/axumkit-server/src/connection/mod.rs +++ b/crates/axumkit-server/src/connection/mod.rs @@ -3,11 +3,9 @@ pub mod http_conn; pub mod meilisearch_conn; pub mod r2_conn; pub mod redis_conn; -pub mod seaweedfs_conn; pub use database_conn::*; pub use http_conn::*; pub use meilisearch_conn::*; pub use r2_conn::*; pub use redis_conn::*; -pub use seaweedfs_conn::*; diff --git a/crates/axumkit-server/src/connection/r2_conn.rs b/crates/axumkit-server/src/connection/r2_conn.rs index 82a66be..235d080 100644 --- a/crates/axumkit-server/src/connection/r2_conn.rs +++ b/crates/axumkit-server/src/connection/r2_conn.rs @@ -144,8 +144,8 @@ pub async fn establish_r2_connection() -> Result, - bucket: String, -} - -impl SeaweedFsClient { - pub fn new(client: Client, bucket: String) -> Self { - Self { - client: Arc::new(client), - bucket, - } - } - - /// 콘텐츠 업로드 (zstd 압축 적용) - pub async fn upload_content( - &self, - key: &str, - content: &str, - ) -> Result<(), Box> { - let compressed = zstd::encode_all(content.as_bytes(), 3)?; - - self.client - .put_object() - .bucket(&self.bucket) - .key(key) - .body(compressed.into()) - .content_type("application/zstd") - .send() - .await?; - - Ok(()) - } - - /// 콘텐츠 다운로드 (zstd 압축 해제) - pub async fn download_content( - &self, - key: &str, - ) -> Result> { - let resp = self - .client - .get_object() - .bucket(&self.bucket) - .key(key) - .send() - .await?; - - let data = resp.body.collect().await?; - let bytes = data.into_bytes(); - - let decompressed = zstd::decode_all(bytes.as_ref())?; - let content = String::from_utf8(decompressed)?; - - Ok(content) - } - - /// raw 바이트 업로드 (압축 없음) - pub async fn upload(&self, key: &str, body: Vec) -> Result<(), S3Error> { - self.client - .put_object() - .bucket(&self.bucket) - .key(key) - .body(body.into()) - .send() - .await?; - Ok(()) - } - - /// raw 바이트 다운로드 - pub async fn download( - &self, - key: &str, - ) -> Result, Box> { - let resp = self - .client - .get_object() - .bucket(&self.bucket) - .key(key) - .send() - .await?; - - let data = resp.body.collect().await?; - Ok(data.into_bytes().to_vec()) - } - - /// 오브젝트 삭제 - pub async fn delete(&self, key: &str) -> Result<(), S3Error> { - self.client - .delete_object() - .bucket(&self.bucket) - .key(key) - .send() - .await?; - Ok(()) - } - - /// 오브젝트 존재 여부 확인 - pub async fn exists( - &self, - key: &str, - ) -> Result> { - match self - .client - .head_object() - .bucket(&self.bucket) - .key(key) - .send() - .await - { - Ok(_) => Ok(true), - Err(err) => match &err { - SdkError::ServiceError(service_err) => { - if service_err.err().is_not_found() { - Ok(false) - } else { - Err(Box::new(err)) - } - } - _ => Err(Box::new(err)), - }, - } - } -} - -/// 버킷 이름 (하드코딩 - 변경할 이유 없음) -const BUCKET_NAME: &str = "axumkit-content"; - -pub async fn establish_seaweedfs_connection() --> Result> { - let config = ServerConfig::get(); - - info!("Connecting to SeaweedFS at: {}", config.seaweedfs_endpoint); - - // SeaweedFS S3 API - 내부 네트워크, 인증 없음 - let aws_config = aws_config::defaults(BehaviorVersion::latest()) - .region(Region::new("us-east-1")) - .endpoint_url(&config.seaweedfs_endpoint) - .credentials_provider(aws_sdk_s3::config::Credentials::new( - "", - "", - None, - None, - "anonymous", - )) - .load() - .await; - - let s3_config = aws_sdk_s3::config::Builder::from(&aws_config) - .force_path_style(true) - .build(); - - let client = Client::from_conf(s3_config); - let seaweedfs_client = SeaweedFsClient::new(client, BUCKET_NAME.to_string()); - - // 버킷 생성 (없으면) - if seaweedfs_client - .client - .head_bucket() - .bucket(BUCKET_NAME) - .send() - .await - .is_err() - { - info!("Creating SeaweedFS bucket: {}", BUCKET_NAME); - seaweedfs_client - .client - .create_bucket() - .bucket(BUCKET_NAME) - .send() - .await?; - } - - info!("Successfully connected to SeaweedFS"); - Ok(seaweedfs_client) -} diff --git a/crates/axumkit-server/src/main.rs b/crates/axumkit-server/src/main.rs index 4b2334f..295c934 100644 --- a/crates/axumkit-server/src/main.rs +++ b/crates/axumkit-server/src/main.rs @@ -1,12 +1,11 @@ use axum::error_handling::HandleErrorLayer; -use axum::handler::Handler; use axum::{Router, extract::DefaultBodyLimit, middleware}; use axumkit_config::ServerConfig; use axumkit_dto::action_logs::ActionLogResponse; use axumkit_server::api::routes::api_routes; use axumkit_server::connection::{ MeilisearchClient, create_http_client, establish_r2_connection, establish_read_connection, - establish_redis_connection, establish_seaweedfs_connection, establish_write_connection, + establish_redis_connection, establish_write_connection, }; use axumkit_server::eventstream::start_eventstream_subscriber; use axumkit_server::middleware::anonymous_user::anonymous_user_middleware; @@ -36,10 +35,6 @@ pub async fn run_server() -> anyhow::Result<()> { error!("Failed to establish cloudflare_r2 connection: {}", e); anyhow::anyhow!("R2 connection failed: {}", e) })?; - let seaweedfs_client = establish_seaweedfs_connection().await.map_err(|e| { - error!("Failed to establish SeaweedFS connection: {}", e); - anyhow::anyhow!("SeaweedFS connection failed: {}", e) - })?; let redis_session = establish_redis_connection( &ServerConfig::get().redis_session_host, &ServerConfig::get().redis_session_port, @@ -102,7 +97,6 @@ pub async fn run_server() -> anyhow::Result<()> { write_db, read_db, r2_client, - seaweedfs_client, redis_session, redis_cache, worker, diff --git a/crates/axumkit-server/src/repository/mod.rs b/crates/axumkit-server/src/repository/mod.rs index b29783f..a029e3d 100644 --- a/crates/axumkit-server/src/repository/mod.rs +++ b/crates/axumkit-server/src/repository/mod.rs @@ -1,4 +1,3 @@ pub mod action_logs; pub mod oauth; -pub mod posts; pub mod user; diff --git a/crates/axumkit-server/src/repository/posts/create.rs b/crates/axumkit-server/src/repository/posts/create.rs deleted file mode 100644 index e440cb5..0000000 --- a/crates/axumkit-server/src/repository/posts/create.rs +++ /dev/null @@ -1,27 +0,0 @@ -use axumkit_entity::posts::{ActiveModel as PostActiveModel, Model as PostModel}; -use axumkit_errors::errors::Errors; -use sea_orm::{ActiveModelTrait, ConnectionTrait, Set}; -use uuid::Uuid; - -pub async fn repository_create_post( - conn: &C, - author_id: Uuid, - title: String, - storage_key: String, -) -> Result -where - C: ConnectionTrait, -{ - let new_post = PostActiveModel { - id: Default::default(), - author_id: Set(author_id), - title: Set(title), - storage_key: Set(storage_key), - created_at: Default::default(), - updated_at: Default::default(), - }; - - let post = new_post.insert(conn).await?; - - Ok(post) -} diff --git a/crates/axumkit-server/src/repository/posts/delete.rs b/crates/axumkit-server/src/repository/posts/delete.rs deleted file mode 100644 index f0c6431..0000000 --- a/crates/axumkit-server/src/repository/posts/delete.rs +++ /dev/null @@ -1,18 +0,0 @@ -use axumkit_entity::posts::Entity as PostEntity; -use axumkit_errors::errors::Errors; -use sea_orm::{ConnectionTrait, EntityTrait, ModelTrait}; -use uuid::Uuid; - -pub async fn repository_delete_post(conn: &C, id: Uuid) -> Result<(), Errors> -where - C: ConnectionTrait, -{ - let post = PostEntity::find_by_id(id) - .one(conn) - .await? - .ok_or(Errors::PostNotFound)?; - - post.delete(conn).await?; - - Ok(()) -} diff --git a/crates/axumkit-server/src/repository/posts/find_by_author.rs b/crates/axumkit-server/src/repository/posts/find_by_author.rs deleted file mode 100644 index 2025c43..0000000 --- a/crates/axumkit-server/src/repository/posts/find_by_author.rs +++ /dev/null @@ -1,20 +0,0 @@ -use axumkit_entity::posts::{Column, Entity as PostEntity, Model as PostModel}; -use axumkit_errors::errors::Errors; -use sea_orm::{ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter, QueryOrder}; -use uuid::Uuid; - -pub async fn repository_find_posts_by_author( - conn: &C, - author_id: Uuid, -) -> Result, Errors> -where - C: ConnectionTrait, -{ - let posts = PostEntity::find() - .filter(Column::AuthorId.eq(author_id)) - .order_by_desc(Column::CreatedAt) - .all(conn) - .await?; - - Ok(posts) -} diff --git a/crates/axumkit-server/src/repository/posts/find_by_id.rs b/crates/axumkit-server/src/repository/posts/find_by_id.rs deleted file mode 100644 index 90aaed9..0000000 --- a/crates/axumkit-server/src/repository/posts/find_by_id.rs +++ /dev/null @@ -1,12 +0,0 @@ -use axumkit_entity::posts::{Entity as PostEntity, Model as PostModel}; -use axumkit_errors::errors::Errors; -use sea_orm::{ConnectionTrait, EntityTrait}; -use uuid::Uuid; - -pub async fn repository_find_post_by_id(conn: &C, id: Uuid) -> Result, Errors> -where - C: ConnectionTrait, -{ - let post = PostEntity::find_by_id(id).one(conn).await?; - Ok(post) -} diff --git a/crates/axumkit-server/src/repository/posts/get_by_id.rs b/crates/axumkit-server/src/repository/posts/get_by_id.rs deleted file mode 100644 index 295eefb..0000000 --- a/crates/axumkit-server/src/repository/posts/get_by_id.rs +++ /dev/null @@ -1,15 +0,0 @@ -use axumkit_entity::posts::Model as PostModel; -use axumkit_errors::errors::Errors; -use sea_orm::ConnectionTrait; -use uuid::Uuid; - -use super::find_by_id::repository_find_post_by_id; - -pub async fn repository_get_post_by_id(conn: &C, id: Uuid) -> Result -where - C: ConnectionTrait, -{ - repository_find_post_by_id(conn, id) - .await? - .ok_or(Errors::PostNotFound) -} diff --git a/crates/axumkit-server/src/repository/posts/list.rs b/crates/axumkit-server/src/repository/posts/list.rs deleted file mode 100644 index 1aebc71..0000000 --- a/crates/axumkit-server/src/repository/posts/list.rs +++ /dev/null @@ -1,21 +0,0 @@ -use axumkit_entity::posts::{Column, Entity as PostEntity, Model as PostModel}; -use axumkit_errors::errors::Errors; -use sea_orm::{ConnectionTrait, EntityTrait, QueryOrder, QuerySelect}; - -pub async fn repository_list_posts( - conn: &C, - limit: u64, - offset: u64, -) -> Result, Errors> -where - C: ConnectionTrait, -{ - let posts = PostEntity::find() - .order_by_desc(Column::CreatedAt) - .offset(offset) - .limit(limit) - .all(conn) - .await?; - - Ok(posts) -} diff --git a/crates/axumkit-server/src/repository/posts/mod.rs b/crates/axumkit-server/src/repository/posts/mod.rs deleted file mode 100644 index eda6667..0000000 --- a/crates/axumkit-server/src/repository/posts/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub mod create; -pub mod delete; -pub mod find_by_author; -pub mod find_by_id; -pub mod get_by_id; -pub mod list; -pub mod update; - -pub use create::repository_create_post; -pub use delete::repository_delete_post; -pub use find_by_author::repository_find_posts_by_author; -pub use find_by_id::repository_find_post_by_id; -pub use get_by_id::repository_get_post_by_id; -pub use list::repository_list_posts; -pub use update::{PostUpdateParams, repository_update_post}; diff --git a/crates/axumkit-server/src/repository/posts/update.rs b/crates/axumkit-server/src/repository/posts/update.rs deleted file mode 100644 index f69732d..0000000 --- a/crates/axumkit-server/src/repository/posts/update.rs +++ /dev/null @@ -1,34 +0,0 @@ -use axumkit_entity::posts::{ActiveModel as PostActiveModel, Model as PostModel}; -use axumkit_errors::errors::Errors; -use chrono::Utc; -use sea_orm::{ActiveModelTrait, ConnectionTrait, IntoActiveModel, Set}; - -pub struct PostUpdateParams { - pub title: Option, - pub storage_key: Option, -} - -pub async fn repository_update_post( - conn: &C, - post: PostModel, - params: PostUpdateParams, -) -> Result -where - C: ConnectionTrait, -{ - let mut active_model: PostActiveModel = post.into_active_model(); - - if let Some(title) = params.title { - active_model.title = Set(title); - } - - if let Some(storage_key) = params.storage_key { - active_model.storage_key = Set(storage_key); - } - - active_model.updated_at = Set(Utc::now().into()); - - let updated_post = active_model.update(conn).await?; - - Ok(updated_post) -} diff --git a/crates/axumkit-server/src/service/auth/change_email.rs b/crates/axumkit-server/src/service/auth/change_email.rs index 0ffb3eb..d8bd5c9 100644 --- a/crates/axumkit-server/src/service/auth/change_email.rs +++ b/crates/axumkit-server/src/service/auth/change_email.rs @@ -1,17 +1,17 @@ -use crate::bridge::worker_client; +use crate::bridge::worker_client; use crate::repository::user::{repository_find_user_by_email, repository_get_user_by_id}; use crate::state::WorkerClient; use crate::utils::crypto::password::verify_password; use crate::utils::crypto::token::generate_secure_token; -use crate::utils::redis_cache::set_json_with_ttl; -use axumkit_config::ServerConfig; -use axumkit_dto::auth::request::ChangeEmailRequest; -use axumkit_errors::errors::{Errors, ServiceResult}; +use crate::utils::redis_cache::issue_token_and_store_json_with_ttl; use redis::aio::ConnectionManager; use sea_orm::ConnectionTrait; use serde::{Deserialize, Serialize}; use tracing::info; use uuid::Uuid; +use axumkit_config::ServerConfig; +use axumkit_dto::auth::request::ChangeEmailRequest; +use axumkit_errors::errors::{Errors, ServiceResult}; #[derive(Debug, Serialize, Deserialize)] pub struct EmailChangeData { @@ -19,7 +19,6 @@ pub struct EmailChangeData { pub new_email: String, } -/// 이메일 변경을 요청합니다. 새 이메일로 인증 메일이 발송됩니다. pub async fn service_change_email( conn: &C, redis_conn: &ConnectionManager, @@ -32,21 +31,17 @@ where { let config = ServerConfig::get(); - // 1. 사용자 조회 let user = repository_get_user_by_id(conn, user_id).await?; - // 2. 비밀번호 검증 (OAuth 전용 사용자는 비밀번호 변경 불가) let password_hash = user.password.ok_or(Errors::UserPasswordNotSet)?; verify_password(&payload.password, &password_hash)?; - // 3. 새 이메일이 현재 이메일과 동일한지 확인 if user.email == payload.new_email { return Err(Errors::BadRequestError( "New email must be different from current email.".to_string(), )); } - // 4. 새 이메일이 이미 사용 중인지 확인 if repository_find_user_by_email(conn, payload.new_email.clone()) .await? .is_some() @@ -54,20 +49,22 @@ where return Err(Errors::UserEmailAlreadyExists); } - // 5. 토큰 생성 - let token = generate_secure_token(); - let token_key = format!("email_change:{}", token); let change_data = EmailChangeData { user_id: user.id.to_string(), new_email: payload.new_email.clone(), }; - // 6. Redis에 토큰 저장 (분 단위 → 초 단위 변환) let ttl_seconds = (config.auth_email_change_token_expire_time * 60) as u64; - set_json_with_ttl(redis_conn, &token_key, &change_data, ttl_seconds).await?; + let token = issue_token_and_store_json_with_ttl( + redis_conn, + generate_secure_token, + axumkit_constants::email_change_key, + &change_data, + ttl_seconds, + ) + .await?; - // 7. Worker 서비스에 이메일 발송 요청 (새 이메일로) worker_client::send_email_change_verification( worker, &payload.new_email, @@ -77,10 +74,8 @@ where ) .await?; - info!( - "Email change verification sent to {} for user {}", - payload.new_email, user_id - ); + info!(user_id = %user_id, "Email change verification sent"); Ok(()) } + diff --git a/crates/axumkit-server/src/service/auth/change_password.rs b/crates/axumkit-server/src/service/auth/change_password.rs index 54333b1..a91c97d 100644 --- a/crates/axumkit-server/src/service/auth/change_password.rs +++ b/crates/axumkit-server/src/service/auth/change_password.rs @@ -1,55 +1,42 @@ -use crate::repository::user::UserUpdateParams; +use crate::repository::user::UserUpdateParams; use crate::repository::user::repository_get_user_by_id; use crate::repository::user::repository_update_user; use crate::service::auth::session::SessionService; use crate::utils::crypto::password::{hash_password, verify_password}; -use axumkit_dto::auth::request::ChangePasswordRequest; -use axumkit_errors::errors::{Errors, ServiceResult}; use redis::aio::ConnectionManager; -use sea_orm::ConnectionTrait; +use sea_orm::{DatabaseConnection, TransactionTrait}; use tracing::info; use uuid::Uuid; +use axumkit_dto::auth::request::ChangePasswordRequest; +use axumkit_errors::errors::{Errors, ServiceResult}; -/// 비밀번호를 변경합니다. /// /// # Arguments -/// * `conn` - 데이터베이스 연결 -/// * `redis_conn` - Redis 연결 -/// * `user_id` - 사용자 ID -/// * `session_id` - 현재 세션 ID (유지할 세션) -/// * `payload` - 비밀번호 변경 요청 -pub async fn service_change_password( - conn: &C, +pub async fn service_change_password( + conn: &DatabaseConnection, redis_conn: &ConnectionManager, user_id: Uuid, session_id: &str, payload: ChangePasswordRequest, -) -> ServiceResult<()> -where - C: ConnectionTrait, -{ - // 1. 사용자 조회 - let user = repository_get_user_by_id(conn, user_id).await?; +) -> ServiceResult<()> { + let txn = conn.begin().await?; + + let user = repository_get_user_by_id(&txn, user_id).await?; - // 2. 비밀번호가 설정되어 있는지 확인 (OAuth 전용 사용자 제외) let password_hash = user.password.ok_or(Errors::UserPasswordNotSet)?; - // 3. 현재 비밀번호 검증 verify_password(&payload.current_password, &password_hash)?; - // 4. 새 비밀번호가 현재 비밀번호와 동일한지 확인 if payload.current_password == payload.new_password { return Err(Errors::BadRequestError( "New password must be different from current password.".to_string(), )); } - // 5. 새 비밀번호 해싱 let new_password_hash = hash_password(&payload.new_password)?; - // 6. 비밀번호 업데이트 repository_update_user( - conn, + &txn, user_id, UserUpdateParams { password: Some(Some(new_password_hash)), @@ -58,14 +45,13 @@ where ) .await?; - // 7. 현재 세션을 제외한 모든 세션 무효화 + txn.commit().await?; + let deleted_count = SessionService::delete_other_sessions(redis_conn, &user_id.to_string(), session_id).await?; - info!( - "Password changed for user {}, {} other sessions invalidated", - user_id, deleted_count - ); + info!(user_id = %user_id, invalidated_sessions = deleted_count, "Password changed"); Ok(()) } + diff --git a/crates/axumkit-server/src/service/auth/confirm_email_change.rs b/crates/axumkit-server/src/service/auth/confirm_email_change.rs index 9162d48..6f57b8e 100644 --- a/crates/axumkit-server/src/service/auth/confirm_email_change.rs +++ b/crates/axumkit-server/src/service/auth/confirm_email_change.rs @@ -1,52 +1,42 @@ -use crate::repository::user::{ +use crate::repository::user::{ UserUpdateParams, repository_find_user_by_email, repository_update_user, }; use crate::service::auth::change_email::EmailChangeData; -use axumkit_errors::errors::{Errors, ServiceResult}; -use redis::AsyncCommands; +use crate::utils::redis_cache::get_json_and_delete; use redis::aio::ConnectionManager; -use sea_orm::ConnectionTrait; +use sea_orm::{DatabaseConnection, TransactionTrait}; use tracing::info; use uuid::Uuid; +use axumkit_errors::errors::{Errors, ServiceResult}; -/// 이메일 변경을 확인합니다. -pub async fn service_confirm_email_change( - conn: &C, +pub async fn service_confirm_email_change( + conn: &DatabaseConnection, redis_conn: &ConnectionManager, token: &str, -) -> ServiceResult<()> -where - C: ConnectionTrait, -{ - // 1. Redis에서 토큰 검증 (get_del로 일회용) - let token_key = format!("email_change:{}", token); - let mut redis_mut = redis_conn.clone(); - - let token_json: Option = redis_mut - .get_del(&token_key) - .await - .map_err(|e| Errors::SysInternalError(format!("Redis error: {}", e)))?; - - let token_data = token_json.ok_or(Errors::TokenInvalidEmailChange)?; - - let change_data: EmailChangeData = - serde_json::from_str(&token_data).map_err(|_| Errors::TokenInvalidEmailChange)?; +) -> ServiceResult<()> { + let token_key = axumkit_constants::email_change_key(token); + let change_data: EmailChangeData = get_json_and_delete( + redis_conn, + &token_key, + || Errors::TokenInvalidEmailChange, + |_| Errors::TokenInvalidEmailChange, + ) + .await?; - // 2. user_id 파싱 let user_id = Uuid::parse_str(&change_data.user_id).map_err(|_| Errors::TokenInvalidEmailChange)?; - // 3. 이메일 중복 체크 (토큰 발급 후 다른 사용자가 해당 이메일을 사용했을 수 있음) + let txn = conn.begin().await?; + if let Some(existing) = - repository_find_user_by_email(conn, change_data.new_email.clone()).await? + repository_find_user_by_email(&txn, change_data.new_email.clone()).await? && existing.id != user_id { return Err(Errors::UserEmailAlreadyExists); } - // 4. 이메일 업데이트 (verified_at도 현재 시간으로 설정 - 이메일 인증 완료로 간주) repository_update_user( - conn, + &txn, user_id, UserUpdateParams { email: Some(change_data.new_email.clone()), @@ -56,10 +46,10 @@ where ) .await?; - info!( - "Email changed successfully for user {} to {}", - user_id, change_data.new_email - ); + txn.commit().await?; + + info!(user_id = %user_id, "Email changed"); Ok(()) } + diff --git a/crates/axumkit-server/src/service/auth/forgot_password.rs b/crates/axumkit-server/src/service/auth/forgot_password.rs index c922f2c..5f679c8 100644 --- a/crates/axumkit-server/src/service/auth/forgot_password.rs +++ b/crates/axumkit-server/src/service/auth/forgot_password.rs @@ -1,24 +1,21 @@ -use crate::bridge::worker_client; +use crate::bridge::worker_client; use crate::repository::user::repository_find_user_by_email; use crate::state::WorkerClient; use crate::utils::crypto::token::generate_secure_token; -use crate::utils::redis_cache::set_json_with_ttl; -use axumkit_config::ServerConfig; -use axumkit_errors::errors::ServiceResult; +use crate::utils::redis_cache::issue_token_and_store_json_with_ttl; use redis::aio::ConnectionManager; use sea_orm::ConnectionTrait; use serde::{Deserialize, Serialize}; use tracing::info; +use axumkit_config::ServerConfig; +use axumkit_errors::errors::ServiceResult; -/// Redis에 저장되는 비밀번호 재설정 토큰 데이터 #[derive(Debug, Serialize, Deserialize)] pub struct PasswordResetData { pub user_id: String, } -/// 비밀번호 재설정 이메일을 발송합니다. /// -/// 보안: 이메일 존재 여부와 관계없이 항상 성공을 반환합니다. pub async fn service_forgot_password( conn: &C, redis_conn: &ConnectionManager, @@ -30,40 +27,36 @@ where { let config = ServerConfig::get(); - // 1. 이메일로 사용자 조회 let user = repository_find_user_by_email(conn, email.to_string()).await?; - // 2. 사용자가 없으면 조용히 반환 (이메일 존재 여부 노출 방지) let user = match user { Some(u) => u, None => { - info!("Password reset requested for non-existent email: {}", email); + info!("Password reset requested for non-existent email"); return Ok(()); } }; - // 3. 비밀번호가 설정되지 않은 사용자는 조용히 반환 if user.password.is_none() { - info!( - "Password reset requested for user without password: {}", - email - ); + info!("Password reset requested for user without password"); return Ok(()); } - // 4. 토큰 생성 - let token = generate_secure_token(); - let token_key = format!("password_reset:{}", token); let reset_data = PasswordResetData { user_id: user.id.to_string(), }; - // 5. Redis에 토큰 저장 (분 단위 → 초 단위 변환) let ttl_seconds = (config.auth_password_reset_token_expire_time * 60) as u64; - set_json_with_ttl(redis_conn, &token_key, &reset_data, ttl_seconds).await?; + let token = issue_token_and_store_json_with_ttl( + redis_conn, + generate_secure_token, + axumkit_constants::password_reset_key, + &reset_data, + ttl_seconds, + ) + .await?; - // 6. Worker 서비스에 이메일 발송 요청 worker_client::send_password_reset_email( worker, &user.email, @@ -73,7 +66,8 @@ where ) .await?; - info!("Password reset email sent to: {}", email); + info!("Password reset email sent"); Ok(()) } + diff --git a/crates/axumkit-server/src/service/auth/login.rs b/crates/axumkit-server/src/service/auth/login.rs index ae89077..2a840aa 100644 --- a/crates/axumkit-server/src/service/auth/login.rs +++ b/crates/axumkit-server/src/service/auth/login.rs @@ -1,6 +1,7 @@ -use crate::repository::user::repository_find_user_by_email; +use crate::repository::user::repository_find_user_by_email; use crate::service::auth::session::SessionService; use crate::service::auth::totp::TotpTempToken; +use tracing::info; use axumkit_dto::auth::request::LoginRequest; use axumkit_errors::errors::{Errors, ServiceResult}; @@ -8,14 +9,11 @@ use crate::utils::crypto::password::verify_password; use redis::aio::ConnectionManager; use sea_orm::DatabaseConnection; -/// 로그인 결과: 세션 생성 또는 TOTP 필요 pub enum LoginResult { - /// TOTP 없음: 세션 ID 반환 SessionCreated { session_id: String, remember_me: bool, }, - /// TOTP 필요: 임시 토큰 반환 TotpRequired(String), } @@ -26,31 +24,30 @@ pub async fn service_login( user_agent: Option, ip_address: Option, ) -> ServiceResult { - // 사용자 검증 let user = repository_find_user_by_email(conn, payload.email.clone()) .await? - .ok_or(Errors::UserNotFound)?; + .ok_or(Errors::InvalidCredentials)?; - // 비밀번호 검증 - let password_hash = user.password.ok_or(Errors::UserPasswordNotSet)?; - verify_password(&payload.password, &password_hash)?; + let password_hash = user.password.ok_or(Errors::InvalidCredentials)?; + verify_password(&payload.password, &password_hash).map_err(|_| Errors::InvalidCredentials)?; - // TOTP 활성화 확인 if user.totp_enabled_at.is_some() { - // TOTP 필요: 임시 토큰 생성 let temp_token = TotpTempToken::create(redis, user.id, user_agent, ip_address, payload.remember_me) .await?; + info!(user_id = %user.id, "Login requires TOTP"); return Ok(LoginResult::TotpRequired(temp_token.token)); } - // TOTP 없음: 바로 세션 생성 let session = SessionService::create_session(redis, user.id.to_string(), user_agent, ip_address).await?; + info!(user_id = %user.id, "Login successful"); + Ok(LoginResult::SessionCreated { session_id: session.session_id, remember_me: payload.remember_me, }) } + diff --git a/crates/axumkit-server/src/service/auth/logout.rs b/crates/axumkit-server/src/service/auth/logout.rs index bc71385..1c9acd2 100644 --- a/crates/axumkit-server/src/service/auth/logout.rs +++ b/crates/axumkit-server/src/service/auth/logout.rs @@ -1,10 +1,13 @@ -use crate::service::auth::session::SessionService; -use axumkit_errors::errors::ServiceResult; +use crate::service::auth::session::SessionService; use redis::aio::ConnectionManager; +use tracing::info; +use axumkit_errors::errors::ServiceResult; pub async fn service_logout(redis: &ConnectionManager, session_id: &str) -> ServiceResult<()> { - // 세션 삭제 (delete_session 내부에서 유효성 확인) SessionService::delete_session(redis, session_id).await?; + info!(session_id = %session_id, "Logout"); + Ok(()) } + diff --git a/crates/axumkit-server/src/service/auth/mod.rs b/crates/axumkit-server/src/service/auth/mod.rs index 322a48a..457b85f 100644 --- a/crates/axumkit-server/src/service/auth/mod.rs +++ b/crates/axumkit-server/src/service/auth/mod.rs @@ -1,4 +1,4 @@ -pub mod change_email; +pub mod change_email; pub mod change_password; pub mod confirm_email_change; pub mod forgot_password; @@ -13,3 +13,4 @@ pub mod verify_email; pub use login::LoginResult; pub use session_types::{Session, SessionContext}; + diff --git a/crates/axumkit-server/src/service/auth/resend_verification_email.rs b/crates/axumkit-server/src/service/auth/resend_verification_email.rs index d8d3b1e..cc1ab27 100644 --- a/crates/axumkit-server/src/service/auth/resend_verification_email.rs +++ b/crates/axumkit-server/src/service/auth/resend_verification_email.rs @@ -1,25 +1,20 @@ -use crate::bridge::worker_client; +use crate::bridge::worker_client; use crate::repository::user::repository_get_user_by_id; use crate::service::auth::verify_email::EmailVerificationData; use crate::state::WorkerClient; use crate::utils::crypto::token::generate_secure_token; -use crate::utils::redis_cache::set_json_with_ttl; -use axumkit_config::ServerConfig; -use axumkit_errors::errors::{Errors, ServiceResult}; +use crate::utils::redis_cache::issue_token_and_store_json_with_ttl; use redis::aio::ConnectionManager; use sea_orm::ConnectionTrait; +use tracing::info; use uuid::Uuid; +use axumkit_config::ServerConfig; +use axumkit_errors::errors::{Errors, ServiceResult}; -/// 이메일 인증 메일을 재발송합니다. /// /// # Arguments -/// * `conn` - 데이터베이스 연결 -/// * `redis_conn` - Redis 연결 -/// * `worker` - Worker Redis 연결 -/// * `user_id` - 사용자 ID /// /// # Returns -/// * `()` - 성공 시 pub async fn service_resend_verification_email( conn: &C, redis_conn: &ConnectionManager, @@ -31,33 +26,32 @@ where { let config = ServerConfig::get(); - // 1. 사용자 조회 let user = repository_get_user_by_id(conn, user_id).await?; - // 2. 이미 인증된 사용자인지 확인 if user.verified_at.is_some() { return Err(Errors::EmailAlreadyVerified); } - // 3. OAuth 전용 사용자인지 확인 (password가 없으면 OAuth 사용자) if user.password.is_none() { return Err(Errors::UserPasswordNotSet); } - // 4. 새 토큰 생성 (암호학적으로 안전한 랜덤 토큰) - let token = generate_secure_token(); - let token_key = format!("email_verification:{}", token); let verification_data = EmailVerificationData { user_id: user.id.to_string(), email: user.email.clone(), }; - // 5. Redis에 토큰 저장 (분 단위 → 초 단위 변환) let ttl_seconds = (config.auth_email_verification_token_expire_time * 60) as u64; - set_json_with_ttl(redis_conn, &token_key, &verification_data, ttl_seconds).await?; + let token = issue_token_and_store_json_with_ttl( + redis_conn, + generate_secure_token, + axumkit_constants::email_verification_key, + &verification_data, + ttl_seconds, + ) + .await?; - // 6. Worker 서비스에 이메일 발송 요청 worker_client::send_verification_email( worker, &user.email, @@ -67,5 +61,8 @@ where ) .await?; + info!(user_id = %user_id, "Verification email resent"); + Ok(()) } + diff --git a/crates/axumkit-server/src/service/auth/reset_password.rs b/crates/axumkit-server/src/service/auth/reset_password.rs index 3d83310..ff9ce8e 100644 --- a/crates/axumkit-server/src/service/auth/reset_password.rs +++ b/crates/axumkit-server/src/service/auth/reset_password.rs @@ -1,21 +1,16 @@ -use crate::repository::user::{UserUpdateParams, repository_update_user}; +use crate::repository::user::{UserUpdateParams, repository_update_user}; use crate::service::auth::forgot_password::PasswordResetData; use crate::service::auth::session::SessionService; use crate::utils::crypto::password::hash_password; -use axumkit_errors::errors::{Errors, ServiceResult}; -use redis::AsyncCommands; +use crate::utils::redis_cache::get_json_and_delete; use redis::aio::ConnectionManager; use sea_orm::ConnectionTrait; use tracing::info; use uuid::Uuid; +use axumkit_errors::errors::{Errors, ServiceResult}; -/// 비밀번호를 재설정합니다. /// /// # Arguments -/// * `conn` - 데이터베이스 연결 -/// * `redis_conn` - Redis 연결 -/// * `token` - 비밀번호 재설정 토큰 -/// * `new_password` - 새 비밀번호 pub async fn service_reset_password( conn: &C, redis_conn: &ConnectionManager, @@ -25,27 +20,19 @@ pub async fn service_reset_password( where C: ConnectionTrait, { - // 1. Redis에서 토큰 검증 (get_del로 일회용) - let token_key = format!("password_reset:{}", token); - let mut redis_mut = redis_conn.clone(); - - let token_json: Option = redis_mut - .get_del(&token_key) - .await - .map_err(|e| Errors::SysInternalError(format!("Redis error: {}", e)))?; - - let token_data = token_json.ok_or(Errors::TokenInvalidReset)?; - - let reset_data: PasswordResetData = - serde_json::from_str(&token_data).map_err(|_| Errors::TokenInvalidReset)?; + let token_key = axumkit_constants::password_reset_key(token); + let reset_data: PasswordResetData = get_json_and_delete( + redis_conn, + &token_key, + || Errors::TokenInvalidReset, + |_| Errors::TokenInvalidReset, + ) + .await?; - // 2. user_id 파싱 let user_id = Uuid::parse_str(&reset_data.user_id).map_err(|_| Errors::TokenInvalidReset)?; - // 3. 새 비밀번호 해싱 let password_hash = hash_password(new_password)?; - // 4. 비밀번호 업데이트 repository_update_user( conn, user_id, @@ -56,14 +43,11 @@ where ) .await?; - // 5. 해당 사용자의 모든 세션 무효화 let deleted_count = SessionService::delete_all_user_sessions(redis_conn, &user_id.to_string()).await?; - info!( - "Password reset completed for user {}, {} sessions invalidated", - user_id, deleted_count - ); + info!(user_id = %user_id, invalidated_sessions = deleted_count, "Password reset completed"); Ok(()) } + diff --git a/crates/axumkit-server/src/service/auth/session.rs b/crates/axumkit-server/src/service/auth/session.rs index 27a0703..24ac320 100644 --- a/crates/axumkit-server/src/service/auth/session.rs +++ b/crates/axumkit-server/src/service/auth/session.rs @@ -1,10 +1,10 @@ -use crate::service::auth::session_types::Session; -use axumkit_config::ServerConfig; -use axumkit_errors::errors::Errors; +use crate::service::auth::session_types::Session; use chrono::Utc; use redis::AsyncCommands; use redis::aio::ConnectionManager as RedisClient; use std::collections::HashSet; +use axumkit_config::ServerConfig; +use axumkit_errors::errors::Errors; pub struct SessionService; @@ -116,7 +116,6 @@ impl SessionService { Errors::SysInternalError(format!("Redis session retrieval failed: {}", e)) })?; - // Redis TTL이 만료를 처리하므로 키가 존재하면 유효한 세션 match session_data { Some(data) => { let session: Session = serde_json::from_str(&data).map_err(|e| { @@ -164,7 +163,6 @@ impl SessionService { Ok(()) } - /// 세션 TTL 연장 (최대 수명 체크 포함) pub async fn refresh_session( redis: &RedisClient, session: &Session, @@ -172,12 +170,10 @@ impl SessionService { let config = ServerConfig::get(); let now = Utc::now(); - // 최대 수명 초과 시 연장 불가 if now >= session.max_expires_at { return Ok(None); } - // 새 만료 시간 = min(now + sliding_ttl, max_expires_at) let sliding_expiry = now + chrono::Duration::hours(config.auth_session_sliding_ttl_hours); let new_expires_at = sliding_expiry.min(session.max_expires_at); @@ -211,7 +207,6 @@ impl SessionService { Ok(Some(refreshed_session)) } - /// 조건부 세션 연장 (임계값 체크 + 최대 수명 체크) pub async fn maybe_refresh_session( redis: &RedisClient, session: &Session, @@ -230,7 +225,6 @@ impl SessionService { } } - /// 특정 사용자의 모든 세션 삭제 (비밀번호 재설정 시 사용) pub async fn delete_all_user_sessions( redis: &RedisClient, user_id: &str, @@ -255,7 +249,6 @@ impl SessionService { Ok(count) } - /// 현재 세션을 제외한 모든 세션 삭제 (비밀번호 변경 시 사용) pub async fn delete_other_sessions( redis: &RedisClient, user_id: &str, @@ -290,3 +283,4 @@ impl SessionService { Ok(count) } } + diff --git a/crates/axumkit-server/src/service/auth/session_types.rs b/crates/axumkit-server/src/service/auth/session_types.rs index 2baeb02..6ee9dca 100644 --- a/crates/axumkit-server/src/service/auth/session_types.rs +++ b/crates/axumkit-server/src/service/auth/session_types.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, Duration, Utc}; +use chrono::{DateTime, Duration, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -46,12 +46,10 @@ impl Session { self } - /// 세션을 연장할 수 있는지 확인 (최대 수명 체크) pub fn can_refresh(&self) -> bool { Utc::now() < self.max_expires_at } - /// 세션 연장이 필요한지 확인 (TTL 임계값 체크) pub fn needs_refresh(&self, threshold_percent: u8, sliding_ttl_hours: i64) -> bool { let now = Utc::now(); let remaining = (self.expires_at - now).num_seconds(); @@ -113,3 +111,4 @@ mod tests { assert!(!session.needs_refresh(50, 0)); } } + diff --git a/crates/axumkit-server/src/service/auth/totp/backup_codes.rs b/crates/axumkit-server/src/service/auth/totp/backup_codes.rs index cd6f32e..ab726c0 100644 --- a/crates/axumkit-server/src/service/auth/totp/backup_codes.rs +++ b/crates/axumkit-server/src/service/auth/totp/backup_codes.rs @@ -1,46 +1,37 @@ -use super::common::{generate_backup_codes, verify_totp_code}; +use super::common::{generate_backup_codes, verify_totp_code}; use crate::repository::user::{ UserUpdateParams, repository_get_user_by_id, repository_update_user, }; use crate::utils::crypto::backup_code::hash_backup_codes; +use sea_orm::{DatabaseConnection, TransactionTrait}; +use uuid::Uuid; use axumkit_dto::auth::response::TotpBackupCodesResponse; use axumkit_errors::errors::{Errors, ServiceResult}; -use sea_orm::ConnectionTrait; -use uuid::Uuid; -/// 백업 코드 재생성: 현재 TOTP 코드 검증 후 새 백업 코드 생성 -pub async fn service_regenerate_backup_codes( - conn: &C, +pub async fn service_regenerate_backup_codes( + conn: &DatabaseConnection, user_id: Uuid, - email: &str, code: &str, -) -> ServiceResult -where - C: ConnectionTrait, -{ - // 사용자 조회 - let user = repository_get_user_by_id(conn, user_id).await?; - - // TOTP가 활성화되어 있어야 함 +) -> ServiceResult { + let txn = conn.begin().await?; + + let user = repository_get_user_by_id(&txn, user_id).await?; + if user.totp_enabled_at.is_none() { return Err(Errors::TotpNotEnabled); } let secret_base32 = user.totp_secret.clone().ok_or(Errors::TotpNotEnabled)?; - // TOTP 코드 검증 (백업 코드 재생성은 반드시 TOTP 코드로만) - if !verify_totp_code(&secret_base32, email, code)? { + if !verify_totp_code(&secret_base32, &user.email, code)? { return Err(Errors::TotpInvalidCode); } - // 새 백업 코드 생성 (평문) let backup_codes = generate_backup_codes(); - // 해시하여 DB에 저장 let hashed_codes = hash_backup_codes(&backup_codes); - // DB 업데이트 repository_update_user( - conn, + &txn, user_id, UserUpdateParams { totp_backup_codes: Some(Some(hashed_codes)), @@ -49,6 +40,8 @@ where ) .await?; - // 평문 백업 코드 반환 (사용자가 저장해야 함) + txn.commit().await?; + Ok(TotpBackupCodesResponse { backup_codes }) } + diff --git a/crates/axumkit-server/src/service/auth/totp/common.rs b/crates/axumkit-server/src/service/auth/totp/common.rs index 564ad55..91afdbb 100644 --- a/crates/axumkit-server/src/service/auth/totp/common.rs +++ b/crates/axumkit-server/src/service/auth/totp/common.rs @@ -1,13 +1,12 @@ -use axumkit_errors::errors::{Errors, ServiceResult}; -use rand::Rng; +use rand::RngExt; use totp_rs::{Algorithm, Secret, TOTP}; +use axumkit_errors::errors::{Errors, ServiceResult}; pub const ISSUER: &str = "Sevenwiki"; pub const BACKUP_CODE_COUNT: usize = 10; pub const BACKUP_CODE_LENGTH: usize = 8; const BACKUP_CODE_CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; -/// TOTP 코드 검증 pub fn verify_totp_code(secret_base32: &str, email: &str, code: &str) -> ServiceResult { let secret = Secret::Encoded(secret_base32.to_string()) .to_bytes() @@ -27,7 +26,6 @@ pub fn verify_totp_code(secret_base32: &str, email: &str, code: &str) -> Service Ok(totp.check_current(code).unwrap_or(false)) } -/// 백업 코드 생성 (10개, 8자리 영숫자) pub fn generate_backup_codes() -> Vec { let mut rng = rand::rng(); (0..BACKUP_CODE_COUNT) @@ -41,3 +39,4 @@ pub fn generate_backup_codes() -> Vec { }) .collect() } + diff --git a/crates/axumkit-server/src/service/auth/totp/disable.rs b/crates/axumkit-server/src/service/auth/totp/disable.rs index 7105604..f45c780 100644 --- a/crates/axumkit-server/src/service/auth/totp/disable.rs +++ b/crates/axumkit-server/src/service/auth/totp/disable.rs @@ -1,26 +1,22 @@ -use super::common::verify_totp_code; +use super::common::verify_totp_code; use crate::repository::user::{ UserUpdateParams, repository_get_user_by_id, repository_update_user, }; use crate::utils::crypto::backup_code::verify_backup_code; -use axumkit_errors::errors::{Errors, ServiceResult}; -use sea_orm::ConnectionTrait; +use sea_orm::{DatabaseConnection, TransactionTrait}; +use tracing::info; use uuid::Uuid; +use axumkit_errors::errors::{Errors, ServiceResult}; -/// TOTP 비활성화: 현재 코드 검증 후 모든 TOTP 필드 초기화 -pub async fn service_totp_disable( - conn: &C, +pub async fn service_totp_disable( + conn: &DatabaseConnection, user_id: Uuid, - email: &str, code: &str, -) -> ServiceResult<()> -where - C: ConnectionTrait, -{ - // 사용자 조회 - let user = repository_get_user_by_id(conn, user_id).await?; - - // TOTP가 활성화되어 있어야 함 +) -> ServiceResult<()> { + let txn = conn.begin().await?; + + let user = repository_get_user_by_id(&txn, user_id).await?; + if user.totp_enabled_at.is_none() { return Err(Errors::TotpNotEnabled); } @@ -28,13 +24,11 @@ where let secret_base32 = user.totp_secret.clone().ok_or(Errors::TotpNotEnabled)?; let backup_codes = user.totp_backup_codes.clone().unwrap_or_default(); - // 코드 검증 (TOTP 6자리 또는 백업 코드 8자리) if code.len() == 6 { - if !verify_totp_code(&secret_base32, email, code)? { + if !verify_totp_code(&secret_base32, &user.email, code)? { return Err(Errors::TotpInvalidCode); } } else if code.len() == 8 { - // 해시 비교로 백업 코드 검증 if verify_backup_code(code, &backup_codes).is_none() { return Err(Errors::TotpInvalidCode); } @@ -42,9 +36,8 @@ where return Err(Errors::TotpInvalidCode); } - // TOTP 비활성화 (모든 필드 초기화) repository_update_user( - conn, + &txn, user_id, UserUpdateParams { totp_secret: Some(None), @@ -55,5 +48,10 @@ where ) .await?; + txn.commit().await?; + + info!(user_id = %user_id, "TOTP disabled"); + Ok(()) } + diff --git a/crates/axumkit-server/src/service/auth/totp/enable.rs b/crates/axumkit-server/src/service/auth/totp/enable.rs index c1ae103..9bb4274 100644 --- a/crates/axumkit-server/src/service/auth/totp/enable.rs +++ b/crates/axumkit-server/src/service/auth/totp/enable.rs @@ -1,48 +1,39 @@ -use super::common::{generate_backup_codes, verify_totp_code}; +use super::common::{generate_backup_codes, verify_totp_code}; use crate::repository::user::{ UserUpdateParams, repository_get_user_by_id, repository_update_user, }; use crate::utils::crypto::backup_code::hash_backup_codes; -use axumkit_dto::auth::response::TotpEnableResponse; -use axumkit_errors::errors::{Errors, ServiceResult}; use chrono::Utc; -use sea_orm::ConnectionTrait; +use sea_orm::{DatabaseConnection, TransactionTrait}; +use tracing::info; use uuid::Uuid; +use axumkit_dto::auth::response::TotpEnableResponse; +use axumkit_errors::errors::{Errors, ServiceResult}; -/// TOTP 활성화: 첫 코드 검증 후 활성화 + 백업 코드 생성 -pub async fn service_totp_enable( - conn: &C, +pub async fn service_totp_enable( + conn: &DatabaseConnection, user_id: Uuid, - email: &str, code: &str, -) -> ServiceResult -where - C: ConnectionTrait, -{ - // 사용자 조회 - let user = repository_get_user_by_id(conn, user_id).await?; - - // 이미 TOTP 활성화된 경우 +) -> ServiceResult { + let txn = conn.begin().await?; + + let user = repository_get_user_by_id(&txn, user_id).await?; + if user.totp_enabled_at.is_some() { return Err(Errors::TotpAlreadyEnabled); } - // Secret이 없는 경우 (setup 안 함) let secret_base32 = user.totp_secret.clone().ok_or(Errors::TotpNotEnabled)?; - // TOTP 검증 - if !verify_totp_code(&secret_base32, email, code)? { + if !verify_totp_code(&secret_base32, &user.email, code)? { return Err(Errors::TotpInvalidCode); } - // 백업 코드 생성 (평문) let backup_codes = generate_backup_codes(); - // 해시하여 DB에 저장 (평문은 사용자에게만 반환) let hashed_codes = hash_backup_codes(&backup_codes); - // DB 업데이트: totp_enabled_at 설정 + 해시된 백업 코드 저장 repository_update_user( - conn, + &txn, user_id, UserUpdateParams { totp_enabled_at: Some(Some(Utc::now())), @@ -52,6 +43,10 @@ where ) .await?; - // 평문 백업 코드 반환 (사용자가 저장해야 함) + txn.commit().await?; + + info!(user_id = %user_id, "TOTP enabled"); + Ok(TotpEnableResponse { backup_codes }) } + diff --git a/crates/axumkit-server/src/service/auth/totp/mod.rs b/crates/axumkit-server/src/service/auth/totp/mod.rs index 2240b2d..df115b4 100644 --- a/crates/axumkit-server/src/service/auth/totp/mod.rs +++ b/crates/axumkit-server/src/service/auth/totp/mod.rs @@ -1,4 +1,4 @@ -pub mod backup_codes; +pub mod backup_codes; mod common; pub mod disable; pub mod enable; @@ -14,3 +14,4 @@ pub use setup::service_totp_setup; pub use status::service_totp_status; pub use temp_token::TotpTempToken; pub use verify::{TotpVerifyResult, service_totp_verify}; + diff --git a/crates/axumkit-server/src/service/auth/totp/setup.rs b/crates/axumkit-server/src/service/auth/totp/setup.rs index 7d98a66..9703d5b 100644 --- a/crates/axumkit-server/src/service/auth/totp/setup.rs +++ b/crates/axumkit-server/src/service/auth/totp/setup.rs @@ -1,32 +1,27 @@ -use super::common::ISSUER; +use super::common::ISSUER; use crate::repository::user::{ UserUpdateParams, repository_get_user_by_id, repository_update_user, }; -use axumkit_dto::auth::response::TotpSetupResponse; -use axumkit_errors::errors::{Errors, ServiceResult}; -use rand::Rng; -use sea_orm::ConnectionTrait; +use rand::RngExt; +use sea_orm::{DatabaseConnection, TransactionTrait}; use totp_rs::{Algorithm, Secret, TOTP}; +use tracing::info; use uuid::Uuid; +use axumkit_dto::auth::response::TotpSetupResponse; +use axumkit_errors::errors::{Errors, ServiceResult}; -/// TOTP 설정 시작: secret 생성, DB 저장 (아직 활성화 안 함), QR 반환 -pub async fn service_totp_setup( - conn: &C, +pub async fn service_totp_setup( + conn: &DatabaseConnection, user_id: Uuid, - email: &str, -) -> ServiceResult -where - C: ConnectionTrait, -{ - // 사용자 조회 - let user = repository_get_user_by_id(conn, user_id).await?; +) -> ServiceResult { + let txn = conn.begin().await?; + + let user = repository_get_user_by_id(&txn, user_id).await?; - // 이미 TOTP 활성화된 경우 if user.totp_enabled_at.is_some() { return Err(Errors::TotpAlreadyEnabled); } - // Secret 생성 (20 bytes = 160 bits, RFC 4226 권장) let (secret_bytes, secret_base32) = { let mut rng = rand::rng(); let bytes: [u8; 20] = rng.random(); @@ -34,7 +29,6 @@ where (bytes, secret.to_encoded().to_string()) }; - // TOTP 객체 생성 let totp = TOTP::new( Algorithm::SHA1, 6, // digits @@ -42,19 +36,17 @@ where 30, // step secret_bytes.to_vec(), Some(ISSUER.to_string()), - email.to_string(), + user.email, ) .map_err(|_| Errors::TotpSecretGenerationFailed)?; - // QR 코드 생성 (PNG base64) let qr_code_uri = totp.get_url(); let qr_code_png_base64 = totp .get_qr_base64() .map_err(|_| Errors::TotpQrGenerationFailed)?; - // DB에 secret 저장 (totp_enabled_at은 아직 NULL) repository_update_user( - conn, + &txn, user_id, UserUpdateParams { totp_secret: Some(Some(secret_base32)), @@ -63,8 +55,13 @@ where ) .await?; + txn.commit().await?; + + info!(user_id = %user_id, "TOTP setup initiated"); + Ok(TotpSetupResponse { qr_code_base64: qr_code_png_base64, qr_code_uri, }) } + diff --git a/crates/axumkit-server/src/service/auth/totp/status.rs b/crates/axumkit-server/src/service/auth/totp/status.rs index c22e72b..4493112 100644 --- a/crates/axumkit-server/src/service/auth/totp/status.rs +++ b/crates/axumkit-server/src/service/auth/totp/status.rs @@ -1,15 +1,13 @@ -use crate::repository::user::repository_get_user_by_id; -use axumkit_dto::auth::response::TotpStatusResponse; -use axumkit_errors::errors::ServiceResult; +use crate::repository::user::repository_get_user_by_id; use sea_orm::ConnectionTrait; use uuid::Uuid; +use axumkit_dto::auth::response::TotpStatusResponse; +use axumkit_errors::errors::ServiceResult; -/// TOTP 상태 조회 pub async fn service_totp_status(conn: &C, user_id: Uuid) -> ServiceResult where C: ConnectionTrait, { - // 사용자 조회 let user = repository_get_user_by_id(conn, user_id).await?; let enabled = user.totp_enabled_at.is_some(); @@ -24,3 +22,4 @@ where }, }) } + diff --git a/crates/axumkit-server/src/service/auth/totp/temp_token.rs b/crates/axumkit-server/src/service/auth/totp/temp_token.rs index 7e92d26..7199f43 100644 --- a/crates/axumkit-server/src/service/auth/totp/temp_token.rs +++ b/crates/axumkit-server/src/service/auth/totp/temp_token.rs @@ -1,15 +1,13 @@ -use crate::utils::redis_cache::set_json_with_ttl; -use axumkit_errors::errors::Errors; +use crate::utils::redis_cache::{get_optional_json_and_delete, set_json_with_ttl}; use chrono::{DateTime, Utc}; -use rand::RngCore; -use redis::AsyncCommands; +use rand::Rng; use redis::aio::ConnectionManager as RedisClient; use serde::{Deserialize, Serialize}; use uuid::Uuid; +use axumkit_errors::errors::Errors; -const TEMP_TOKEN_TTL_SECONDS: u64 = 120; // 2분 +const TEMP_TOKEN_TTL_SECONDS: u64 = 120; // 2 minutes -/// TOTP 검증용 임시 토큰 (Redis 저장용) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TotpTempToken { pub token: String, @@ -27,7 +25,6 @@ impl TotpTempToken { ip_address: Option, remember_me: bool, ) -> Self { - // 암호학적으로 안전한 랜덤 토큰 생성 (32 bytes = 256 bits) let mut bytes = [0u8; 32]; rand::rng().fill_bytes(&mut bytes); let token = hex::encode(bytes); @@ -46,7 +43,6 @@ impl TotpTempToken { format!("totp_temp:{}", self.token) } - /// 임시 토큰 생성 및 Redis 저장 pub async fn create( redis: &RedisClient, user_id: Uuid, @@ -67,27 +63,13 @@ impl TotpTempToken { Ok(temp_token) } - /// 임시 토큰 조회 및 삭제 (일회용) pub async fn get_and_delete(redis: &RedisClient, token: &str) -> Result, Errors> { - let mut conn = redis.clone(); let key = format!("totp_temp:{}", token); - // GETDEL: 조회 + 삭제 원자적 수행 - let data: Option = conn.get_del(&key).await.map_err(|e| { - Errors::SysInternalError(format!("Redis TOTP temp token retrieval failed: {}", e)) - })?; - - match data { - Some(json) => { - let temp_token: Self = serde_json::from_str(&json).map_err(|e| { - Errors::SysInternalError(format!( - "TOTP temp token deserialization failed: {}", - e - )) - })?; - Ok(Some(temp_token)) - } - None => Ok(None), - } + get_optional_json_and_delete(redis, &key, |e| { + Errors::SysInternalError(format!("TOTP temp token deserialization failed: {}", e)) + }) + .await } } + diff --git a/crates/axumkit-server/src/service/auth/totp/verify.rs b/crates/axumkit-server/src/service/auth/totp/verify.rs index bf7a2cf..b310d6c 100644 --- a/crates/axumkit-server/src/service/auth/totp/verify.rs +++ b/crates/axumkit-server/src/service/auth/totp/verify.rs @@ -1,39 +1,34 @@ -use super::common::verify_totp_code; +use super::common::verify_totp_code; use crate::repository::user::{ UserUpdateParams, repository_get_user_by_id, repository_update_user, }; use crate::service::auth::session::SessionService; use crate::service::auth::totp::TotpTempToken; use crate::utils::crypto::backup_code::verify_backup_code; -use axumkit_errors::errors::{Errors, ServiceResult}; use redis::aio::ConnectionManager as RedisClient; -use sea_orm::ConnectionTrait; +use sea_orm::{DatabaseConnection, TransactionTrait}; +use tracing::info; +use axumkit_errors::errors::{Errors, ServiceResult}; -/// TOTP 검증 결과 pub struct TotpVerifyResult { pub session_id: String, pub remember_me: bool, } -/// TOTP 검증 (로그인 2단계): temp_token + code → session 생성 -pub async fn service_totp_verify( - conn: &C, +pub async fn service_totp_verify( + conn: &DatabaseConnection, redis: &RedisClient, temp_token: &str, code: &str, -) -> ServiceResult -where - C: ConnectionTrait, -{ - // 임시 토큰 조회 및 삭제 (일회용) +) -> ServiceResult { let token_data = TotpTempToken::get_and_delete(redis, temp_token) .await? .ok_or(Errors::TotpTempTokenInvalid)?; - // 사용자 조회 - let user = repository_get_user_by_id(conn, token_data.user_id).await?; + let txn = conn.begin().await?; + + let user = repository_get_user_by_id(&txn, token_data.user_id).await?; - // TOTP가 활성화되어 있어야 함 if user.totp_enabled_at.is_none() { return Err(Errors::TotpNotEnabled); } @@ -41,26 +36,21 @@ where let secret_base32 = user.totp_secret.clone().ok_or(Errors::TotpNotEnabled)?; let backup_codes = user.totp_backup_codes.clone().unwrap_or_default(); - // 코드 길이로 TOTP vs 백업 코드 구분 if code.len() == 6 { - // TOTP 코드 검증 if !verify_totp_code(&secret_base32, &user.email, code)? { return Err(Errors::TotpInvalidCode); } } else if code.len() == 8 { - // 백업 코드 검증 if backup_codes.is_empty() { return Err(Errors::TotpBackupCodeExhausted); } - // 해시 비교로 백업 코드 검증 if let Some(idx) = verify_backup_code(code, &backup_codes) { - // 사용된 백업 코드 제거 let mut new_codes = backup_codes.clone(); new_codes.remove(idx); repository_update_user( - conn, + &txn, token_data.user_id, UserUpdateParams { totp_backup_codes: Some(Some(new_codes)), @@ -75,7 +65,8 @@ where return Err(Errors::TotpInvalidCode); } - // 세션 생성 + txn.commit().await?; + let session = SessionService::create_session( redis, token_data.user_id.to_string(), @@ -84,8 +75,11 @@ where ) .await?; + info!(user_id = %token_data.user_id, "TOTP verified"); + Ok(TotpVerifyResult { session_id: session.session_id, remember_me: token_data.remember_me, }) } + diff --git a/crates/axumkit-server/src/service/auth/verify_email.rs b/crates/axumkit-server/src/service/auth/verify_email.rs index 9f8cce1..64414e0 100644 --- a/crates/axumkit-server/src/service/auth/verify_email.rs +++ b/crates/axumkit-server/src/service/auth/verify_email.rs @@ -1,13 +1,14 @@ -use crate::repository::user::{ +use crate::repository::user::{ UserUpdateParams, repository_get_user_by_id, repository_update_user, }; -use axumkit_errors::errors::{Errors, ServiceResult}; +use crate::utils::redis_cache::get_json_and_delete; use chrono::Utc; -use redis::AsyncCommands; use redis::aio::ConnectionManager; -use sea_orm::ConnectionTrait; +use sea_orm::{DatabaseConnection, TransactionTrait}; use serde::{Deserialize, Serialize}; +use tracing::info; use uuid::Uuid; +use axumkit_errors::errors::{Errors, ServiceResult}; #[derive(Debug, Serialize, Deserialize)] pub struct EmailVerificationData { @@ -15,57 +16,41 @@ pub struct EmailVerificationData { pub email: String, } -/// 이메일 인증을 처리합니다. /// /// # Arguments -/// * `conn` - 데이터베이스 연결 -/// * `redis_conn` - Redis 연결 -/// * `token` - 이메일 인증 토큰 /// /// # Returns -/// * `()` - 성공 시 -pub async fn service_verify_email( - conn: &C, +pub async fn service_verify_email( + conn: &DatabaseConnection, redis_conn: &ConnectionManager, token: &str, -) -> ServiceResult<()> -where - C: ConnectionTrait, -{ - // 1. Redis에서 토큰 검증 (get_del로 일회용) - let token_key = format!("email_verification:{}", token); - let mut redis_mut = redis_conn.clone(); - - let token_json: Option = redis_mut - .get_del(&token_key) - .await - .map_err(|e| Errors::SysInternalError(format!("Redis error: {}", e)))?; - - let token_data = token_json.ok_or(Errors::TokenInvalidVerification)?; - - let verification_data: EmailVerificationData = - serde_json::from_str(&token_data).map_err(|_| Errors::TokenInvalidVerification)?; +) -> ServiceResult<()> { + let token_key = axumkit_constants::email_verification_key(token); + let verification_data: EmailVerificationData = get_json_and_delete( + redis_conn, + &token_key, + || Errors::TokenInvalidVerification, + |_| Errors::TokenInvalidVerification, + ) + .await?; - // 2. user_id 파싱 let user_id = Uuid::parse_str(&verification_data.user_id) .map_err(|_| Errors::TokenInvalidVerification)?; - // 3. 사용자 조회 - let user = repository_get_user_by_id(conn, user_id).await?; + let txn = conn.begin().await?; + + let user = repository_get_user_by_id(&txn, user_id).await?; - // 4. 이미 인증된 사용자인지 확인 if user.verified_at.is_some() { return Err(Errors::EmailAlreadyVerified); } - // 5. 이메일 주소 일치 확인 if user.email != verification_data.email { return Err(Errors::TokenEmailMismatch); } - // 6. verified_at 업데이트 repository_update_user( - conn, + &txn, user_id, UserUpdateParams { verified_at: Some(Some(Utc::now())), @@ -74,5 +59,10 @@ where ) .await?; + txn.commit().await?; + + info!(user_id = %user_id, "Email verified"); + Ok(()) } + diff --git a/crates/axumkit-server/src/service/mod.rs b/crates/axumkit-server/src/service/mod.rs index 04ae6ef..71c6180 100644 --- a/crates/axumkit-server/src/service/mod.rs +++ b/crates/axumkit-server/src/service/mod.rs @@ -2,6 +2,5 @@ pub mod action_logs; pub mod auth; pub mod eventstream; pub mod oauth; -pub mod posts; pub mod search; pub mod user; diff --git a/crates/axumkit-server/src/service/posts/create_post.rs b/crates/axumkit-server/src/service/posts/create_post.rs deleted file mode 100644 index 8da21c8..0000000 --- a/crates/axumkit-server/src/service/posts/create_post.rs +++ /dev/null @@ -1,44 +0,0 @@ -use axumkit_constants::POST_CONTENT_PREFIX; -use axumkit_dto::posts::CreatePostResponse; -use axumkit_errors::errors::Errors; -use sea_orm::DatabaseConnection; -use uuid::Uuid; - -use crate::bridge::worker_client::index_post; -use crate::connection::seaweedfs_conn::SeaweedFsClient; -use crate::repository::posts::repository_create_post; -use crate::state::WorkerClient; - -pub async fn service_create_post( - conn: &DatabaseConnection, - seaweedfs_client: &SeaweedFsClient, - worker: &WorkerClient, - author_id: Uuid, - title: String, - content: String, -) -> Result { - // 1. Generate storage key - let post_id = Uuid::now_v7(); - let storage_key = format!("{}/{}", POST_CONTENT_PREFIX, post_id); - - // 2. Upload content to SeaweedFS - seaweedfs_client - .upload_content(&storage_key, &content) - .await - .map_err(|e| { - tracing::error!("Failed to upload post content: {}", e); - Errors::FileUploadError(e.to_string()) - })?; - - // 3. Create post in database with storage_key - let post = repository_create_post(conn, author_id, title, storage_key).await?; - - // 4. Publish index job via bridge - if let Err(e) = index_post(worker, post.id).await { - tracing::warn!("Failed to queue post index job: {:?}", e); - } - - Ok(CreatePostResponse { - id: post.id.to_string(), - }) -} diff --git a/crates/axumkit-server/src/service/posts/delete_post.rs b/crates/axumkit-server/src/service/posts/delete_post.rs deleted file mode 100644 index a095f47..0000000 --- a/crates/axumkit-server/src/service/posts/delete_post.rs +++ /dev/null @@ -1,39 +0,0 @@ -use axumkit_dto::posts::DeletePostResponse; -use axumkit_errors::errors::Errors; -use sea_orm::DatabaseConnection; -use uuid::Uuid; - -use crate::bridge::worker_client::{delete_content, delete_post_from_index}; -use crate::repository::posts::{repository_delete_post, repository_get_post_by_id}; -use crate::state::WorkerClient; - -pub async fn service_delete_post( - conn: &DatabaseConnection, - worker: &WorkerClient, - id: Uuid, - author_id: Uuid, -) -> Result { - let post = repository_get_post_by_id(conn, id).await?; - - // 작성자만 삭제 가능 - if post.author_id != author_id { - return Err(Errors::UserUnauthorized); - } - - let storage_key = post.storage_key.clone(); - - // DB에서 삭제 - repository_delete_post(conn, id).await?; - - // Index에서 삭제 - if let Err(e) = delete_post_from_index(worker, id).await { - tracing::warn!("Failed to queue post delete from index: {:?}", e); - } - - // Storage에서 삭제 (worker queue) - if let Err(e) = delete_content(worker, vec![storage_key]).await { - tracing::warn!("Failed to queue content delete: {:?}", e); - } - - Ok(DeletePostResponse { success: true }) -} diff --git a/crates/axumkit-server/src/service/posts/get_post.rs b/crates/axumkit-server/src/service/posts/get_post.rs deleted file mode 100644 index 61a188e..0000000 --- a/crates/axumkit-server/src/service/posts/get_post.rs +++ /dev/null @@ -1,33 +0,0 @@ -use axumkit_dto::posts::PostResponse; -use axumkit_errors::errors::Errors; -use sea_orm::DatabaseConnection; -use uuid::Uuid; - -use crate::connection::seaweedfs_conn::SeaweedFsClient; -use crate::repository::posts::repository_get_post_by_id; - -pub async fn service_get_post( - conn: &DatabaseConnection, - seaweedfs_client: &SeaweedFsClient, - id: Uuid, -) -> Result { - let post = repository_get_post_by_id(conn, id).await?; - - // content를 SeaweedFS에서 읽어옴 - let content = seaweedfs_client - .download_content(&post.storage_key) - .await - .map_err(|e| { - tracing::error!("Failed to download post content: {}", e); - Errors::FileReadError(e.to_string()) - })?; - - Ok(PostResponse { - id: post.id.to_string(), - author_id: post.author_id.to_string(), - title: post.title, - content, - created_at: post.created_at, - updated_at: post.updated_at, - }) -} diff --git a/crates/axumkit-server/src/service/posts/list_posts.rs b/crates/axumkit-server/src/service/posts/list_posts.rs deleted file mode 100644 index 29a60c1..0000000 --- a/crates/axumkit-server/src/service/posts/list_posts.rs +++ /dev/null @@ -1,24 +0,0 @@ -use axumkit_dto::posts::{ListPostsResponse, PostListItem}; -use axumkit_errors::errors::Errors; -use sea_orm::DatabaseConnection; - -use crate::repository::posts::repository_list_posts; - -pub async fn service_list_posts( - conn: &DatabaseConnection, - limit: u64, - offset: u64, -) -> Result { - let posts = repository_list_posts(conn, limit, offset).await?; - let posts = posts - .into_iter() - .map(|post| PostListItem { - id: post.id.to_string(), - author_id: post.author_id.to_string(), - title: post.title, - created_at: post.created_at, - updated_at: post.updated_at, - }) - .collect(); - Ok(ListPostsResponse { posts }) -} diff --git a/crates/axumkit-server/src/service/posts/mod.rs b/crates/axumkit-server/src/service/posts/mod.rs deleted file mode 100644 index e0fa30a..0000000 --- a/crates/axumkit-server/src/service/posts/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod create_post; -pub mod delete_post; -pub mod get_post; -pub mod list_posts; -pub mod update_post; - -pub use create_post::service_create_post; -pub use delete_post::service_delete_post; -pub use get_post::service_get_post; -pub use list_posts::service_list_posts; -pub use update_post::service_update_post; diff --git a/crates/axumkit-server/src/service/posts/update_post.rs b/crates/axumkit-server/src/service/posts/update_post.rs deleted file mode 100644 index 870df68..0000000 --- a/crates/axumkit-server/src/service/posts/update_post.rs +++ /dev/null @@ -1,71 +0,0 @@ -use axumkit_dto::posts::PostResponse; -use axumkit_errors::errors::Errors; -use sea_orm::DatabaseConnection; -use uuid::Uuid; - -use crate::bridge::worker_client::index_post; -use crate::connection::seaweedfs_conn::SeaweedFsClient; -use crate::repository::posts::{ - PostUpdateParams, repository_get_post_by_id, repository_update_post, -}; -use crate::state::WorkerClient; - -pub async fn service_update_post( - conn: &DatabaseConnection, - seaweedfs_client: &SeaweedFsClient, - worker: &WorkerClient, - id: Uuid, - author_id: Uuid, - title: Option, - content: Option, -) -> Result { - let post = repository_get_post_by_id(conn, id).await?; - - // 작성자만 수정 가능 - if post.author_id != author_id { - return Err(Errors::UserUnauthorized); - } - - // content가 변경되면 SeaweedFS에 업로드 - let new_storage_key = if let Some(ref new_content) = content { - seaweedfs_client - .upload_content(&post.storage_key, new_content) - .await - .map_err(|e| { - tracing::error!("Failed to upload post content: {}", e); - Errors::FileUploadError(e.to_string()) - })?; - None // 기존 storage_key 유지 - } else { - None - }; - - let params = PostUpdateParams { - title, - storage_key: new_storage_key, - }; - let updated_post = repository_update_post(conn, post, params).await?; - - // Publish index job via bridge - if let Err(e) = index_post(worker, updated_post.id).await { - tracing::warn!("Failed to queue post index job: {:?}", e); - } - - // content를 SeaweedFS에서 읽어옴 - let content = seaweedfs_client - .download_content(&updated_post.storage_key) - .await - .map_err(|e| { - tracing::error!("Failed to download post content: {}", e); - Errors::FileReadError(e.to_string()) - })?; - - Ok(PostResponse { - id: updated_post.id.to_string(), - author_id: updated_post.author_id.to_string(), - title: updated_post.title, - content, - created_at: updated_post.created_at, - updated_at: updated_post.updated_at, - }) -} diff --git a/crates/axumkit-server/src/service/search/mod.rs b/crates/axumkit-server/src/service/search/mod.rs index f88b7f0..b8a331e 100644 --- a/crates/axumkit-server/src/service/search/mod.rs +++ b/crates/axumkit-server/src/service/search/mod.rs @@ -1,5 +1,3 @@ -pub mod search_posts; pub mod search_users; -pub use search_posts::*; pub use search_users::*; diff --git a/crates/axumkit-server/src/service/search/search_posts.rs b/crates/axumkit-server/src/service/search/search_posts.rs deleted file mode 100644 index 2f7bf72..0000000 --- a/crates/axumkit-server/src/service/search/search_posts.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::connection::MeilisearchClient; -use axumkit_dto::search::{PostSearchHit, SearchPostsRequest, SearchPostsResponse}; -use axumkit_errors::errors::{Errors, ServiceResult}; -use serde::Deserialize; -use tracing::{info, warn}; -use uuid::Uuid; - -// Private: MeiliSearch index schema -#[derive(Debug, Deserialize)] -struct IndexedPost { - id: String, - author_id: String, - title: String, - content: String, -} - -pub async fn service_search_posts( - client: &MeilisearchClient, - request: &SearchPostsRequest, -) -> ServiceResult { - info!( - "Searching posts: query='{}', page={}, page_size={}", - request.query, request.page, request.page_size - ); - - // Build and execute search query using page/hitsPerPage mode for exact total_hits - let index = client.get_client().index("posts"); - let mut search_query = index.search(); - - search_query.with_query(&request.query); - search_query.with_page(request.page as usize); - search_query.with_hits_per_page(request.page_size as usize); - - // Execute search - let results = search_query.execute::().await.map_err(|e| { - tracing::error!("MeiliSearch query failed: {}", e); - Errors::MeiliSearchQueryFailed - })?; - - // Get pagination info from response (available in page/hitsPerPage mode) - let total_hits = results.total_hits.unwrap_or(0) as u64; - let total_pages = results.total_pages.unwrap_or(0) as u32; - - // Transform results to DTOs - let hits: Vec = results - .hits - .into_iter() - .filter_map(|hit| { - let post = hit.result; - let id = match Uuid::parse_str(&post.id) { - Ok(id) => id, - Err(e) => { - warn!("Invalid UUID in search index: '{}', error: {}", post.id, e); - return None; - } - }; - let author_id = match Uuid::parse_str(&post.author_id) { - Ok(id) => id, - Err(e) => { - warn!( - "Invalid author_id UUID in search index: '{}', error: {}", - post.author_id, e - ); - return None; - } - }; - Some(PostSearchHit { - id, - author_id, - title: post.title, - content_snippet: truncate_content(&post.content, 200), - }) - }) - .collect(); - - Ok(SearchPostsResponse { - hits, - page: request.page, - page_size: request.page_size, - total_hits, - total_pages, - }) -} - -fn truncate_content(content: &str, max_chars: usize) -> String { - match content.char_indices().nth(max_chars) { - Some((idx, _)) => content[..idx].to_string(), - None => content.to_string(), - } -} diff --git a/crates/axumkit-server/src/state.rs b/crates/axumkit-server/src/state.rs index 7a13a34..abb03d5 100644 --- a/crates/axumkit-server/src/state.rs +++ b/crates/axumkit-server/src/state.rs @@ -2,7 +2,7 @@ use redis::aio::ConnectionManager as RedisClient; use std::sync::Arc; use tokio::sync::broadcast; -use crate::connection::{MeilisearchClient, R2Client, SeaweedFsClient}; +use crate::connection::{MeilisearchClient, R2Client}; use axumkit_dto::action_logs::ActionLogResponse; use reqwest::Client as HttpClient; use sea_orm::DatabaseConnection as PostgresqlClient; @@ -21,7 +21,6 @@ pub struct AppState { pub write_db: PostgresqlClient, pub read_db: PostgresqlClient, pub r2_client: R2Client, - pub seaweedfs_client: SeaweedFsClient, /// Redis for sessions, tokens, rate-limiting (persistent, AOF) pub redis_session: RedisClient, /// Redis for document cache (volatile, LRU eviction) diff --git a/crates/axumkit-server/src/utils/crypto/token.rs b/crates/axumkit-server/src/utils/crypto/token.rs index 04f6b15..3254197 100644 --- a/crates/axumkit-server/src/utils/crypto/token.rs +++ b/crates/axumkit-server/src/utils/crypto/token.rs @@ -1,5 +1,5 @@ use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; -use rand::Rng; +use rand::RngExt; /// 암호학적으로 안전한 토큰 생성 (32바이트 = 256비트) /// URL-safe Base64 인코딩으로 반환 (43자) diff --git a/crates/axumkit-server/src/utils/r2_url.rs b/crates/axumkit-server/src/utils/r2_url.rs index 7a70a52..bb25aed 100644 --- a/crates/axumkit-server/src/utils/r2_url.rs +++ b/crates/axumkit-server/src/utils/r2_url.rs @@ -3,5 +3,5 @@ use axumkit_config::ServerConfig; /// R2 스토리지 key를 전체 public URL로 변환 pub fn build_r2_public_url(key: &str) -> String { let config = ServerConfig::get(); - format!("{}/{}", config.r2_public_domain, key) + format!("{}/{}", config.r2_assets_public_domain, key) } diff --git a/crates/axumkit-worker/src/config/worker_config.rs b/crates/axumkit-worker/src/config/worker_config.rs index 504fb27..e5d3170 100644 --- a/crates/axumkit-worker/src/config/worker_config.rs +++ b/crates/axumkit-worker/src/config/worker_config.rs @@ -43,16 +43,14 @@ pub struct WorkerConfig { // Cron pub cron_timezone: String, - // SeaweedFS (revision content storage) - pub seaweedfs_endpoint: String, - - // R2 (Sitemap storage) + // Cloudflare R2 (shared credentials) pub r2_endpoint: String, pub r2_region: String, pub r2_access_key_id: String, pub r2_secret_access_key: String, - pub r2_bucket_name: String, - pub r2_public_domain: String, + // R2 Assets (public bucket - images, sitemap) + pub r2_assets_bucket_name: String, + pub r2_assets_public_domain: String, } static CONFIG: LazyLock = LazyLock::new(|| { @@ -84,12 +82,11 @@ static CONFIG: LazyLock = LazyLock::new(|| { let db_write_name = require!("POSTGRES_WRITE_NAME"); let db_write_user = require!("POSTGRES_WRITE_USER"); let db_write_password = require!("POSTGRES_WRITE_PASSWORD"); - let seaweedfs_endpoint = require!("SEAWEEDFS_ENDPOINT"); let r2_endpoint = require!("R2_ENDPOINT"); let r2_access_key_id = require!("R2_ACCESS_KEY_ID"); let r2_secret_access_key = require!("R2_SECRET_ACCESS_KEY"); - let r2_bucket_name = require!("R2_BUCKET_NAME"); - let r2_public_domain = require!("R2_PUBLIC_DOMAIN"); + let r2_assets_bucket_name = require!("R2_ASSETS_BUCKET_NAME"); + let r2_assets_public_domain = require!("R2_ASSETS_PUBLIC_DOMAIN"); // Panic with all errors at once if !errors.is_empty() { @@ -154,16 +151,14 @@ static CONFIG: LazyLock = LazyLock::new(|| { // Cron cron_timezone: env::var("CRON_TIMEZONE").unwrap_or_else(|_| "UTC".into()), - // SeaweedFS - seaweedfs_endpoint, - - // R2 + // Cloudflare R2 (shared credentials) r2_endpoint, r2_region: env::var("R2_REGION").unwrap_or_else(|_| "auto".into()), r2_access_key_id, r2_secret_access_key, - r2_bucket_name, - r2_public_domain, + // R2 Assets (public bucket) + r2_assets_bucket_name, + r2_assets_public_domain, } }); diff --git a/crates/axumkit-worker/src/connection/mod.rs b/crates/axumkit-worker/src/connection/mod.rs index b4b3fa7..b02fa64 100644 --- a/crates/axumkit-worker/src/connection/mod.rs +++ b/crates/axumkit-worker/src/connection/mod.rs @@ -1,7 +1,5 @@ mod database_conn; mod r2_conn; -mod seaweedfs_conn; pub use database_conn::establish_connection; pub use r2_conn::{R2Client, establish_r2_connection}; -pub use seaweedfs_conn::{SeaweedFsClient, StorageObjectInfo, establish_seaweedfs_connection}; diff --git a/crates/axumkit-worker/src/connection/r2_conn.rs b/crates/axumkit-worker/src/connection/r2_conn.rs index e5d0fd0..6755706 100644 --- a/crates/axumkit-worker/src/connection/r2_conn.rs +++ b/crates/axumkit-worker/src/connection/r2_conn.rs @@ -161,8 +161,8 @@ pub async fn establish_r2_connection(config: &WorkerConfig) -> anyhow::Result, - bucket: String, -} - -impl SeaweedFsClient { - pub fn new(client: Client, bucket: String) -> Self { - Self { - client: Arc::new(client), - bucket, - } - } - - /// 콘텐츠 다운로드 (zstd 압축 해제) - pub async fn download_content( - &self, - key: &str, - ) -> Result> { - let resp = self - .client - .get_object() - .bucket(&self.bucket) - .key(key) - .send() - .await?; - - let data = resp.body.collect().await?; - let bytes = data.into_bytes(); - - let decompressed = zstd::decode_all(bytes.as_ref())?; - let content = String::from_utf8(decompressed)?; - - Ok(content) - } - - /// 오브젝트 삭제 - pub async fn delete(&self, key: &str) -> Result<(), Box> { - self.client - .delete_object() - .bucket(&self.bucket) - .key(key) - .send() - .await?; - Ok(()) - } - - /// 오브젝트 목록 조회 (페이지네이션) - /// Returns (keys, continuation_token) - pub async fn list_objects( - &self, - continuation_token: Option<&str>, - max_keys: i32, - ) -> Result<(Vec, Option), Box> - { - let mut request = self - .client - .list_objects_v2() - .bucket(&self.bucket) - .max_keys(max_keys); - - if let Some(token) = continuation_token { - request = request.continuation_token(token); - } - - let resp = request.send().await?; - - let objects: Vec = resp - .contents() - .iter() - .filter_map(|obj| { - let key = obj.key()?.to_string(); - let last_modified = obj.last_modified().map(|t| { - chrono::DateTime::from_timestamp(t.secs(), t.subsec_nanos()).unwrap_or_default() - }); - Some(StorageObjectInfo { key, last_modified }) - }) - .collect(); - - let next_token = resp.next_continuation_token().map(|s| s.to_string()); - - Ok((objects, next_token)) - } -} - -/// Storage object info for cleanup -#[derive(Debug, Clone)] -pub struct StorageObjectInfo { - pub key: String, - pub last_modified: Option>, -} - -const BUCKET_NAME: &str = "axumkit-content"; - -pub async fn establish_seaweedfs_connection( - config: &WorkerConfig, -) -> anyhow::Result { - info!("Connecting to SeaweedFS at: {}", config.seaweedfs_endpoint); - - let aws_config = aws_config::defaults(BehaviorVersion::latest()) - .region(Region::new("us-east-1")) - .endpoint_url(&config.seaweedfs_endpoint) - .credentials_provider(aws_sdk_s3::config::Credentials::new( - "", - "", - None, - None, - "anonymous", - )) - .load() - .await; - - let s3_config = aws_sdk_s3::config::Builder::from(&aws_config) - .force_path_style(true) - .build(); - - let client = Client::from_conf(s3_config); - let seaweedfs_client = SeaweedFsClient::new(client, BUCKET_NAME.to_string()); - - info!("Successfully connected to SeaweedFS"); - Ok(seaweedfs_client) -} diff --git a/crates/axumkit-worker/src/jobs/cron/cleanup_orphaned_blobs.rs b/crates/axumkit-worker/src/jobs/cron/cleanup_orphaned_blobs.rs deleted file mode 100644 index 4641d26..0000000 --- a/crates/axumkit-worker/src/jobs/cron/cleanup_orphaned_blobs.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::connection::SeaweedFsClient; -use axumkit_entity::posts; -use chrono::{Duration, Utc}; -use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect}; -use std::collections::HashSet; - -/// Batch size for listing objects from storage -const BATCH_SIZE: i32 = 1000; - -/// Minimum age in hours before a blob can be considered orphaned -/// (to avoid race conditions with in-flight transactions) -const MIN_AGE_HOURS: i64 = 1; - -/// Run orphaned blob cleanup -/// -/// Finds storage keys in SeaweedFS that don't exist in posts table -/// and deletes them. Only deletes blobs older than MIN_AGE_HOURS to avoid -/// race conditions with in-flight transactions. -pub async fn run_cleanup_orphaned_blobs(db: &DatabaseConnection, storage: &SeaweedFsClient) { - tracing::info!( - batch_size = BATCH_SIZE, - min_age_hours = MIN_AGE_HOURS, - "Starting orphaned blob cleanup" - ); - - let cutoff_time = Utc::now() - Duration::hours(MIN_AGE_HOURS); - let mut continuation_token: Option = None; - let mut total_checked = 0u64; - let mut total_deleted = 0u64; - let mut total_errors = 0u64; - - loop { - // 1. List objects from storage - let (objects, next_token) = match storage - .list_objects(continuation_token.as_deref(), BATCH_SIZE) - .await - { - Ok(result) => result, - Err(e) => { - tracing::error!(error = %e, "Failed to list objects from storage"); - break; - } - }; - - if objects.is_empty() { - break; - } - - // 2. Filter to only old enough objects - let old_objects: Vec<_> = objects - .into_iter() - .filter(|obj| obj.last_modified.map(|t| t < cutoff_time).unwrap_or(false)) - .collect(); - - if !old_objects.is_empty() { - let keys: Vec = old_objects.iter().map(|o| o.key.clone()).collect(); - total_checked += keys.len() as u64; - - // 3. Check which keys exist in DB - let existing_keys = match find_existing_storage_keys(db, &keys).await { - Ok(keys) => keys, - Err(e) => { - tracing::error!(error = %e, "Failed to query existing storage keys"); - break; - } - }; - - // 4. Delete orphaned keys (keys that don't exist in DB) - for obj in old_objects { - if !existing_keys.contains(&obj.key) { - match storage.delete(&obj.key).await { - Ok(_) => { - total_deleted += 1; - tracing::debug!(key = %obj.key, "Deleted orphaned blob"); - } - Err(e) => { - total_errors += 1; - tracing::warn!(key = %obj.key, error = %e, "Failed to delete orphaned blob"); - } - } - } - } - } - - // Continue to next page - continuation_token = next_token; - if continuation_token.is_none() { - break; - } - } - - tracing::info!( - total_checked = total_checked, - total_deleted = total_deleted, - total_errors = total_errors, - "Orphaned blob cleanup completed" - ); -} - -/// Find which storage keys exist in posts table -async fn find_existing_storage_keys( - db: &DatabaseConnection, - keys: &[String], -) -> Result, anyhow::Error> { - if keys.is_empty() { - return Ok(HashSet::new()); - } - - let results: Vec = posts::Entity::find() - .select_only() - .column(posts::Column::StorageKey) - .filter(posts::Column::StorageKey.is_in(keys.to_vec())) - .into_tuple() - .all(db) - .await?; - - Ok(results.into_iter().collect()) -} diff --git a/crates/axumkit-worker/src/jobs/cron/lua/extend_lock.lua b/crates/axumkit-worker/src/jobs/cron/lua/extend_lock.lua new file mode 100644 index 0000000..0e51e41 --- /dev/null +++ b/crates/axumkit-worker/src/jobs/cron/lua/extend_lock.lua @@ -0,0 +1,5 @@ +if redis.call("GET", KEYS[1]) == ARGV[1] then + return redis.call("EXPIRE", KEYS[1], ARGV[2]) +else + return 0 +end diff --git a/crates/axumkit-worker/src/jobs/cron/lua/release_lock.lua b/crates/axumkit-worker/src/jobs/cron/lua/release_lock.lua new file mode 100644 index 0000000..e9aa40a --- /dev/null +++ b/crates/axumkit-worker/src/jobs/cron/lua/release_lock.lua @@ -0,0 +1,5 @@ +if redis.call("GET", KEYS[1]) == ARGV[1] then + return redis.call("DEL", KEYS[1]) +else + return 0 +end diff --git a/crates/axumkit-worker/src/jobs/cron/mod.rs b/crates/axumkit-worker/src/jobs/cron/mod.rs index c351649..f57e17a 100644 --- a/crates/axumkit-worker/src/jobs/cron/mod.rs +++ b/crates/axumkit-worker/src/jobs/cron/mod.rs @@ -1,14 +1,18 @@ mod cleanup; -mod cleanup_orphaned_blobs; pub mod sitemap; +use crate::CacheClient; use crate::DbPool; use crate::config::WorkerConfig; use crate::connection::R2Client; -use crate::connection::SeaweedFsClient; use chrono_tz::Tz; +use redis::Script; +use std::pin::pin; use std::sync::Arc; +use std::sync::LazyLock; +use std::time::Duration; use tokio_cron_scheduler::{Job, JobBuilder, JobScheduler, JobSchedulerError}; +use uuid::Uuid; /// Cleanup cron schedule: 4:00 AM every Saturday /// Format: "sec min hour day month weekday" @@ -17,14 +21,24 @@ const CLEANUP_SCHEDULE: &str = "0 0 4 * * 6"; /// Sitemap cron schedule: 3:00 AM every Sunday const SITEMAP_SCHEDULE: &str = "0 0 3 * * 0"; -/// Orphaned blob cleanup schedule: 5:00 AM every Friday -const ORPHANED_BLOB_CLEANUP_SCHEDULE: &str = "0 0 5 * * 5"; +/// Distributed lock TTL for cron jobs (seconds). +const CRON_LOCK_TTL_SECONDS: u64 = 60 * 30; // 30 minutes +/// Heartbeat interval for lock extension (seconds). +const CRON_LOCK_HEARTBEAT_SECONDS: u64 = 60 * 10; // 10 minutes -/// Create and start the cron scheduler +const CLEANUP_LOCK_KEY: &str = "cron:lock:cleanup"; +const SITEMAP_LOCK_KEY: &str = "cron:lock:sitemap"; + +static RELEASE_LOCK_SCRIPT: LazyLock