Skip to content

Comments

fix: 修复删除密钥的两个问题 (#431 #438)#439

Merged
ding113 merged 5 commits intodevfrom
fix/issue-431-438-key-deletion-bugs
Dec 24, 2025
Merged

fix: 修复删除密钥的两个问题 (#431 #438)#439
ding113 merged 5 commits intodevfrom
fix/issue-431-438-key-deletion-bugs

Conversation

@NightYuYyy
Copy link
Collaborator

问题描述

本 PR 修复了两个与删除密钥相关的 bug:

Issue #431: 删除禁用 key 时的错误提示

重现步骤

  1. 用户有多个 key,至少一个是启用状态
  2. 禁用其中一个 key
  3. 尝试删除这个已禁用的 key
  4. 系统错误地提示"该用户至少需要保留一个可用密钥,无法删除最后一个密钥"

预期行为:只要用户还有其他启用状态的 key,就应该允许删除已禁用的 key

Issue #438: 删除 key 后前端未刷新

问题描述:点击删除后,前端页面没有实时刷新,然后点编辑用户里面已删除的 key 还存在,保存会提示 key 不存在

根本原因分析

#431 原因

src/actions/keys.ts:478-484removeKey 函数中,未区分要删除的 key 是启用还是禁用状态,只要用户的启用 key 数量 ≤1,就阻止删除任何 key(包括禁用的 key)。

#438 原因

src/app/[locale]/dashboard/_components/user/user-key-table-row.tsxhandleDeleteKey 中,只调用了 router.refresh(),但没有使 React Query 缓存失效,导致前端显示的仍是缓存的旧数据。

修复方案

#431 修复

src/actions/keys.ts:478-488 中添加条件判断:

// 只有删除启用的密钥时,才需要检查是否是最后一个启用的密钥
// 删除禁用的密钥不会影响用户的可用密钥数量
if (key.isEnabled) {
  const activeKeyCount = await countActiveKeysByUser(key.userId);
  if (activeKeyCount <= 1) {
    return {
      ok: false,
      error: "该用户至少需要保留一个可用的密钥,无法删除最后一个密钥",
    };
  }
}

#438 修复

src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx 中:

  1. 导入 useQueryClient hook
  2. 在组件中初始化 queryClient
  3. handleDeleteKey 中添加缓存失效逻辑:
queryClient.invalidateQueries({ queryKey: ["users"] });

测试场景

场景 用户 keys 状态 删除操作 预期结果
1 key1(启用), key2(禁用), key3(禁用) 删除 key2 ✅ 成功删除
2 key1(启用), key2(禁用), key3(禁用) 删除 key1 ❌ 阻止删除
3 key1(启用), key2(启用), key3(禁用) 删除 key1 ✅ 成功删除
4 key1(启用), key2(启用), key3(禁用) 删除 key3 ✅ 成功删除
5 key1(启用) 删除 key1 ❌ 阻止删除

代码质量

  • ✅ TypeScript 类型检查通过
  • ✅ Biome 代码检查通过
  • ✅ 符合 SOLID、KISS、DRY 原则

Closes #431
Closes #438

**Issue #431**: 修复删除禁用 key 时的错误提示
- 问题:删除已禁用的 key 时,系统错误地提示"至少需要保留一个可用密钥"
- 原因:未区分要删除的 key 是启用还是禁用状态
- 修复:只有删除启用的 key 时才检查剩余启用 key 数量
- 位置:src/actions/keys.ts:478-488

**Issue #438**: 修复删除 key 后前端未刷新的问题
- 问题:删除 key 后,前端页面没有实时刷新,已删除的 key 仍然显示
- 原因:只调用了 router.refresh(),但没有使 React Query 缓存失效
- 修复:在删除成功后调用 queryClient.invalidateQueries({ queryKey: ["users"] })
- 位置:src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx:107,151

**变更内容**:
- src/actions/keys.ts: 添加 key.isEnabled 条件判断,只对启用的 key 进行数量检查
- src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx:
  - 导入 useQueryClient hook
  - 在 handleDeleteKey 中添加缓存失效逻辑

**符合原则**:
- SOLID: 单一职责,每个检查都有明确的职责
- KISS: 逻辑简单清晰,易于理解
- DRY: 与 toggleKeyEnabled 函数保持一致的验证逻辑
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @NightYuYyy, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求旨在解决与密钥删除功能相关的两个关键缺陷。首先,它修正了后端逻辑,允许用户在仍有其他启用密钥的情况下删除禁用密钥,从而提升了密钥管理的灵活性。其次,它通过在前端引入缓存失效机制,确保了删除操作后的用户界面能够即时反映最新的数据状态,显著改善了用户体验。

Highlights

  • 修复删除禁用密钥的逻辑错误: 解决了当用户拥有多个密钥(其中至少一个启用)时,无法删除禁用密钥的问题。现在,系统将仅在删除启用密钥时检查是否为最后一个可用密钥。
  • 解决前端删除密钥后未刷新问题: 引入 React Query 缓存失效机制,确保在删除密钥后,前端页面能够实时更新,避免显示旧数据。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added the size/XS Extra Small PR (< 50 lines) label Dec 24, 2025
Copy link
Contributor

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

Choose a reason for hiding this comment

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

Code Review

这个 PR 修复了两个关于删除密钥的问题:一个是后端逻辑,现在允许在有其他可用密钥时删除已禁用的密钥;另一个是前端问题,通过在删除后使 React Query 缓存失效来确保 UI 刷新。代码更改看起来是正确的,并直接解决了所述问题。我只在 removeKey 函数中发现了一个潜在的竞态条件问题,并提出了改进建议以提高其健壮性。

Comment on lines +480 to 488
if (key.isEnabled) {
const activeKeyCount = await countActiveKeysByUser(key.userId);
if (activeKeyCount <= 1) {
return {
ok: false,
error: "该用户至少需要保留一个可用的密钥,无法删除最后一个密钥",
};
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

此更改正确地解决了删除禁用密钥的问题。但是,这里存在一个潜在的竞态条件(race condition)。如果在 countActiveKeysByUser 检查和 deleteKey 执行之间,有另一个并发请求禁用了用户的其他密钥,这可能会导致用户最终没有可用的密钥,从而违反了业务规则。

为了确保操作的原子性并防止此竞态条件,整个检查和删除逻辑应该被包装在一个数据库事务中,这与此文件中的 batchUpdateKeys 函数处理类似问题的方式一致。在事务中,您应该:

  1. (可选,但推荐)为要更新的行加锁。
  2. 执行活动密钥数量检查。
  3. 执行删除操作。

这将保证检查和删除操作之间数据的一致性。

@github-actions github-actions bot added bug Something isn't working area:UI area:core labels Dec 24, 2025
@github-actions
Copy link
Contributor

🧪 测试结果

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

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

};
// 只有删除启用的密钥时,才需要检查是否是最后一个启用的密钥
// 删除禁用的密钥不会影响用户的可用密钥数量
if (key.isEnabled) {
Copy link
Contributor

Choose a reason for hiding this comment

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

[HIGH] [TEST-MISSING-CRITICAL] removeKey 新增分支缺少覆盖用例

Evidence: src/actions/keys.ts:480 新增逻辑:

if (key.isEnabled) {
  const activeKeyCount = await countActiveKeysByUser(key.userId);
  if (activeKeyCount <= 1) {
    return {
      ok: false,
      error: "该用户至少需要保留一个可用的密钥,无法删除最后一个密钥",
    };
  }
}

Why this is a problem: 该分支改变了「删除禁用 key」的行为,但当前测试集中没有断言“删除禁用 key 不应触发最后一个启用 key 检查”。一旦回归,很容易在 CI 中漏掉(vitest.config.ts:32-38 仍开启了 branches: 40 覆盖率阈值)。

Suggested fix (添加一个不依赖 Next 运行时的单元测试,用 mock 固定依赖):

// tests/unit/actions/remove-key.test.ts
import { expect, it, vi } from "vitest";

vi.mock("@/lib/auth", () => ({
  getSession: vi.fn(async () => ({ user: { id: 1, role: "admin" } })),
}));

const repoMock = {
  findKeyById: vi.fn(async () => ({ id: 123, userId: 1, isEnabled: false })),
  countActiveKeysByUser: vi.fn(async () => 1),
  deleteKey: vi.fn(async () => undefined),
  findKeyList: vi.fn(async () => []),
};

vi.mock("@/repository/key", () => repoMock);
vi.mock("@/actions/users", () => ({ syncUserProviderGroupFromKeys: vi.fn(async () => undefined) }));
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));

it("removeKey allows deleting disabled key without last-active check", async () => {
  const { removeKey } = await import("@/actions/keys");

  const res = await removeKey(123);

  expect(res.ok).toBe(true);
  expect(repoMock.countActiveKeysByUser).not.toHaveBeenCalled();
});

Copy link
Contributor

@github-actions github-actions bot left a comment

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 is small and changes key deletion semantics plus the dashboard refresh path. The behavior change in removeKey introduces a new branch that is currently untested and could regress without being caught.

PR Size: XS

  • Lines changed: 20
  • Files changed: 2

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 1 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

None.

High Priority Issues (Should Fix)

  • (90) [TEST-MISSING-CRITICAL] Add coverage for deleting a disabled key without triggering the last-active-key check (src/actions/keys.ts:480).

Review Coverage

  • Logic and correctness
  • Security (OWASP Top 10)
  • Error handling
  • Type safety
  • Documentation accuracy
  • Test coverage
  • Code clarity

Automated review by Codex AI

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

  • Reviewed PR #439 (fix: 修复删除密钥的两个问题 (#431 #438)), size classified as XS and applied label size/XS.
  • Posted 1 inline review comment on src/actions/keys.ts:480 for [HIGH] [TEST-MISSING-CRITICAL]: the new key.isEnabled branch in removeKey changes behavior but has no targeted test coverage; included a concrete vitest mock-based test snippet suggestion.
  • Submitted the required PR review summary via gh pr review --comment.

**问题1**: Key 侧快捷续期时间计算不准确
- 问题:快捷选择 7 天时,实际只延长约 7 天减去当前小时数
- 原因:使用 addDays() 后没有设置时间到当天结束 (23:59:59.999)
- 修复:添加 newDate.setHours(23, 59, 59, 999) 确保整天有效
- 位置:src/app/[locale]/dashboard/_components/user/forms/quick-renew-key-dialog.tsx:104-105

**问题2**: 用户侧快捷续期后前端未实时刷新
- 问题:续期后页面不刷新,显示旧数据
- 原因:只调用 router.refresh(),未使 React Query 缓存失效
- 修复:添加 queryClient.invalidateQueries({ queryKey: ["users"] })
- 位置:src/app/[locale]/dashboard/_components/user/user-management-table.tsx:122,356

**变更内容**:
- quick-renew-key-dialog.tsx: 添加时间设置到当天结束
- user-management-table.tsx:
  - 导入 useQueryClient hook
  - 在续期成功后使缓存失效

**验证**:
- ✅ TypeScript 类型检查通过
- ✅ 与用户侧 quick-renew-dialog.tsx 保持一致的时间处理逻辑
- ✅ 与 #438 修复保持一致的缓存失效策略
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

No significant issues identified in this PR. The changes are minimal, well-targeted bug fixes that follow existing codebase patterns.

PR Size: XS

  • Lines changed: 20 (14 additions, 6 deletions)
  • Files changed: 2

Change Analysis

src/actions/keys.ts (Issue #431 fix)

  • The fix correctly wraps the active key count check inside a key.isEnabled condition
  • Logic is sound: disabled keys don't contribute to the active key count (per countActiveKeysByUser), so deleting a disabled key shouldn't trigger the "last key" protection
  • Comments accurately describe the fix rationale

src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx (Issue #438 fix)

  • Properly imports and uses useQueryClient hook from @tanstack/react-query
  • The component is rendered within UsersPageClient which provides QueryClientProvider context
  • Query key ["users"] matches the existing pattern used throughout the codebase (verified in unified-edit-dialog.tsx, batch-edit-dialog.tsx)
  • Cache invalidation is correctly placed after successful deletion confirmation

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean (existing patterns maintained)
  • Type safety - Clean
  • Documentation accuracy - Clean (comments match implementation)
  • Test coverage - Existing tests are skipped (pending integration test refactor), but the fix logic is straightforward
  • Code clarity - Good

Automated review by Claude AI

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

No significant issues identified in this PR. The changes are minimal, well-targeted bug fixes that follow existing codebase patterns.

PR Size: XS

  • Lines changed: 20 (14 additions, 6 deletions)
  • Files changed: 2

Change Analysis

src/actions/keys.ts (Issue #431 fix)

  • The fix correctly wraps the active key count check inside a key.isEnabled condition
  • Logic is sound: disabled keys don't contribute to the active key count (per countActiveKeysByUser), so deleting a disabled key shouldn't trigger the "last key" protection
  • Comments accurately describe the fix rationale

src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx (Issue #438 fix)

  • Properly imports and uses useQueryClient hook from @tanstack/react-query
  • The component is rendered within UsersPageClient which provides QueryClientProvider context
  • Query key ["users"] matches the existing pattern used throughout the codebase (verified in unified-edit-dialog.tsx, batch-edit-dialog.tsx)
  • Cache invalidation is correctly placed after successful deletion confirmation

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean (existing patterns maintained)
  • Type safety - Clean
  • Documentation accuracy - Clean (comments match implementation)
  • Test coverage - Existing tests are skipped (pending integration test refactor), but the fix logic is straightforward
  • Code clarity - Good

Automated review by Claude AI

@github-actions
Copy link
Contributor

🧪 测试结果

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

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

- Add form submission validation to ensure at least one key remains enabled
- Disable Switch and show tooltip when trying to disable the last enabled key
- Add i18n translations for all locales (zh-CN, en, ja, ru, zh-TW)

Related to issue #431 and key deletion protection improvements
@github-actions
Copy link
Contributor

🧪 测试结果

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

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

NightYu added 2 commits December 24, 2025 18:38
Previously, when creating a new key through the edit user dialog with
isEnabled set to false, the key would still be created as enabled. This
was because:

1. addKey() function signature was missing the isEnabled parameter
2. The implementation hardcoded is_enabled to true
3. The frontend was passing the value but it was silently ignored

Changes:
- Add isEnabled parameter to addKey() function signature
- Use data.isEnabled ?? true in createKey() call (defaults to true)
- Pass isEnabled: key.isEnabled from unified-edit-dialog

Fixes the issue where disabled keys were always created as enabled.
Previously, the error message for CANNOT_DISABLE_LAST_KEY had a hardcoded
Chinese fallback text "无法禁用最后一个可用密钥". This violated i18n
principles because:

1. All 5 languages (en/ja/ru/zh-CN/zh-TW) already have proper translations
   in errors.json
2. The hardcoded fallback would show Chinese to non-Chinese users if i18n
   system failed
3. It's inconsistent with other error messages that don't have fallbacks

Changes:
- Removed hardcoded fallback from batchUpdateKeys (2 occurrences)
- Now relies on proper i18n translations from errors.json

Related translations:
- en: "Cannot disable the last active key..."
- zh-CN: "无法禁用最后一个可用密钥..."
- zh-TW: "無法禁用最後一個可用金鑰..."
@github-actions
Copy link
Contributor

🧪 测试结果

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

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

@ding113 ding113 merged commit 1c7ee2f into dev Dec 24, 2025
8 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Dec 24, 2025
@NightYuYyy NightYuYyy deleted the fix/issue-431-438-key-deletion-bugs branch December 26, 2025 05:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core area:UI bug Something isn't working size/XS Extra Small PR (< 50 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants