Skip to content

feat(backend): メール認証を行えるように#13

Open
yupix wants to merge 23 commits into
mainfrom
feat/verify-mail
Open

feat(backend): メール認証を行えるように#13
yupix wants to merge 23 commits into
mainfrom
feat/verify-mail

Conversation

@yupix
Copy link
Copy Markdown
Contributor

@yupix yupix commented May 20, 2026

メール認証フロー(フロント含む)

sequenceDiagram
    autonumber
    actor U as ユーザー
    participant F as フロント(Web)
    participant B as API(バックエンド)
    participant A as Apalisワーカー<br/>(同一プロセス)
    participant Q as PostgreSQL<br/>(Apalis ジョブキュー)
    participant DB as PostgreSQL<br/>(アプリ DB)
    participant R as Redis
    participant S as SMTPサーバ
    participant M as メール

    rect rgb(240, 248, 255)
    Note over U,F: アカウント作成
    U->>F: 登録情報を入力
    F->>B: POST /v1/auth/register
    B->>DB: トランザクション<br/>ユーザー作成(未認証)
    B->>Q: Apalis にジョブ投入<br/>(user_id, email, token, issued_at)
    alt エンキュー成功
        B->>F: 201 Created
    else エンキュー失敗
        Note over B,F: ユーザーは未認証のまま残す
        B->>F: 503(認証メール再送の案内)
    end
    F->>U: メール確認の案内を表示
    par バックグラウンド送信(Apalis)
        A->>Q: ジョブ取得(NOTIFY + ポーリング)
        A->>R: 認証トークン保存(issued_at 世代・Lua で原子的)
        A->>S: 認証メール送信
        S->>M: 配信
        A->>Q: ジョブ完了(成功)
    and 送信失敗時
        A->>Q: リトライ(最大 8 回・Apalis RetryPolicy)
        Note over A,Q: 失敗が続くとジョブは failed 相当で打ち切り<br/>古い issued_at のリトライは Redis 更新・送信をスキップ
    end
    end

    rect rgb(245, 255, 245)
    Note over U,F: メールのリンクからフロントへ
    U->>M: メール受信・リンクをクリック
    M-->>U: ブラウザでフロントを開く<br/>(token は URL エンコード済み)
    U->>F: /verify-email?token=... でアクセス
    F->>F: URL から token を取得(デコード)
    end

    rect rgb(255, 250, 240)
    Note over F,B: 認証完了(副作用は API が担当)
    F->>B: POST /v1/auth/verify-email(JSON: token)
    B->>R: トークン検証・消費(GETDEL + Lua)
    alt トークン有効
        B->>DB: email_verified = true に更新
        B->>F: 200 OK
        F->>U: 認証完了の表示
    else 無効・期限切れ
        B->>F: 400
        F->>U: エラー/再送案内
    end
    end

    rect rgb(255, 245, 245)
    Note over U,F: 認証メールの再送(任意)
    U->>F: 再送を依頼
    F->>B: POST /v1/auth/resend-verification-email
    B->>R: 再送間隔チェック(SET NX・60秒)
    B->>DB: ユーザー状態を確認
    alt 送信可能な未認証ユーザー
        B->>Q: Apalis にジョブ投入(新トークン)
        B->>F: 200 OK
        F->>U: 「送信しました」等
        A->>Q: ジョブ取得
        A->>R: 新トークン保存(より新しい issued_at のみ・旧トークン無効化)
        A->>S: メール再送
        S->>M: 配信
        A->>Q: ジョブ完了
    else 該当なし/既認証など
        B->>F: 404 / 429 / 409
        F->>U: 案内のみ(詳細は API 設計どおり)
    end
    end

    rect rgb(248, 248, 255)
    Note over U,F: ログイン(認証後)
    U->>F: メール・PWでログイン
    F->>B: POST /v1/auth/login
    alt 未メール確認
        B->>F: 403(message: email-not-verified)
        F->>U: メール確認の案内
    else メール確認済み
        B->>F: 204 No Content(セッション Cookie)
        F->>U: ログイン後画面へ
    end
    end
Loading

Summary by CodeRabbit

  • New Features

    • メール認証フローを導入(検証トークン生成・検証・再送、再送クールダウン)
    • 検証メールの非同期ジョブキューとワーカー、SMTP送信、ジョブ管理連携を追加
    • ユーザにemail_verifiedフラグを追加、メール正規化と検証用アプリURLバリデーションを追加
  • Bug Fixes

    • 認証系のHTTPステータス整理(login:204, register:201, logout:204 等)
    • ラベル取得での内部エラー伝播を修正
    • APIエラー応答を共通形式に統一
  • Documentation

    • OpenAPI仕様・エラースキーマ日本語化・更新、Brunoテストコレクションと手順追加
  • Tests

    • 設定検証・パスワード検証関連ユニット追加

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Walkthrough

ユーザー登録から検証メール生成・Redis管理・Apalisジョブによる送信、検証トークン消費で email_verified を更新する一連の実装と関連ドキュメント/OpenAPI/Brunoテストを追加しました。

Changes

メール認証フロー実装

Layer / File(s) Summary
エンティティと基本設定
apps/backend/src/entities/users.rs, apps/backend/src/entities/README.md, apps/backend/src/entities/scopes.rs, apps/backend/.env.example, apps/backend/src/settings.rs, apps/backend/Cargo.toml
users モデルに email_verifiedemail の一意制約を追加。entities README と Scope コメント更新。.env.example と Settings に email_verification_app_url を追加し検証ロジックとテストを実装。chrono/url/urlencoding を依存に追加。
認証エラー体系とトークン生成
apps/backend/src/error.rs, apps/backend/src/utils/auth.rs
共通 ServerErrorAppError/AuthError を整理・拡張。AuthError→HTTP マッピング追加。ダミーhash とメール検証トークン生成、関連テストを追加。
OpenAPI エラーレスポンス定義
apps/backend/src/openapi/mod.rs, apps/backend/src/openapi/responses.rs
Register/Verify/Resend 用 Errors を追加しレスポンス説明を日本語化、openapi モジュールの再エクスポートを更新。
Redis トークン管理と DB ヘルパ
apps/backend/src/utils/email_verification.rs, apps/backend/src/utils/db.rs, apps/backend/src/utils/mod.rs
Redis 上で token↔user の双方向マッピングを Lua スクリプトで原子的に管理する store_token/consume_token、再送クールダウン制御、Postgres 一意制約判定 is_postgres_unique_violationwith_transaction を追加。
SMTP 検証メール配信
apps/backend/src/utils/verification_email_delivery.rs, apps/backend/src/utils/email.rs
検証 URL 組立てと SMTP 送信関数を追加。メールアドレス正規化ユーティリティとテストも追加。
Apalis ジョブキューシステム
apps/backend/src/jobs/mod.rs, apps/backend/src/jobs/verification_email.rs, apps/backend/src/lib.rs
VerificationEmailJob と PostgresStorage 型、setup/enqueue/process 実装、worker 並列度制御を追加。AppStatepg_poolverification_email_storage を組み込む。
認証ハンドラーの更新と追加
apps/backend/src/handlers/auth.rs
login を 204、register を 201 に変更。register はトランザクション内で作成→トークン生成→ジョブ enqueue。verify_emailresend_verification_email を追加。logout を 204 に変更。
ルーティング・サーバ起動・状態管理
apps/backend/src/routes/auth.rs, apps/backend/src/handlers/labels.rs, apps/backend/src/handlers/personal_tokens.rs, apps/backend/src/server.rs, apps/backend/src/main.rs
auth ルートに verify/resend を追加。labels::get_labels を Result に変更。personal_tokens の utoipa メタ更新。サーバ起動で Apalis worker を tokio 背景タスク化し graceful shutdown を連携。AppState 初期化で pg_pool と verification_email_storage をセット。
OpenAPI ドキュメント更新(frontend)
apps/frontend/app/utils/openapi.json
login/logout/register/me 等のステータス/エラー表現を更新(204/201 等反映)。ResendVerificationRequest 等スキーマと email_verified の追加。
Bruno 認証テストコレクション
apps/backend/bruno/, .vscode/extensions.json
登録→メール確認→ログイン→me の Bruno シナリオと Apalis Board 連携、README、ローカル env を追加。

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant BackendServer
  participant Redis
  participant Postgres
  participant ApalisWorker
  participant SMTP
  Client->>BackendServer: POST /v1/auth/register (email,password)
  BackendServer->>Postgres: Create user (transaction)
  BackendServer->>BackendServer: generate_email_verification_token()
  BackendServer->>ApalisWorker: enqueue VerificationEmailJob(user_id,email,token)
  ApalisWorker->>Postgres: persist job (PostgresStorage)
  ApalisWorker->>Redis: store_token(token -> user_id) (Lua atomic)
  ApalisWorker->>SMTP: send_verification_email(email, verify_url)
  Client->>BackendServer: POST /v1/auth/verify-email (token)
  BackendServer->>Redis: consume_token(token) -> user_id
  BackendServer->>Postgres: update users.email_verified = true
  BackendServer-->>Client: 200 OK (verification result)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • TeamBlackCrystal/task#11: SMTP クライアント実装と SmtpClient の提供が本 PR の send_verification_email と連携。

Suggested reviewers

  • sousuke0422

Poem

🐰 うさぎが書く、検証メールの歌
トークンは静かに Redis に眠るよ
Apalis がぴょんと飛んで SMTP に届ける
クリックで true になる email_verified の日
おめでとう、ログインの扉がひらくよ ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly describes the main feature: enabling email verification functionality in the backend. It accurately summarizes the primary change.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/verify-mail

Comment @coderabbitai help to get the list of available commands and usage tips.

@yupix yupix requested a review from sousuke0422 May 20, 2026 08:06
@yupix yupix self-assigned this May 20, 2026
@yupix yupix added kind/新機能 New feature or request priority/medium labels May 20, 2026
@yupix yupix changed the title feat(backend): add email verification functionality with token manage… feat(backend): メール認証を行えるように May 20, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/backend/src/entities/users.rs`:
- Around line 20-24: The project is missing the database migration that matches
the users entity (so the #[sea_orm(unique)] on the email field and the
email_verified: bool column are not created); add a new migration file (e.g.,
m20240101_create_users.rs) under apps/backend/migration/src/ that creates the
users table with a unique email column and a boolean email_verified column (plus
any other columns present in the users entity), and register that migration in
the migrations() function in apps/backend/migration/src/lib.rs so it returns a
vec! containing the new migration module; ensure the migration’s up/down
functions reflect the schema (unique constraint on email and creation/removal of
email_verified) to prevent schema mismatch errors when handlers/auth.rs uses
Set(false) and reads email_verified.

In `@apps/backend/src/handlers/auth.rs`:
- Around line 121-146: The current flow persists the user via
users::Entity::insert then performs email_verification::store_token and
send_verification_email, causing inconsistent state if Redis/SMTP fail; change
this by either (preferred) writing an outbox entry (e.g., create an Outbox
entity record in the same DB transaction alongside users::Entity::insert) and
dispatching email/redis work asynchronously from the outbox worker, or (minimal)
implement compensating cleanup: if email_verification::store_token or
send_verification_email returns an error, delete the created user (undo
users::Entity::insert) and return a proper error mapping instead of leaving a
half-created user; update error handling around users::Entity::insert,
email_verification::store_token, and send_verification_email accordingly so
state remains consistent.
- Around line 292-297: The build_verify_url function embeds the raw token into
the query string, which can break links if the token contains reserved
characters; update build_verify_url to percent-encode the token before
interpolation (e.g., use a URL-encoding helper such as url::form_urlencoded or
Url::parse + append_pair) and build the final URL from the trimmed
settings.email_verification_app_url plus the encoded token so the query is
always safe and correct.

In `@apps/backend/src/settings.rs`:
- Around line 17-27: The field email_verification_app_url is currently given a
default via serde(default = "default_email_verification_app_url") which will
silently fall back to "http://localhost:3000"; remove the default so the config
deserialization fails when that key is missing (make the struct field required),
delete or stop using default_email_verification_app_url()/default_allow_origin()
for this field, and ensure any callers that relied on the default now require
the explicit setting at startup so the process fails fast if
email_verification_app_url is not provided.

In `@apps/backend/src/utils/db.rs`:
- Around line 6-12: Replace the brittle string-based check in
is_postgres_unique_violation by using DbErr::sql_err() and matching the
structured SQL error; specifically, call err.sql_err(), if Some, match against
sea_orm::error::SqlErr::UniqueConstraintViolation(_) and return true for that
branch and false otherwise, falling back to false when sql_err() is None,
instead of using err.to_string() and substring checks.

In `@apps/backend/src/utils/email_verification.rs`:
- Around line 29-60: The current sequence using redis::cmd to GET old_token, DEL
old token, SET token_key and SET user_key is non-atomic and can leave multiple
valid tokens during concurrent requests; replace these multi-command ops with a
single Redis Lua script (use redis::Script or EVAL via the redis client) that:
reads the existing value of user_key, if present deletes the corresponding
KEY_TOKEN..old_token, then sets KEY_TOKEN..token to user_id with EX
TOKEN_TTL_SECS and sets user_key to token with EX TOKEN_TTL_SECS, and returns
success/failure; invoke this script in place of the code manipulating old_token,
token_key, KEY_TOKEN, TOKEN_TTL_SECS and propagate errors via the same
anyhow::anyhow! handling so error messages still include context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 22356514-2dca-4680-a0f1-7a304649adb4

📥 Commits

Reviewing files that changed from the base of the PR and between 4f2e4ab and 8c4d22e.

📒 Files selected for processing (15)
  • apps/backend/.env.example
  • apps/backend/src/entities/README.md
  • apps/backend/src/entities/scopes.rs
  • apps/backend/src/entities/users.rs
  • apps/backend/src/handlers/auth.rs
  • apps/backend/src/handlers/labels.rs
  • apps/backend/src/handlers/personal_tokens.rs
  • apps/backend/src/openapi/mod.rs
  • apps/backend/src/openapi/responses.rs
  • apps/backend/src/routes/auth.rs
  • apps/backend/src/settings.rs
  • apps/backend/src/utils/auth.rs
  • apps/backend/src/utils/db.rs
  • apps/backend/src/utils/email_verification.rs
  • apps/backend/src/utils/mod.rs

Comment thread apps/backend/src/entities/users.rs
Comment thread apps/backend/src/handlers/auth.rs Outdated
Comment thread apps/backend/src/handlers/auth.rs Outdated
Comment thread apps/backend/src/settings.rs Outdated
Comment thread apps/backend/src/utils/db.rs Outdated
Comment thread apps/backend/src/utils/email_verification.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/backend/src/utils/verification_email_outbox.rs`:
- Around line 61-70: run_worker currently sleeps before the first work cycle
causing up to a 30s startup delay; change the loop so the initial iteration runs
process_pending immediately (or set idle=false for the first loop) and only then
awaits POLL_INTERVAL_IDLE/POLL_INTERVAL_ACTIVE. Specifically, in run_worker
adjust the idle handling so process_pending() (or the function that handles
pending emails) is invoked before the first tokio::time::sleep, keeping the
existing idle flag logic and using POLL_INTERVAL_IDLE / POLL_INTERVAL_ACTIVE
unchanged for subsequent sleeps to preserve backoff behavior.
- Around line 52-58: process_pending currently selects rows with
status='Pending' without locking, so wake_worker and run_worker can pick the
same outbox row and cause duplicate processing; change process_pending to
atomically claim work by either using UPDATE ... WHERE status='Pending'
RETURNING (to flip status to 'Processing' and return claimed rows) or use SELECT
... FOR UPDATE SKIP LOCKED to fetch rows while preventing others from seeing
them, and ensure process_one only runs on claimed rows; also remove or move the
initial POLL_INTERVAL_IDLE sleep in run_worker so the worker polls immediately
on startup (or call process_pending once at startup) to avoid delayed backlog
processing.

In `@apps/frontend/app/utils/openapi.json`:
- Around line 244-250: OpenAPI responses for register, verify_email, and
resend_verification_email currently declare "text/plain" but the handlers return
Axum's Json<String> (application/json); update the response content media type
to "application/json" (keeping the schema as type: string) in the generated
openapi.json for those endpoints, or adjust the utoipa annotations for the
corresponding handlers (register, verify_email, resend_verification_email) so
the generated response content_type is application/json instead of text/plain.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 44537a31-a7d4-482a-97b5-a606b66b4e48

📥 Commits

Reviewing files that changed from the base of the PR and between 835e83f and 46c9b87.

⛔ Files ignored due to path filters (17)
  • apps/backend/Cargo.lock is excluded by !**/*.lock
  • apps/frontend/app/utils/openapi/.openapi-generator/FILES is excluded by !apps/frontend/app/utils/openapi/**
  • apps/frontend/app/utils/openapi/apis/DefaultApi.ts is excluded by !apps/frontend/app/utils/openapi/**
  • apps/frontend/app/utils/openapi/docs/CrateEntitiesUsersModel.md is excluded by !apps/frontend/app/utils/openapi/**
  • apps/frontend/app/utils/openapi/docs/DefaultApi.md is excluded by !apps/frontend/app/utils/openapi/**
  • apps/frontend/app/utils/openapi/docs/Login401Response.md is excluded by !apps/frontend/app/utils/openapi/**
  • apps/frontend/app/utils/openapi/docs/ResendVerificationRequest.md is excluded by !apps/frontend/app/utils/openapi/**
  • apps/frontend/app/utils/openapi/docs/ServerError.md is excluded by !apps/frontend/app/utils/openapi/**
  • apps/frontend/app/utils/openapi/docs/VerifyEmailRequest.md is excluded by !apps/frontend/app/utils/openapi/**
  • apps/frontend/app/utils/openapi/models/CrateEntitiesUsersModel.ts is excluded by !apps/frontend/app/utils/openapi/**
  • apps/frontend/app/utils/openapi/models/CreatePersonalTokenResponse.ts is excluded by !apps/frontend/app/utils/openapi/**
  • apps/frontend/app/utils/openapi/models/Login401Response.ts is excluded by !apps/frontend/app/utils/openapi/**
  • apps/frontend/app/utils/openapi/models/PersonalTokenResponse.ts is excluded by !apps/frontend/app/utils/openapi/**
  • apps/frontend/app/utils/openapi/models/ResendVerificationRequest.ts is excluded by !apps/frontend/app/utils/openapi/**
  • apps/frontend/app/utils/openapi/models/ServerError.ts is excluded by !apps/frontend/app/utils/openapi/**
  • apps/frontend/app/utils/openapi/models/VerifyEmailRequest.ts is excluded by !apps/frontend/app/utils/openapi/**
  • apps/frontend/app/utils/openapi/models/index.ts is excluded by !apps/frontend/app/utils/openapi/**
📒 Files selected for processing (11)
  • apps/backend/.env.example
  • apps/backend/Cargo.toml
  • apps/backend/src/entities/mod.rs
  • apps/backend/src/entities/verification_email_outbox.rs
  • apps/backend/src/handlers/auth.rs
  • apps/backend/src/server.rs
  • apps/backend/src/settings.rs
  • apps/backend/src/utils/mod.rs
  • apps/backend/src/utils/verification_email_delivery.rs
  • apps/backend/src/utils/verification_email_outbox.rs
  • apps/frontend/app/utils/openapi.json

Comment thread apps/backend/src/utils/verification_email_outbox.rs Outdated
Comment thread apps/backend/src/utils/verification_email_outbox.rs Outdated
Comment thread apps/frontend/app/utils/openapi.json
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/backend/src/handlers/auth.rs (2)

237-252: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

ユーザー列挙攻撃が可能な状態になっています

レスポンスが 404 UserNotFound409 EmailAlreadyVerified200 成功 と分かれているため、攻撃者がメールアドレスの登録有無を特定できます。レートリミットもメールアドレス単位のため、異なるメールを連続して試行でき、列挙を抑止できません。

推奨対策: 存在しないユーザーや既に確認済みのユーザーに対しても、成功時と同一のレスポンス(200)を返すことでタイミングや応答コードによる情報漏洩を防ぎます。

🛡️ 修正案
 pub async fn resend_verification_email(
     State(state): State<AppState>,
     Valid(Json(payload)): Valid<Json<ResendVerificationRequest>>,
 ) -> Result<Json<String>, AuthError> {
     let email = payload.email.trim().to_string();
 
     if !email_verification::try_acquire_resend_slot(&state.redis_client, &email)
         .await
         .map_err(|e| AuthError::Internal(anyhow::anyhow!("redis resend cooldown: {e}")))?
     {
         return Err(AuthError::TooManyRequests);
     }
 
+    // 存在しないユーザー・既確認ユーザーでも同一レスポンスを返し、列挙を防止
+    let generic_response = Json(format!(
+        "確認メールを再送しました(同一メールアドレスへの再送は{}秒に1回までです)。",
+        email_verification::RESEND_COOLDOWN_SECS
+    ));
+
     let user = users::Entity::find()
         .filter(users::Column::Email.eq(email.clone()))
         .one(&state.db)
-        .await?
-        .ok_or(AuthError::UserNotFound)?;
+        .await?;
 
-    if user.email_verified {
-        return Err(AuthError::EmailAlreadyVerified);
+    let Some(user) = user else {
+        return Ok(generic_response);
+    };
+
+    if user.email_verified {
+        return Ok(generic_response);
     }
 
     let token = generate_email_verification_token();
     verification_email_outbox::enqueue(&state.db, user.id, email.clone(), token)
         .await
         .map_err(|e| AuthError::Internal(anyhow::anyhow!("enqueue verification email: {e}")))?;
 
     verification_email_outbox::wake_worker(state);
 
-    Ok(Json(format!(
-        "確認メールを再送しました(同一メールアドレスへの再送は{}秒に1回までです)。",
-        email_verification::RESEND_COOLDOWN_SECS
-    )))
+    Ok(generic_response)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/handlers/auth.rs` around lines 237 - 252, The handler leaks
user existence via differing errors; change the flow so both a non-existent user
and an already-verified user produce the same successful response (HTTP 200) and
body as the genuine resend path: still call
email_verification::try_acquire_resend_slot(...) and perform
users::Entity::find(...).one(&state.db) and inspect user.email_verified to
decide whether to enqueue/send email, but do not return AuthError::UserNotFound
or AuthError::EmailAlreadyVerified — instead return the same success result for
those cases while logging internally if needed; keep returning
AuthError::TooManyRequests and internal errors unchanged so rate-limits and
failure modes remain enforced.

235-235: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

メールアドレスの大文字小文字を正規化していません

trim() のみで大文字小文字の正規化がされていないため、ユーザーが登録時と異なるケースで入力すると照合に失敗します。多くのサービスでは小文字に正規化して保存・検索します。

🔧 修正案
-    let email = payload.email.trim().to_string();
+    let email = payload.email.trim().to_lowercase();

register ハンドラの email 保存時にも同様の正規化を適用してください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/handlers/auth.rs` at line 235, 現在の `let email =
payload.email.trim().to_string();`
は大文字小文字を正規化しておらず、照合に失敗する可能性があります。payload.email を trim()
のあとで小文字化(to_lowercase)して保存/比較するように変更し、ログインや比較に使っているすべての箇所と同様に `register`
ハンドラでも同じ正規化を適用してください(参照するシンボル: payload.email, 変数 email, register ハンドラ)。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/backend/src/handlers/auth.rs`:
- Around line 237-252: The handler leaks user existence via differing errors;
change the flow so both a non-existent user and an already-verified user produce
the same successful response (HTTP 200) and body as the genuine resend path:
still call email_verification::try_acquire_resend_slot(...) and perform
users::Entity::find(...).one(&state.db) and inspect user.email_verified to
decide whether to enqueue/send email, but do not return AuthError::UserNotFound
or AuthError::EmailAlreadyVerified — instead return the same success result for
those cases while logging internally if needed; keep returning
AuthError::TooManyRequests and internal errors unchanged so rate-limits and
failure modes remain enforced.
- Line 235: 現在の `let email = payload.email.trim().to_string();`
は大文字小文字を正規化しておらず、照合に失敗する可能性があります。payload.email を trim()
のあとで小文字化(to_lowercase)して保存/比較するように変更し、ログインや比較に使っているすべての箇所と同様に `register`
ハンドラでも同じ正規化を適用してください(参照するシンボル: payload.email, 変数 email, register ハンドラ)。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 45a85525-5046-4a88-8d21-2e0ed9be8203

📥 Commits

Reviewing files that changed from the base of the PR and between 46c9b87 and 1371d4b.

📒 Files selected for processing (10)
  • apps/backend/src/entities/verification_email_outbox.rs
  • apps/backend/src/error.rs
  • apps/backend/src/handlers/auth.rs
  • apps/backend/src/handlers/labels.rs
  • apps/backend/src/lib.rs
  • apps/backend/src/openapi/mod.rs
  • apps/backend/src/openapi/responses.rs
  • apps/backend/src/utils/auth.rs
  • apps/backend/src/utils/db.rs
  • apps/backend/src/utils/verification_email_outbox.rs

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/backend/bruno/01-register-verify/01-register.bru`:
- Around line 25-30: The vars:pre-request block currently hardcodes baseUrl,
username, email, and password; remove those hardcoded entries and instead read
them from the shared environment file by referencing the environment variables
defined in environments/local.bru (baseUrl, email, password, username,
verificationToken) so the script uses the centralized environment values; update
any references in the 01-register.bru flow that assumed the inline vars to use
the environment variables instead.

In `@apps/backend/bruno/01-register-verify/02-verify-email.bru`:
- Around line 23-26: The pre-request block currently hardcodes verificationToken
(vars:pre-request { verificationToken: ... }), which overrides
environment/runtime values and causes expiry/mismatch failures; remove the
verificationToken entry from the pre-request vars so the flow uses the value
from environments/local.bru or the runtime-updated token, leaving baseUrl (and
other required entries) intact and ensuring verificationToken is set only in
environments/local.bru or updated at runtime.

In `@apps/backend/bruno/01-register-verify/03-login-unverified.bru`:
- Around line 24-27: Add a concrete test that asserts the unverified-login
behavior described in the docs: create or extend the tests block for
03-login-unverified.bru to perform the login request for an unverified email and
assert the HTTP status is 403 and the returned error code equals
"email-not-verified"; locate the test near the existing docs block in this file,
invoke the same login endpoint used by the scenario, and include assertions for
both the status and response error.code to ensure future regressions are caught.

In `@apps/backend/bruno/02-login/01-login.bru`:
- Around line 24-27: The vars:pre-request block currently hardcodes baseUrl and
email which must be moved to environment config; remove the hardcoded entries
from the vars:pre-request block in 01-login.bru and instead reference the
environment variables defined in environments/local.bru (ensure
environments/local.bru contains baseUrl, email, password, username,
verificationToken per guidelines), so the login scenario reads those variables
at runtime rather than using the fixed values.

In `@apps/backend/bruno/02-login/02-me.bru`:
- Around line 13-15: 現在の固定ヘッダ "Cookies: session=..." を削除し、正しいヘッダ名 "Cookie"
を使わないでください;代わりに Bruno の Cookie
管理を有効にして直前ログインで保存されたセッションCookieを再利用するように変更してください(固定の "session"
値を取り除き、リクエスト実行は同一コレクション内で順次実行して Cookie ストアが引き継がれるようにしてください)。

In `@apps/backend/bruno/environments/local.bru`:
- Around line 3-6: Replace the hardcoded/reused values in environments/local.bru
with explicit placeholders so re-runs don't collide: update the email, password,
username, and verificationToken entries to clearly indicate they must be filled
per-run (e.g., use "<your-unique-email@example.com>", "<your-password>",
"<your-username>", "<verification-token-from-email-or-apalis>"); ensure the keys
email, password, username, and verificationToken remain present but set to these
explicit placeholder strings so testers are forced to supply an unregistered
email and a valid verificationToken before running.

In `@apps/backend/Cargo.toml`:
- Around line 51-54: The apalis crate versions are inconsistent (apalis = rc.9
vs apalis-postgres/apalis-board = rc.8); update the Cargo.toml so the
apalis-family crates use the same RC (e.g., set apalis-postgres and apalis-board
to "1.0.0-rc.9" or pin apalis to "1.0.0-rc.8"), then run cargo update to
regenerate Cargo.lock and verify no duplicate resolutions; additionally check
apps/backend/migration/Cargo.lock to ensure it doesn't introduce a different
apalis RC and reconcile it if needed.

In `@apps/backend/src/entities/personal_tokens.rs`:
- Line 4: このファイルの不要なインポートを削除してください — 現在の行で宣言されている `use
crate::entities::scopes::Scope;` は `scopes`
フィールドを削除したため未使用になっています。該当インポートを取り除き、ビルド/コンパイル時の未使用警告を解消してください(参照箇所: `Scope`
インポート、`personal_tokens.rs` 内の `scopes` 関連コードが既に削除されていることを前提)。

In `@apps/backend/src/jobs/verification_email.rs`:
- Around line 65-73: The process function race allows an old retry job to
overwrite a newer token because process always calls
email_verification::store_token first; add a generation/issued_at field to
VerificationEmailJob (e.g., job.generation or job.issued_at) and change
email_verification::store_token to perform a conditional/atomic update that only
writes the token if the provided generation is strictly newer than the stored
one (use Redis compare-and-set/transaction or server-side check) so old jobs
cannot re-enable older tokens; update process to pass the new generation field
when calling store_token and ensure
verification_email_delivery::send_verification_email still receives the token
only after a successful conditional store.

In `@apps/backend/src/server.rs`:
- Around line 36-48: Extract the duplicated EnvFilter construction by first
computing the filter string once (e.g., let filter_str =
std::env::var("RUST_LOG").unwrap_or_else(|_| "info,sqlx=warn".into())), then
build a tracing_subscriber::EnvFilter from that value and reuse it in both
places (the TracingSubscriber::new(&broadcaster).layer().with_filter(...) that
produces board_tracing and the registry().with(...) call), using cloning if
necessary; update references to use the single env_filter variable instead of
creating EnvFilter twice.

In `@apps/backend/src/settings.rs`:
- Around line 25-27: The field verification_email_worker_concurrency currently
accepts 0 which can stop worker execution; add validation to ensure its value is
>= 1 during settings load/validation (e.g., in Settings::validate or
Settings::try_from after deserialization) and return a clear error (or panic on
invalid config) if it's < 1; reference the verification_email_worker_concurrency
field and the default_verification_email_worker_concurrency helper when
implementing the check so startup will fail fast when configuration is invalid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 90ed11e3-06b7-4dce-853f-be61e2f57c56

📥 Commits

Reviewing files that changed from the base of the PR and between 1371d4b and d8b66fc.

⛔ Files ignored due to path filters (1)
  • apps/backend/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • .vscode/extensions.json
  • apps/backend/.env.example
  • apps/backend/Cargo.toml
  • apps/backend/bruno/01-register-verify/01-register.bru
  • apps/backend/bruno/01-register-verify/02-verify-email.bru
  • apps/backend/bruno/01-register-verify/03-login-unverified.bru
  • apps/backend/bruno/01-register-verify/04-resend-verification.bru
  • apps/backend/bruno/01-register-verify/folder.bru
  • apps/backend/bruno/02-login/01-login.bru
  • apps/backend/bruno/02-login/02-me.bru
  • apps/backend/bruno/02-login/03-logout.bru
  • apps/backend/bruno/02-login/folder.bru
  • apps/backend/bruno/03-apalis/01-list-tasks.bru
  • apps/backend/bruno/03-apalis/folder.bru
  • apps/backend/bruno/README.md
  • apps/backend/bruno/bruno.json
  • apps/backend/bruno/collection.bru
  • apps/backend/bruno/environments/local.bru
  • apps/backend/src/entities/personal_tokens.rs
  • apps/backend/src/handlers/auth.rs
  • apps/backend/src/jobs/mod.rs
  • apps/backend/src/jobs/verification_email.rs
  • apps/backend/src/lib.rs
  • apps/backend/src/main.rs
  • apps/backend/src/server.rs
  • apps/backend/src/settings.rs
  • apps/backend/src/utils/mod.rs
💤 Files with no reviewable changes (1)
  • apps/backend/src/utils/mod.rs

Comment on lines +23 to +26
vars:pre-request {
verificationToken: trs1GDUsdryLKl-ckR2sVGMHWJIf5PaKwGaw2BOiTIY
baseUrl: http://localhost:3400
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

検証トークンの固定値が手順を破壊しています。

verificationToken を pre-request で固定すると、Env 設定(または最新トークン取得)が上書きされ、期限切れ/不一致で検証が失敗します。token は environments/local.bru または実行時更新値のみを使うようにしてください。

As per coding guidelines "Set required environment variables in environments/local.bru: baseUrl, email, password, username, verificationToken".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/bruno/01-register-verify/02-verify-email.bru` around lines 23 -
26, The pre-request block currently hardcodes verificationToken
(vars:pre-request { verificationToken: ... }), which overrides
environment/runtime values and causes expiry/mismatch failures; remove the
verificationToken entry from the pre-request vars so the flow uses the value
from environments/local.bru or the runtime-updated token, leaving baseUrl (and
other required entries) intact and ensuring verificationToken is set only in
environments/local.bru or updated at runtime.

Comment on lines +24 to +27
docs {
メール確認前に実行すると 403 email-not-verified が返る想定。
確認後は 4. ログイン を使う。
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

未認証ログインの期待結果をテストで固定してください。

docs に期待値はありますが、tests が無いため回帰を検出できません。403 とエラーコード(email-not-verified)の検証を追加してください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/bruno/01-register-verify/03-login-unverified.bru` around lines
24 - 27, Add a concrete test that asserts the unverified-login behavior
described in the docs: create or extend the tests block for
03-login-unverified.bru to perform the login request for an unverified email and
assert the HTTP status is 403 and the returned error code equals
"email-not-verified"; locate the test near the existing docs block in this file,
invoke the same login endpoint used by the scenario, and include assertions for
both the status and response error.code to ensure future regressions are caught.

Comment thread apps/backend/bruno/02-login/02-me.bru Outdated
Comment thread apps/backend/bruno/environments/local.bru Outdated
Comment thread apps/backend/Cargo.toml
Comment thread apps/backend/src/entities/personal_tokens.rs Outdated
Comment thread apps/backend/src/jobs/verification_email.rs
Comment thread apps/backend/src/server.rs Outdated
Comment thread apps/backend/src/settings.rs
@yupix yupix force-pushed the feat/verify-mail branch from d8b66fc to 2db4f55 Compare May 21, 2026 02:48
@TeamBlackCrystal TeamBlackCrystal deleted a comment from coderabbitai Bot May 21, 2026
@TeamBlackCrystal TeamBlackCrystal deleted a comment from coderabbitai Bot May 21, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/backend/src/handlers/auth.rs (1)

236-275: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

メールアドレスの正規化が不十分です

resend_verification_email では trim() のみ適用していますが、register では正規化なしで保存しています。PostgreSQL のデフォルトでは大文字小文字を区別するため、User@Example.com で登録後に user@example.com で再送リクエストするとユーザーが見つからず 404 になります。

登録時と再送時の両方でメールアドレスを小文字に正規化することを検討してください。

♻️ 修正案

registerresend_verification_email の両方で統一した正規化を適用:

 pub async fn register(
     State(state): State<AppState>,
     Valid(Json(payload)): Valid<Json<RegisterRequest>>,
 ) -> Result<(StatusCode, Json<String>), AuthError> {
     let RegisterRequest {
         username,
         email,
         password,
     } = payload;
+    let email = email.trim().to_lowercase();

     let password_hash = create_password_hash(&password)?;
 pub async fn resend_verification_email(
     State(state): State<AppState>,
     Valid(Json(payload)): Valid<Json<ResendVerificationRequest>>,
 ) -> Result<Json<String>, AuthError> {
-    let email = payload.email.trim().to_string();
+    let email = payload.email.trim().to_lowercase();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/handlers/auth.rs` around lines 236 - 275, The
resend_verification_email handler only trims the email but does not lowercase
it, causing mismatches with registrations; update both resend_verification_email
and the register path to apply a consistent normalization (trim() then
to_lowercase()) to the user's email before saving and before any lookups or
rate-limit checks (e.g., when calling
email_verification::try_acquire_resend_slot, users::Entity::find filter on
users::Column::Email, and when enqueuing VerificationEmailJob) so stored emails
and query inputs always use the same canonical lowercase form.
♻️ Duplicate comments (3)
apps/backend/src/settings.rs (1)

25-27: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

verification_email_worker_concurrency は起動時に >= 1 を検証した方が安全です。

現状は 0 設定でも受理され、後段で暗黙に丸められるため設定ミスを見逃します。設定ロード時に明示的に弾くと運用時の切り分けが容易です。

As per coding guidelines apps/backend/**/*.rs: 「非同期処理の安全性を優先して確認してください」。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/settings.rs` around lines 25 - 27, Add an explicit runtime
validation that verification_email_worker_concurrency is >= 1 when settings are
loaded: in the Settings type (the struct containing
verification_email_worker_concurrency) or its settings-load helper, check the
field after deserialization (use the existing
default_verification_email_worker_concurrency for fallback) and return an error
/ panic with a clear message if the value is 0; reference the
verification_email_worker_concurrency field and
default_verification_email_worker_concurrency function so the check is colocated
with settings deserialization/creation (or expose a Settings::validate() called
after load) to fail fast on invalid configuration.
apps/backend/src/jobs/verification_email.rs (1)

20-24: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

古いリトライジョブが新しい検証トークンを再上書きできる競合リスクがあります。

process() が毎回トークン保存を実行し、ジョブ側に世代情報(issued_at/generation)がないため、再送後に遅延した旧ジョブが走ると最新トークンを巻き戻す可能性があります。トークン保存は「より新しい世代のみ反映」の原子的条件付き更新にしてください。

As per coding guidelines apps/backend/**/*.rs: 「認証・認可、非同期処理の安全性を優先して確認してください」。

#!/bin/bash
set -euo pipefail

echo "== VerificationEmailJob definition =="
rg -n --type rust -C3 '\bstruct\s+VerificationEmailJob\b|\buser_id\b|\bemail\b|\btoken\b'

echo
echo "== store_token definition and call sites =="
rg -n --type rust -C4 '\bstore_token\s*\('

echo
echo "== resend/enqueue paths for verification jobs =="
rg -n --type rust -C4 '\benqueue\s*\(|resend|verification_email'

Also applies to: 65-73

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/jobs/verification_email.rs` around lines 20 - 24, The
VerificationEmailJob struct and its process() currently overwrite tokens
unconditionally, allowing stale retry jobs to roll back a newer token; update
the token persistence (store_token and where process() calls it) to perform an
atomic conditional update that only writes when the incoming token is newer
(compare an issued_at timestamp or a numeric generation on the user/token row).
Concretely: add a generation/issued_at field to VerificationEmailJob and include
that value when calling store_token, then change store_token to execute a single
atomic DB update (e.g., UPDATE ... SET token=?, generation=? WHERE user_id=? AND
(generation IS NULL OR generation < ?) or use a conditional INSERT/UPDATE in a
transaction/UPSERT) so old jobs cannot overwrite newer tokens; ensure process(),
VerificationEmailJob, and store_token signatures are updated consistently.
apps/backend/bruno/environments/local.bru (1)

3-6: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

再実行時に衝突しやすい初期値です。

email の固定値は再実行で登録衝突を起こしやすく、verificationToken 空値はそのまま実行時に失敗要因になります。埋め忘れ防止のプレースホルダにしてください。

🔧 修正例
 vars {
   baseUrl: http://localhost:3400
-  email: test-user@example.com
+  email: your-unregistered-email@example.com
   password: password123
   username: testuser
-  verificationToken:
+  verificationToken: paste-token-from-email-or-apalis-board
 }

As per coding guidelines apps/backend/bruno/**/environments/local.bru: Bruno environment variable email must be set to an unregistered email address. Based on learnings, same-email retests can conflict unless changed or DB is reset.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/bruno/environments/local.bru` around lines 3 - 6, Replace the
hard-coded email and empty verificationToken placeholders in the environment
block: change the email field from a fixed address to a non-conflicting
placeholder or pattern (e.g., a template like your-email+timestamp@example.com)
so tests won’t collide on re-runs, and set verificationToken to an explicit
placeholder string (e.g., <verification-token> or a generated token) instead of
leaving it empty; update the email and verificationToken entries referenced as
"email" and "verificationToken" in the local.bru environment to these
placeholder values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/backend/bruno/01-register-verify/04-resend-verification.bru`:
- Line 24: vars:pre-request に固定で書かれている baseUrl: http://localhost:3400 を残さないでください
— 代わりにリクエストで参照している変数名 {{baseUrl}} を使するか、該当の baseUrl エントリ自体を削除してください(対象の識別子:
baseUrl, vars:pre-request, {{baseUrl}})。これによりローカル固定値が環境変数を上書きするリスクを解消します。

---

Outside diff comments:
In `@apps/backend/src/handlers/auth.rs`:
- Around line 236-275: The resend_verification_email handler only trims the
email but does not lowercase it, causing mismatches with registrations; update
both resend_verification_email and the register path to apply a consistent
normalization (trim() then to_lowercase()) to the user's email before saving and
before any lookups or rate-limit checks (e.g., when calling
email_verification::try_acquire_resend_slot, users::Entity::find filter on
users::Column::Email, and when enqueuing VerificationEmailJob) so stored emails
and query inputs always use the same canonical lowercase form.

---

Duplicate comments:
In `@apps/backend/bruno/environments/local.bru`:
- Around line 3-6: Replace the hard-coded email and empty verificationToken
placeholders in the environment block: change the email field from a fixed
address to a non-conflicting placeholder or pattern (e.g., a template like
your-email+timestamp@example.com) so tests won’t collide on re-runs, and set
verificationToken to an explicit placeholder string (e.g., <verification-token>
or a generated token) instead of leaving it empty; update the email and
verificationToken entries referenced as "email" and "verificationToken" in the
local.bru environment to these placeholder values.

In `@apps/backend/src/jobs/verification_email.rs`:
- Around line 20-24: The VerificationEmailJob struct and its process() currently
overwrite tokens unconditionally, allowing stale retry jobs to roll back a newer
token; update the token persistence (store_token and where process() calls it)
to perform an atomic conditional update that only writes when the incoming token
is newer (compare an issued_at timestamp or a numeric generation on the
user/token row). Concretely: add a generation/issued_at field to
VerificationEmailJob and include that value when calling store_token, then
change store_token to execute a single atomic DB update (e.g., UPDATE ... SET
token=?, generation=? WHERE user_id=? AND (generation IS NULL OR generation < ?)
or use a conditional INSERT/UPDATE in a transaction/UPSERT) so old jobs cannot
overwrite newer tokens; ensure process(), VerificationEmailJob, and store_token
signatures are updated consistently.

In `@apps/backend/src/settings.rs`:
- Around line 25-27: Add an explicit runtime validation that
verification_email_worker_concurrency is >= 1 when settings are loaded: in the
Settings type (the struct containing verification_email_worker_concurrency) or
its settings-load helper, check the field after deserialization (use the
existing default_verification_email_worker_concurrency for fallback) and return
an error / panic with a clear message if the value is 0; reference the
verification_email_worker_concurrency field and
default_verification_email_worker_concurrency function so the check is colocated
with settings deserialization/creation (or expose a Settings::validate() called
after load) to fail fast on invalid configuration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d790fd2d-1583-4603-b205-c3d63a3ff171

📥 Commits

Reviewing files that changed from the base of the PR and between 1371d4b and 43e5187.

⛔ Files ignored due to path filters (1)
  • apps/backend/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • .vscode/extensions.json
  • apps/backend/.env.example
  • apps/backend/Cargo.toml
  • apps/backend/bruno/01-register-verify/01-register.bru
  • apps/backend/bruno/01-register-verify/02-verify-email.bru
  • apps/backend/bruno/01-register-verify/03-login-unverified.bru
  • apps/backend/bruno/01-register-verify/04-resend-verification.bru
  • apps/backend/bruno/01-register-verify/folder.bru
  • apps/backend/bruno/02-login/01-login.bru
  • apps/backend/bruno/02-login/02-me.bru
  • apps/backend/bruno/02-login/03-logout.bru
  • apps/backend/bruno/02-login/folder.bru
  • apps/backend/bruno/03-apalis/01-list-tasks.bru
  • apps/backend/bruno/03-apalis/folder.bru
  • apps/backend/bruno/README.md
  • apps/backend/bruno/bruno.json
  • apps/backend/bruno/collection.bru
  • apps/backend/bruno/environments/local.bru
  • apps/backend/src/entities/personal_tokens.rs
  • apps/backend/src/handlers/auth.rs
  • apps/backend/src/jobs/mod.rs
  • apps/backend/src/jobs/verification_email.rs
  • apps/backend/src/lib.rs
  • apps/backend/src/main.rs
  • apps/backend/src/server.rs
  • apps/backend/src/settings.rs
  • apps/backend/src/utils/mod.rs
💤 Files with no reviewable changes (1)
  • apps/backend/src/utils/mod.rs

Comment thread apps/backend/bruno/01-register-verify/04-resend-verification.bru Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/backend/src/handlers/auth.rs (1)

242-265: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

enqueue 失敗時に再送クールダウンだけが残ります。

Line 242 で枠を確保したあとに Line 260-265 のジョブ投入が失敗すると、メールは一度も送られていないのに同じアドレスの次回試行が 429 になります。enqueue 失敗時は補償的にクールダウンキーを削除するか、少なくとも「投入成功時のみ枠を確定する」形にしておかないと、503 からの自己回復が 60 秒止まります。 As per coding guidelines apps/backend/**/*.rs: 「エラーハンドリング、非同期処理の安全性を優先して確認してください。」

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/handlers/auth.rs` around lines 242 - 265, The resend
cooldown slot acquired by email_verification::try_acquire_resend_slot is never
released if verification_email::enqueue fails, causing false 429s; fix by either
(a) releasing/clearing the cooldown key on enqueue failure (call the
complementary email_verification::release_resend_slot or delete the Redis key)
inside the map_err branch after enqueue fails, or (b) change the flow so you
only call try_acquire_resend_slot after enqueue succeeds (i.e., perform
VerificationEmailJob creation and enqueue first, then acquire/confirm the slot);
update the handler around try_acquire_resend_slot, verification_email::enqueue,
and VerificationEmailJob to ensure the slot is only persisted when enqueue
completes successfully.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/backend/src/utils/email_verification.rs`:
- Around line 21-39: 現在の比較は current_gen > issued_at
のみなので、同一ミリ秒に発行された別トークンを区別できません。gen_key と issued_at の比較で等しい(current_gen ==
issued_at)の場合は、現在 user_key に入っているトークンが今回の ARGV[3]
と同一のときだけ再実行を許可するように条件を追加してください(等しくなければ 0 を返す)。対象箇所は gen_key/user_key
の取得と比較ロジック(current_gen と issued_at を扱うブロック)で、既存の古いトークン削除と新規 SET(token_key ←
ARGV[2], user_key ← ARGV[3], gen_key ← issued_at) の流れはそのまま維持してください。

---

Outside diff comments:
In `@apps/backend/src/handlers/auth.rs`:
- Around line 242-265: The resend cooldown slot acquired by
email_verification::try_acquire_resend_slot is never released if
verification_email::enqueue fails, causing false 429s; fix by either (a)
releasing/clearing the cooldown key on enqueue failure (call the complementary
email_verification::release_resend_slot or delete the Redis key) inside the
map_err branch after enqueue fails, or (b) change the flow so you only call
try_acquire_resend_slot after enqueue succeeds (i.e., perform
VerificationEmailJob creation and enqueue first, then acquire/confirm the slot);
update the handler around try_acquire_resend_slot, verification_email::enqueue,
and VerificationEmailJob to ensure the slot is only persisted when enqueue
completes successfully.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d9e6ebdb-00dc-46d6-918b-55799a981914

📥 Commits

Reviewing files that changed from the base of the PR and between 43e5187 and a6f311a.

📒 Files selected for processing (13)
  • apps/backend/bruno/01-register-verify/01-register.bru
  • apps/backend/bruno/01-register-verify/04-resend-verification.bru
  • apps/backend/bruno/02-login/02-me.bru
  • apps/backend/bruno/environments/local.bru
  • apps/backend/src/handlers/auth.rs
  • apps/backend/src/jobs/verification_email.rs
  • apps/backend/src/openapi/responses.rs
  • apps/backend/src/server.rs
  • apps/backend/src/settings.rs
  • apps/backend/src/utils/auth.rs
  • apps/backend/src/utils/email.rs
  • apps/backend/src/utils/email_verification.rs
  • apps/backend/src/utils/mod.rs

Comment thread apps/backend/src/utils/email_verification.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/backend/src/utils/email_verification.rs`:
- Line 17: The current logic treats issued_at (Unix ms) as the sole generation
key which can collide for concurrent register/resend actions; change the
VerificationEmailJob payload to carry a monotonic generation identifier (e.g.,
user-level incrementing u64 or a globally-unique job_sequence/UUID) in addition
to issued_at, update the job-creation path (the function that enqueues jobs,
e.g., enqueue_verification_email) to populate that generation value atomically
(DB sequence or atomic counter) instead of relying on wall-clock ms, and modify
the comparison logic in email_verification.rs (the verification job processor
that currently compares issued_at) to use the new generation field (or compare
generation first then issued_at as fallback) so only strictly older generations
are skipped; finally update related docs/comments and any shared DTO/entity
(VerificationEmailJob struct) and tests to reflect the new contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 83f584a0-9571-4026-b611-54991e144806

📥 Commits

Reviewing files that changed from the base of the PR and between a6f311a and 7113f41.

📒 Files selected for processing (1)
  • apps/backend/src/utils/email_verification.rs

use super::redis::RedisConnection;

/// 世代チェック後、旧 token キー削除 → 新 token/user/gen キー SET を一括実行。
/// 返却: 1 = 反映した, 0 = より新しい世代、または同世代で別トークンのためスキップ。
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

issued_at を Unix ミリ秒の単独キーにしているため、正当な別ジョブを取りこぼします。

この条件分岐は「同一 issued_at の別トークン」を安全側で落とせていますが、逆に同一ユーザーの register/resend が同一ミリ秒に発行された場合は後勝ちが保証されません。結果として、後から enqueue された正当なジョブでも Redis 到着順しだいで Ok(false) になり、再送成功扱いなのに新しい確認メールが送られない経路が残ります。issued_at を世代として使うなら、壁時計ミリ秒ではなく単調増加な値に変える必要があります。少なくとも VerificationEmailJob の payload、ジョブ作成側、ここの比較ロジック、関連ドキュメントの契約まで揃えて更新してください。 As per coding guidelines apps/backend/**/*.rs: 「認証・認可、テナント境界、SQL の正しさ、エラーハンドリング、非同期処理の安全性を優先して確認してください。ハンドラーは薄く保ち、共有 DTO、エンティティ、OpenAPI に影響する変更は必ず波及範囲を指摘してください。」

Also applies to: 24-39, 84-95

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/utils/email_verification.rs` at line 17, The current logic
treats issued_at (Unix ms) as the sole generation key which can collide for
concurrent register/resend actions; change the VerificationEmailJob payload to
carry a monotonic generation identifier (e.g., user-level incrementing u64 or a
globally-unique job_sequence/UUID) in addition to issued_at, update the
job-creation path (the function that enqueues jobs, e.g.,
enqueue_verification_email) to populate that generation value atomically (DB
sequence or atomic counter) instead of relying on wall-clock ms, and modify the
comparison logic in email_verification.rs (the verification job processor that
currently compares issued_at) to use the new generation field (or compare
generation first then issued_at as fallback) so only strictly older generations
are skipped; finally update related docs/comments and any shared DTO/entity
(VerificationEmailJob struct) and tests to reflect the new contract.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/新機能 New feature or request priority/medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

アカウント作成時にメールアドレスの認証を必要とする

1 participant