Skip to content

fix(keys): removeKey 错误码透传 + 密钥创建弹窗文案与真实行为对齐#1266

Merged
ding113 merged 1 commit into
devfrom
fix/key-remove-error-code-and-created-copy
Jun 11, 2026
Merged

fix(keys): removeKey 错误码透传 + 密钥创建弹窗文案与真实行为对齐#1266
ding113 merged 1 commit into
devfrom
fix/key-remove-error-code-and-created-copy

Conversation

@ding113

@ding113 ding113 commented Jun 11, 2026

Copy link
Copy Markdown
Owner

问题

Fixes #1256

1. removeKey 错误返回缺少 errorCode,真实错误被吞掉

src/actions/keys.tsremoveKey 所有错误返回都没有设置 errorCode 字段,而 keys handler 的 actionError()errorCode 为 undefined 时兜底为 400 + key.action_failed,且 detailpublicActionErrorDetail() 置为通用文案("Bad request")。REST 桥接路径上客户端 toast 只显示 res.error(即通用 detail),导致"该用户至少需要保留一个可用的密钥"等真实原因完全无法透出。

2. 密钥创建弹窗宣称"完整密钥仅显示一次",与实际行为不符

创建密钥后的提示称"完整密钥仅显示一次,请立即复制并妥善保存"/"关闭后将无法再次查看此密钥",但实际上 owner 和 admin 随时可以通过密钥列表的 reveal 功能(getUnmaskedKey / GET /api/v1/keys/{id}:reveal)重新查看并复制完整密钥。

修复

removeKey 错误码(含全链路透传)

  • removeKey 全部 6 个错误路径补齐 errorCode 并将硬编码中文消息 i18n 化:
    • 未登录 → UNAUTHORIZED(403)
    • 密钥不存在 → NOT_FOUND(404)
    • 越权 → PERMISSION_DENIED(403)
    • 最后一个启用密钥 → 新增业务码 CANNOT_DELETE_LAST_KEY(400)
    • 删除后供应商分组清空 → 新增业务码 CANNOT_DELETE_LAST_GROUP_KEY(400)
    • 异常兜底 → DELETE_FAILED(400)
  • ERROR_CODES 新增 2 个业务码,5 语言 errors.json 补齐对应翻译
  • 两个删除入口(user-key-table-row.tsx / delete-key-confirm.tsx)改为优先按 errorCode 走 errors 命名空间翻译(与 add-key-form.tsx 既有模式一致),无 errorCode 时保留原 fallback

创建弹窗文案(5 语言 × 4 处)

  • keyListHeader.keyCreatedDialog.description / warningText
  • addKeyForm.generatedKey.hint
  • userManagement.createDialog.keyHint

统一改为:可在列表中管理并随时重新查看完整密钥,请妥善保管避免泄露。遵循 ja 半角括号、zh-TW 全角括号的既有规则。

测试(TDD,先红后绿)

  • tests/unit/actions/keys-remove-error-codes.test.ts:7 个用例覆盖全部错误路径的 errorCode + i18n 消息及成功路径
  • tests/unit/api/v1/keys-delete-error-mapping.test.ts:DELETE handler 对各 errorCode 的 404/403/400 映射与透传契约
  • tests/unit/delete-key-error-toast-ui.test.tsx:UI 按 errorCode 翻译 toast、无 errorCode 时回退原始消息
  • tests/unit/i18n/key-created-copy.test.ts:5 语言守护——禁止"仅显示一次"类表述、必须包含"可重新查看"表述、新错误码翻译存在且具体

验证

  • bun run build / lint / typecheck / test(根配置全量)/ test:v1 全部通过
  • test:integration / test:e2e 通过(无 DSN 自动跳过 DB 用例)
  • 4 个特性覆盖率配置(my-usage / proxy-guard-pipeline / include-session-id-in-errors / usage-logs-sessionid-search)在干净 dev 基线上以完全相同的数字失败,确认为存量问题,与本 PR 无关

🤖 Generated with Claude Code

Greptile Summary

This PR fixes two distinct issues: removeKey action errors were silently swallowed (no errorCode on any return path, so clients only ever saw a generic "Bad request" detail), and the key-creation dialogs incorrectly told users the full key could never be seen again when owners/admins can re-reveal it via the key list at any time.

  • All 6 error paths in removeKey now include a structured errorCode; hard-coded Chinese messages are replaced with i18n lookups; two new business codes (CANNOT_DELETE_LAST_KEY, CANNOT_DELETE_LAST_GROUP_KEY) are added to BUSINESS_ERRORS and translated across all 5 locales.
  • Both delete-key UI entry points (delete-key-confirm.tsx, user-key-table-row.tsx) now prioritise errorCode for toast messages, matching the existing pattern in add-key-form.tsx.
  • Key-creation copy is updated in all 5 locales to accurately describe the reveal-from-list capability, backed by a regex-based i18n regression test.

Confidence Score: 4/5

Safe to merge — changes are well-scoped to error handling and copy, with comprehensive unit test coverage across all 6 error paths.

The error-code propagation chain is correctly implemented end-to-end (action → REST bridge → UI toast), translations are present in all 5 locales, and the new codes are guarded by regression tests. One leftover: the catch block still places the raw error.message in the error field even though the UI now ignores it, meaning a DB-level exception message is still serialised into the REST response body.

The catch block in src/actions/keys.ts (around line 789) is the only spot worth a second look — the error field path vs. the errorCode display path now diverge for exception cases.

Important Files Changed

Filename Overview
src/actions/keys.ts All 6 error paths in removeKey now include errorCode; catch block raw error.message still flows into the error field while errorCode silently overrides it for the UI.
src/lib/utils/error-messages.ts Two new business error codes (CANNOT_DELETE_LAST_KEY, CANNOT_DELETE_LAST_GROUP_KEY) added to BUSINESS_ERRORS and propagated into the merged ERROR_CODES object.
src/app/[locale]/dashboard/_components/user/forms/delete-key-confirm.tsx Adds getErrorMessage fallback using errorCode translation, consistent with the existing add-key-form.tsx pattern.
src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx Same errorCode-first toast pattern applied to the inline row delete handler as in delete-key-confirm.tsx.
tests/unit/actions/keys-remove-error-codes.test.ts 7 test cases covering all error paths and the success path; uses dynamic import pattern inside each test which relies on module caching.
tests/unit/api/v1/keys-delete-error-mapping.test.ts Verifies the REST handler correctly maps each errorCode to its expected HTTP status and preserves the errorCode in the response body.
tests/unit/delete-key-error-toast-ui.test.tsx Regression test confirming the UI toasts the errorCode translation instead of the generic Bad request detail, with a fallback test for the no-errorCode case.
tests/unit/i18n/key-created-copy.test.ts I18n guard test enforcing no 'only shown once' patterns and presence of 'view again' markers across all 5 locales for 4 copy paths, plus translation existence checks for the new error codes.

Sequence Diagram

sequenceDiagram
    participant UI as Dashboard UI
    participant Bridge as REST Bridge
    participant Action as removeKey
    participant DB as Repository

    UI->>Bridge: "DELETE /api/v1/keys/{id}"
    Bridge->>Action: removeKey(keyId)

    alt No session
        Action-->>Bridge: ok:false, errorCode:UNAUTHORIZED
        Bridge-->>UI: 403
    else Key not found
        Action-->>Bridge: ok:false, errorCode:NOT_FOUND
        Bridge-->>UI: 404
    else Permission denied
        Action-->>Bridge: ok:false, errorCode:PERMISSION_DENIED
        Bridge-->>UI: 403
    else Last enabled key
        Action-->>Bridge: ok:false, errorCode:CANNOT_DELETE_LAST_KEY
        Bridge-->>UI: 400
    else Last group key (non-admin)
        Action-->>Bridge: ok:false, errorCode:CANNOT_DELETE_LAST_GROUP_KEY
        Bridge-->>UI: 400
    else DB error
        DB-->>Action: throws Error
        Action-->>Bridge: ok:false, errorCode:DELETE_FAILED
        Bridge-->>UI: 400
    else Success
        Action->>DB: deleteKey(keyId)
        DB-->>Action: ok
        Action-->>Bridge: ok:true
        Bridge-->>UI: 204
    end

    UI->>UI: errorCode present → getErrorMessage(tErrors, errorCode) else res.error
Loading

Comments Outside Diff (1)

  1. tests/unit/actions/keys-remove-error-codes.test.ts, line 456-464 (link)

    P2 await import inside tests shares the cached module instance

    Each test does await import("@/actions/keys") inside the test body. Because Node/Vitest caches modules after the first load, all 7 tests receive the same module object. vi.clearAllMocks() resets mock call counts and queued return values, which is sufficient here, but if a future test needs a fresh module state (e.g. to change a module-level constant) the cached import will silently reuse the old module. The conventional pattern is to hoist the import or use vi.resetModules() / vi.isolateModules() when per-test fresh imports are actually required.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: tests/unit/actions/keys-remove-error-codes.test.ts
    Line: 456-464
    
    Comment:
    **`await import` inside tests shares the cached module instance**
    
    Each test does `await import("@/actions/keys")` inside the test body. Because Node/Vitest caches modules after the first load, all 7 tests receive the same module object. `vi.clearAllMocks()` resets mock call counts and queued return values, which is sufficient here, but if a future test needs a fresh module state (e.g. to change a module-level constant) the cached import will silently reuse the old module. The conventional pattern is to hoist the import or use `vi.resetModules()` / `vi.isolateModules()` when per-test fresh imports are actually required.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/actions/keys.ts:786-799
**Raw exception message leaks into REST response body**

When the catch block fires with a real `Error`, `error.message` (e.g. a raw DB driver message) is placed in the `error` field and then serialised into the REST response body. The dashboard UI now silently ignores that field (it switches to the `DELETE_FAILED` translation as soon as `errorCode` is present), but any REST API consumer that inspects the response body still sees the internal message. Consider using the sanitised translation even for `Error` instances in the catch path so the `error` field is safe to expose: `const message = tError("DELETE_KEY_FAILED");`.

### Issue 2 of 2
tests/unit/actions/keys-remove-error-codes.test.ts:456-464
**`await import` inside tests shares the cached module instance**

Each test does `await import("@/actions/keys")` inside the test body. Because Node/Vitest caches modules after the first load, all 7 tests receive the same module object. `vi.clearAllMocks()` resets mock call counts and queued return values, which is sufficient here, but if a future test needs a fresh module state (e.g. to change a module-level constant) the cached import will silently reuse the old module. The conventional pattern is to hoist the import or use `vi.resetModules()` / `vi.isolateModules()` when per-test fresh imports are actually required.

Reviews (1): Last reviewed commit: "fix(keys): translate delete errors and c..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

The removeKey action previously returned hardcoded Chinese strings or
generic messages, causing the UI to display unhelpful toasts via the
REST bridge. It now returns specific error codes that the UI translates
through the errors namespace.

Additionally, the key creation dialogs incorrectly claimed the full key
is only shown once. The copy now accurately reflects that owners and
admins can reveal and copy the full key from the key list at any time.

Includes regression tests for the action error codes, API error mapping,
UI toast translation, and i18n copy guards.
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

总体概述

这个 PR 改进了密钥删除的错误处理机制。将密钥创建成功后的文案从"仅显示一次"改为"可随时重新查看",同时新增两个业务错误码(不能删除最后一个密钥、删除后无可用分组),在后端验证逻辑中返回对应的错误码,前端根据错误码翻译后展示给用户,并通过完整的测试覆盖验证各个环节。

变更说明

多语言文案与错误码更新

图层 / 文件 概述
多语言文案与错误码定义
messages/en/dashboard.json, messages/en/errors.json, messages/ja/dashboard.json, messages/ja/errors.json, messages/ru/dashboard.json, messages/ru/errors.json, messages/zh-CN/dashboard.json, messages/zh-CN/errors.json, messages/zh-TW/dashboard.json, messages/zh-TW/errors.json
在所有 5 种语言中统一更新密钥创建和查看相关的文案,将强调"仅显示一次"的表述改为"可随时在列表中重新查看";新增 CANNOT_DELETE_LAST_KEYCANNOT_DELETE_LAST_GROUP_KEY 两个错误码的翻译。
错误码常量定义
src/lib/utils/error-messages.ts
BUSINESS_ERRORS 中定义新增的两个业务错误码,使其纳入 ErrorCode 联合类型,供客户端和服务端的 i18n 翻译调用。
后端删除逻辑与错误处理
src/actions/keys.ts
修改 removeKey action 的错误分支:在未登录、密钥不存在、权限不足、删除最后一个密钥、删除后无可用分组等情况下,返回对应的 i18n 错误消息和 errorCode;异常捕获中统一使用 i18n 获取错误文案并返回 DELETE_FAILED 错误码。
前端错误处理与展示
src/app/[locale]/dashboard/_components/user/forms/delete-key-confirm.tsx, src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx
引入 getErrorMessage 工具和 errors 命名空间的翻译,在删除失败时优先根据 errorCode 生成用户友好的错误提示,缺失 errorCode 时回退到通用错误消息。
后端删除逻辑单元测试
tests/unit/actions/keys-remove-error-codes.test.ts
removeKey action 添加全面的单元测试,覆盖所有失败分支(未登录、密钥不存在、权限不足、删除最后一个密钥、删除后无分组、数据库异常)和成功情况,验证返回值和删除操作调用。
API 错误码映射测试
tests/unit/api/v1/keys-delete-error-mapping.test.ts
测试 REST API 端点 DELETE /api/v1/keys/{keyId} 的错误码到 HTTP 状态码映射,验证业务错误返回 400,权限和存在性错误返回 403/404。
UI 错误提示集成测试
tests/unit/delete-key-error-toast-ui.test.tsx
测试删除确认对话框中的 toast 错误展示,验证优先使用 errorCode 翻译后的消息,无 errorCode 时显示原始错误文本。
多语言与错误码回归测试
tests/unit/i18n/key-created-copy.test.ts
跨多语言验证密钥创建文案已去除"仅显示一次"的表述、新增错误码在各语言中均有独立的非空翻译。

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 标题准确总结了主要改动:修复 removeKey 的错误码透传和密钥创建弹窗文案与实际行为的对齐,这正是 PR 的核心目标。
Description check ✅ Passed PR 描述详细说明了两个问题、修复方案、测试覆盖和验证结果,完全对应代码变更内容,信息充分且相关。
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 fix/key-remove-error-code-and-created-copy

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added bug Something isn't working area:i18n area:UI labels Jun 11, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request updates API key deletion error handling and refactors key creation copy across multiple locales to accurately reflect that keys can be viewed again. It introduces specific error codes (such as CANNOT_DELETE_LAST_KEY and CANNOT_DELETE_LAST_GROUP_KEY) and updates the backend action, API mapping, and frontend UI to display localized error messages instead of generic errors. Comprehensive unit tests have also been added. The review feedback suggests hoisting the tError initialization outside of the try block in removeKey to avoid duplicate translation fetching in the catch block.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/actions/keys.ts
Comment on lines 687 to +689
export async function removeKey(keyId: number): Promise<ActionResult> {
try {
const tError = await getTranslations("errors");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

建议将 tError 的获取提升到 try 块外部。这样 tError 变量在 trycatch 块中都处于同一作用域内,从而避免在 catch 块中重复调用 getTranslations("errors") 并重新声明该变量,提升代码执行效率和可维护性。

Suggested change
export async function removeKey(keyId: number): Promise<ActionResult> {
try {
const tError = await getTranslations("errors");
export async function removeKey(keyId: number): Promise<ActionResult> {
const tError = await getTranslations("errors");
try {

Comment thread src/actions/keys.ts
Comment on lines +788 to +789
const tError = await getTranslations("errors");
const message = error instanceof Error ? error.message : tError("DELETE_KEY_FAILED");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

配合将 tError 提升到 try 块外部的修改,这里可以移除重复的 getTranslations 调用和变量声明,直接使用外层作用域的 tError

Suggested change
const tError = await getTranslations("errors");
const message = error instanceof Error ? error.message : tError("DELETE_KEY_FAILED");
const message = error instanceof Error ? error.message : tError("DELETE_KEY_FAILED");

Comment thread src/actions/keys.ts
Comment on lines 786 to +799
@@ -780,7 +796,7 @@ export async function removeKey(keyId: number): Promise<ActionResult> {
errorMessage: "DELETE_FAILED",
redactExtraKeys: ["key"],
});
return { ok: false, error: message };
return { ok: false, error: message, errorCode: ERROR_CODES.DELETE_FAILED };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Raw exception message leaks into REST response body

When the catch block fires with a real Error, error.message (e.g. a raw DB driver message) is placed in the error field and then serialised into the REST response body. The dashboard UI now silently ignores that field (it switches to the DELETE_FAILED translation as soon as errorCode is present), but any REST API consumer that inspects the response body still sees the internal message. Consider using the sanitised translation even for Error instances in the catch path so the error field is safe to expose: const message = tError("DELETE_KEY_FAILED");.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/keys.ts
Line: 786-799

Comment:
**Raw exception message leaks into REST response body**

When the catch block fires with a real `Error`, `error.message` (e.g. a raw DB driver message) is placed in the `error` field and then serialised into the REST response body. The dashboard UI now silently ignores that field (it switches to the `DELETE_FAILED` translation as soon as `errorCode` is present), but any REST API consumer that inspects the response body still sees the internal message. Consider using the sanitised translation even for `Error` instances in the catch path so the `error` field is safe to expose: `const message = tError("DELETE_KEY_FAILED");`.

How can I resolve this? If you propose a fix, please make it concise.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/actions/keys.ts (1)

788-789: ⚡ Quick win

消除冗余的翻译获取调用。

第 788 行在 catch 块中重新调用 getTranslations("errors"),但外层作用域已在第 689 行声明了 tError。由于 try 块从第 688 行开始,当错误发生在第 689 行之后时(大多数情况),外层的 tError 已经可用,重复获取翻译是低效的。

建议直接使用外层的 tError 或添加条件判断。

改进建议
  } catch (error) {
    logger.error("删除密钥失败:", error);
-   const tError = await getTranslations("errors");
-   const message = error instanceof Error ? error.message : tError("DELETE_KEY_FAILED");
+   const message = error instanceof Error ? error.message : tError("DELETE_KEY_FAILED");
    emitActionAudit({
      category: "key",
      action: "key.delete",
      targetType: "key",
      targetId: String(keyId),
      success: false,
      errorMessage: "DELETE_FAILED",
      redactExtraKeys: ["key"],
    });
    return { ok: false, error: message, errorCode: ERROR_CODES.DELETE_FAILED };
  }
🤖 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 `@src/actions/keys.ts` around lines 788 - 789, The catch block redundantly
calls getTranslations("errors") again even though tError was already declared in
the outer scope; update the catch handling in the function that uses tError so
it reuses the outer tError (or only calls getTranslations if tError is
undefined) instead of always invoking getTranslations("errors") again, and
ensure the error message assignment uses the existing tError variable (and keeps
the fallback to error.message when error instanceof Error).
🤖 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.

Nitpick comments:
In `@src/actions/keys.ts`:
- Around line 788-789: The catch block redundantly calls
getTranslations("errors") again even though tError was already declared in the
outer scope; update the catch handling in the function that uses tError so it
reuses the outer tError (or only calls getTranslations if tError is undefined)
instead of always invoking getTranslations("errors") again, and ensure the error
message assignment uses the existing tError variable (and keeps the fallback to
error.message when error instanceof Error).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9866cda1-89ae-4a70-9c2b-19a931e03604

📥 Commits

Reviewing files that changed from the base of the PR and between 499d925 and 78b71ef.

📒 Files selected for processing (18)
  • messages/en/dashboard.json
  • messages/en/errors.json
  • messages/ja/dashboard.json
  • messages/ja/errors.json
  • messages/ru/dashboard.json
  • messages/ru/errors.json
  • messages/zh-CN/dashboard.json
  • messages/zh-CN/errors.json
  • messages/zh-TW/dashboard.json
  • messages/zh-TW/errors.json
  • src/actions/keys.ts
  • src/app/[locale]/dashboard/_components/user/forms/delete-key-confirm.tsx
  • src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx
  • src/lib/utils/error-messages.ts
  • tests/unit/actions/keys-remove-error-codes.test.ts
  • tests/unit/api/v1/keys-delete-error-mapping.test.ts
  • tests/unit/delete-key-error-toast-ui.test.tsx
  • tests/unit/i18n/key-created-copy.test.ts

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Summary

This PR cleanly addresses two related issues: (1) removeKey error paths lacked errorCode, causing the REST bridge to return generic "Bad request" details instead of the real reason; (2) key-creation dialog copy incorrectly claimed the full key could "only be shown once" despite the reveal feature existing. Both fixes are well-scoped with end-to-end coverage across the action layer, REST handler, UI toast, and i18n files.

PR Size: L

  • Lines changed: 588 (558 additions, 30 deletions)
  • Files changed: 18

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Review Notes

The PR is well-executed. Specific observations that passed validation:

  • Error code propagation chain: All 6 error paths in removeKey now include errorCode with proper i18n messages. The REST handler's actionError() correctly maps NOT_FOUND to 404, UNAUTHORIZED/PERMISSION_DENIED to 403, and business codes to 400.
  • i18n completeness: Both new error codes (CANNOT_DELETE_LAST_KEY, CANNOT_DELETE_LAST_GROUP_KEY) have translations in all 5 locales. All pre-existing translation keys used by the action (KEY_NOT_FOUND, UNAUTHORIZED, PERMISSION_DENIED, DELETE_KEY_FAILED) are confirmed present.
  • UI consistency: Both delete entry points (delete-key-confirm.tsx, user-key-table-row.tsx) follow the same errorCode-first pattern already established in add-key-form.tsx, with proper fallback to raw error for backward compatibility.
  • Test coverage: 4 new test files provide 7 action tests, 7 handler mapping tests, 2 UI toast tests, and 5-locale i18n regression tests. Coverage is comprehensive.
  • Pre-existing note: The catch block in removeKey still passes error.message into the error field for Error instances, which can leak internal DB messages into the REST response body. However, this is pre-existing (unchanged in this PR) and the new errorCode field ensures the UI uses the sanitized translation instead.

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Comprehensive (4 new test files)
  • Code clarity - Good

Automated review by Claude AI

@ding113

ding113 commented Jun 11, 2026

Copy link
Copy Markdown
Owner Author

评审复核(deep-review + /code-review max effort)

本轮多代理评审已覆盖本 PR,未发现需要改动的代码问题:removeKey 六条错误路径的 errorCode 透传、五语言 errors.json 文案、两处删除入口的 errorCode 翻译均经独立验证确认正确;弹窗文案"可随时重新查看"与真实 reveal 行为一致(reveal 鉴权 owner/admin 未变)。

唯一的红色检查 pr-review 与代码无关:它是 OpenAI codex-action 评审 Action 的网络流断开(stream disconnected before completion / Reconnecting... 5/5 / codex exited with code 1),其余所有真实门禁(Code Quality / Docker Build / 单测 / API / CodeRabbit / Greptile)均通过。已触发重跑。

@ding113
ding113 merged commit 7b9cefb into dev Jun 11, 2026
22 of 24 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap Jun 11, 2026
@github-actions github-actions Bot mentioned this pull request Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:i18n area:UI bug Something isn't working size/L Large PR (< 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant