Skip to content

feat: add quick toggle and renew for users and keys#414

Merged
ding113 merged 1 commit intodevfrom
feat/user-key-quick-toggle-and-renew
Dec 23, 2025
Merged

feat: add quick toggle and renew for users and keys#414
ding113 merged 1 commit intodevfrom
feat/user-key-quick-toggle-and-renew

Conversation

@NightYuYyy
Copy link
Collaborator

@NightYuYyy NightYuYyy commented Dec 23, 2025

Summary

This PR adds quick enable/disable toggle switches and quick renewal functionality for users and keys in the User Management page, with optimistic UI updates for instant feedback.

这个 PR 在用户管理页面为用户和密钥添加了快速启用/禁用开关和快捷续期功能,并使用乐观更新模式实现 UI 即时响应。

Problem

Managing user and key status in the User Management page required opening edit dialogs and navigating through forms, which was cumbersome for simple operations like:

  • Toggling a user or key's enabled/disabled status
  • Quickly extending expiration dates
  • Viewing expiration status at a glance

Related Issues:

Builds on:

Solution

Implemented inline quick actions with optimistic updates:

  1. User Enable/Disable Toggle: Added Switch component in user table rows with confirmation dialog
  2. Key Enable/Disable Toggle: Added Switch component in key rows with last-key protection
  3. Key Expiration Display: Shows expiration date inline with click-to-renew functionality
  4. Quick Renew Dialog for Keys: New dialog component with quick duration options (7d/30d/90d/1y) and custom date picker
  5. Optimistic Updates: UI updates immediately on toggle, with automatic rollback on failure
  6. Last Key Protection: Restored logic to prevent disabling the last active key for a user

Changes

Core Changes

  • key-row-item.tsx: Added key enable/disable Switch, expiration display, and quick renew integration (+188/-18)
  • user-key-table-row.tsx: Added user enable/disable Switch with optimistic update (+93/-19)
  • user-management-table.tsx: Added optimistic update state management (+26/-1)
  • quick-renew-key-dialog.tsx: New component for key quick renewal with duration options (+293)

Backend Changes

  • keys.ts: Restored last key protection logic, added revalidatePath for dashboard/users (+3/-2)
  • users.ts: Added revalidatePath for dashboard/users (+1)

UI Improvements

  • quick-renew-dialog.tsx: Enhanced UI with clearer labels distinguishing "extend duration" vs "set date" (+73/-61)

i18n

  • Added missing translations for user enable/disable confirmation dialogs in EN and ZH-CN
  • Added new error messages: KEY_NOT_FOUND, CREATE/UPDATE/DELETE_KEY_FAILED, CANNOT_DISABLE_LAST_KEY

Testing

Manual Testing

  1. Toggle user enabled status via Switch - verify UI updates immediately
  2. Toggle key enabled status via Switch - verify UI updates immediately
  3. Try to disable the last enabled key for a user - verify error message appears
  4. Click on expiration date to open quick renew dialog
  5. Test quick renew with preset durations (7d/30d/90d/1y)
  6. Test quick renew with custom date picker
  7. Verify state correctly rolls back on operation failure

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally
  • Translations added for new UI strings

Description enhanced by Claude AI

- Add enable/disable Switch for users in user management table
- Add enable/disable Switch for keys in key row
- Add expiration time display for keys with quick renew dialog
- Implement optimistic updates for immediate UI feedback
- Add last key protection to prevent disabling the only active key
- Add missing translations for enable status confirmation dialogs
@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

  • 用户和密钥快捷开关: 在用户管理页面和密钥行中添加了启用/禁用快捷开关(Switch),方便快速管理用户和密钥状态。
  • 密钥过期时间显示与快捷续期: 密钥行现在显示过期时间,并支持点击进行快捷续期操作,提供了预设选项和自定义日期选择。
  • 乐观更新模式: 用户和密钥的启用/禁用以及密钥续期功能均采用乐观更新模式,实现 UI 的即时响应,提升用户体验。
  • 最后一个密钥保护逻辑: 恢复并完善了保护逻辑,防止用户禁用其唯一的启用密钥,确保服务连续性。
  • 多语言支持: 补充了启用/禁用确认对话框的缺失翻译,并新增了密钥操作相关的错误信息翻译。
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 enhancement New feature or request area:UI area:i18n labels Dec 23, 2025
@github-actions
Copy link
Contributor

🧪 测试结果

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

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

@github-actions github-actions bot added the size/L Large PR (< 1000 lines) label Dec 23, 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

这次的 Pull Request 质量很高,为用户和密钥管理页面增加了快捷启用/禁用开关和续期功能,并采用了乐观更新策略,极大地提升了用户体验。代码结构清晰,逻辑严谨。

我主要提出了一些关于国际化(i18n)的改进建议,以确保新添加的 UI 文本和错误信息能够支持多语言。具体来说:

  • 在几个组件中发现了一些硬编码的中文字符串,建议将它们移至 messages 翻译文件中。
  • 发现了一个重复的辅助函数,建议提取以提高代码复用性。

整体而言,这是一次出色的功能增强。在解决上述小问题后,代码将更加完善。

Comment on lines +53 to +61
function getTranslation(translations: Record<string, unknown>, path: string, fallback: string) {
const value = path.split(".").reduce<unknown>((acc, key) => {
if (acc && typeof acc === "object" && key in (acc as Record<string, unknown>)) {
return (acc as Record<string, unknown>)[key];
}
return undefined;
}, translations);
return typeof value === "string" && value.trim() ? value : fallback;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

getTranslation 辅助函数与 quick-renew-dialog.tsx 中的函数重复。为了提高代码的可维护性并遵循 DRY (Don't Repeat Yourself) 原则,建议将此函数提取到一个共享的工具文件中,例如 src/lib/utils/translations.ts

@ding113 ding113 merged commit 1d06d22 into dev Dec 23, 2025
22 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Dec 23, 2025
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 adds useful quick toggle and renewal functionality for users and keys with optimistic UI updates. However, there are significant i18n violations that need to be addressed before merging.

PR Size: L

  • Lines changed: 810 (705 additions + 105 deletions)
  • Files changed: 11

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
Standards 0 0 8 0

Medium Priority Issues (Should Fix)

1. [STANDARD-VIOLATION] Hardcoded Chinese strings in key-row-item.tsx toast messages

Location: src/app/[locale]/dashboard/_components/user/key-row-item.tsx (lines 179, 183, 190, 236, 239, 249)

Problem: The project uses next-intl for i18n with 5 locales (en, ja, ru, zh-CN, zh-TW). Hardcoded Chinese strings violate this standard.

Affected strings:

  • "操作失败" (Operation failed)
  • "密钥已启用" / "密钥已禁用" (Key enabled/disabled)
  • "续期成功" / "续期失败" (Renewal success/failed)

Suggested fix:

const tKey = useTranslations("dashboard.userManagement.keyToggle");
toast.success(checked ? tKey("enableSuccess") : tKey("disableSuccess"));
toast.error(res.error || tKey("toggleFailed"));

2. [STANDARD-VIOLATION] Hardcoded Chinese string with explicit acknowledgment

Location: src/app/[locale]/dashboard/_components/user/key-row-item.tsx (line 274)

Problem: The comment explicitly states // 直接硬编码,因为原文案是"同时启用用户" acknowledging hardcoding.

Suggested fix:

// Add to translations interface and messages files:
enableOnRenew: tKeyRenew("enableKeyOnRenew"),

3. [STANDARD-VIOLATION] Hardcoded Chinese in Tooltip/aria-label

Location: src/app/[locale]/dashboard/_components/user/key-row-item.tsx (lines 302, 320, 326)

Affected strings:

  • title="点击快捷续期" (Click for quick renew)
  • aria-label="切换密钥启用状态" (Toggle key enabled status)
  • "点击禁用密钥" / "点击启用密钥" (Click to disable/enable key)

Suggested fix:

const tKeyToggle = useTranslations("dashboard.userManagement.keyToggle");
title={tKeyToggle("quickRenewTooltip")}
aria-label={tKeyToggle("toggleAriaLabel")}
<TooltipContent>
  {localStatus === "enabled" ? tKeyToggle("disableTooltip") : tKeyToggle("enableTooltip")}
</TooltipContent>

4. [STANDARD-VIOLATION] Hardcoded Chinese in user-key-table-row.tsx

Location: src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx (lines 163, 167, 174, 218, 223)

Affected strings:

  • "操作失败" (Operation failed)
  • "用户已启用" / "用户已禁用" (User enabled/disabled)
  • aria-label="切换用户启用状态" (Toggle user enabled status)
  • "点击禁用用户" / "点击启用用户" (Click to disable/enable user)

Suggested fix: Same pattern as above - use useTranslations and add keys to all locale files.


5. [STANDARD-VIOLATION] Hardcoded Chinese labels in quick-renew-key-dialog.tsx

Location: src/app/[locale]/dashboard/_components/user/forms/quick-renew-key-dialog.tsx (lines 183, 185, 246, 248)

Affected strings:

  • "快捷延期" (Quick extend)
  • "在当前过期时间基础上延长(如已过期则从现在开始计算)" (Extend from current expiry...)
  • "设置到期日期" (Set expiry date)
  • "直接指定具体的到期日期" (Directly specify expiry date)

Suggested fix: Add these to the translations prop interface and pass from parent:

interface QuickRenewKeyDialogProps {
  translations: {
    // ... existing ...
    quickExtend: string;
    quickExtendDescription: string;
    setExpiryDate: string;
    setExpiryDateDescription: string;
  };
}

6. [STANDARD-VIOLATION] Hardcoded Chinese labels in quick-renew-dialog.tsx

Location: src/app/[locale]/dashboard/_components/user/forms/quick-renew-dialog.tsx (lines 189, 191, 243, 245)

Problem: Same issue as quick-renew-key-dialog.tsx - hardcoded Chinese labels in the user quick renew dialog.

Suggested fix: Same pattern - extend translations interface and use translation functions.


Recommendation

All 8 issues are i18n violations of the same type. Consider:

  1. Creating a new translation namespace dashboard.userManagement.toggle for toggle-related strings
  2. Adding all missing translation keys to all 5 locale files (en, ja, ru, zh-CN, zh-TW)
  3. Replacing all hardcoded strings with translation function calls

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean (errors are properly caught and user feedback provided)
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - No new tests added (manual testing documented)
  • Code clarity - Good (optimistic update pattern is well-implemented)

Automated review by Claude AI

updateData.isEnabled = enableKey;
}

const res = await editKey(keyData.id, updateData);
Copy link
Contributor

Choose a reason for hiding this comment

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

[CRITICAL] [LOGIC-BUG] editKey() quick renew can overwrite key settings

Why this is a problem: src/app/[locale]/dashboard/_components/user/key-row-item.tsx:229 calls:
const res = await editKey(keyData.id, updateData);
but src/actions/keys.ts:362 parses the payload with KeyFormSchema.parse(data) and then writes many fields back in updateKey(...) (src/actions/keys.ts:445), including fields that default when omitted (src/lib/validation/schemas.ts:274, src/lib/validation/schemas.ts:288, src/lib/validation/schemas.ts:293, src/lib/validation/schemas.ts:312, src/lib/validation/schemas.ts:320). Because updateData only includes name + expiresAt (+ optional isEnabled), quick renew can reset existing settings like canLoginWebUi, dailyResetMode/time, limitConcurrentSessions, cacheTtlPreference, and (for admins) providerGroup.

Suggested fix: Avoid editKey() for quick renew; add a dedicated server action that only patches expires_at and optional is_enabled, then call it here.

// src/actions/keys.ts
export async function renewKeyExpiresAt(
  keyId: number,
  data: { expiresAt: string; enableKey?: boolean }
): Promise<ActionResult> {
  const tError = await getTranslations("errors");
  const session = await getSession();
  if (!session) {
    return { ok: false, error: tError("UNAUTHORIZED"), errorCode: ERROR_CODES.UNAUTHORIZED };
  }

  const key = await findKeyById(keyId);
  if (!key) {
    return { ok: false, error: tError("KEY_NOT_FOUND"), errorCode: ERROR_CODES.NOT_FOUND };
  }

  if (session.user.role !== "admin" && session.user.id !== key.userId) {
    return { ok: false, error: tError("PERMISSION_DENIED"), errorCode: ERROR_CODES.PERMISSION_DENIED };
  }

  const expiresAt = new Date(data.expiresAt);
  if (Number.isNaN(expiresAt.getTime())) {
    return { ok: false, error: tError("INVALID_FORMAT"), errorCode: ERROR_CODES.INVALID_FORMAT };
  }

  await updateKey(keyId, {
    expires_at: expiresAt,
    ...(data.enableKey === true ? { is_enabled: true } : {}),
  });

  revalidatePath("/dashboard/users");
  revalidatePath("/dashboard");
  return { ok: true };
}

// src/app/[locale]/dashboard/_components/user/key-row-item.tsx
const res = await renewKeyExpiresAt(keyData.id, { expiresAt: expiresAt.toISOString(), enableKey });

if (!keyData || !customDate) return;
setIsSubmitting(true);
try {
const newDate = new Date(customDate);
Copy link
Contributor

Choose a reason for hiding this comment

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

[HIGH] [LOGIC-BUG] new Date(customDate) can shift the selected expiry date and doesn't normalize to end-of-day

Why this is a problem: src/app/[locale]/dashboard/_components/user/forms/quick-renew-key-dialog.tsx:128 uses:
const newDate = new Date(customDate);
customDate is a YYYY-MM-DD string from DatePickerField, and new Date("YYYY-MM-DD") is parsed as UTC midnight, which can be off by one day depending on server/client timezone. It also doesn't apply the existing “end of day” convention used elsewhere (e.g. src/app/[locale]/dashboard/_components/user/forms/key-edit-section.tsx:94-105).

Suggested fix: Parse the date string in local time and set it to end-of-day (and do the same for quick-select renewals when the key is already expired).

const parseEndOfDay = (value: string): Date | null => {
  const [year, month, day] = value.split("-").map(Number);
  if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) return null;
  const d = new Date(year, month - 1, day);
  d.setHours(23, 59, 59, 999);
  return d;
};

// handleCustomDateSubmit
const newDate = parseEndOfDay(customDate);
if (!newDate) return;

// handleQuickSelect
const newDate = addDays(baseDate, days);
newDate.setHours(23, 59, 59, 999);


// 当父组件传入乐观更新的过期时间时,立即更新本地状态
useEffect(() => {
if (optimisticExpiresAt !== undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

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

[HIGH] [LOGIC-BUG] User expiry optimistic rollback leaves stale localExpiresAt after failures

Why this is a problem: When renew fails, the parent removes the optimistic value (src/app/[locale]/dashboard/_components/user/user-management-table.tsx:335-339), so optimisticExpiresAt transitions from Dateundefined. But src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx:124-128 only updates localExpiresAt when optimisticExpiresAt !== undefined, so the row keeps displaying the stale optimistic date even though the renew failed.

Suggested fix: Always derive the displayed expiry from optimisticExpiresAt ?? user.expiresAt so rollback restores the server value.

useEffect(() => {
  setLocalExpiresAt(optimisticExpiresAt ?? user.expiresAt);
}, [optimisticExpiresAt, user.expiresAt, user.id]);

// (then remove the separate effect that only mirrors user.expiresAt)

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 introduces quick toggle/renew UX for users/keys in the dashboard. Key quick-renew currently routes through editKey() with a partial payload that can unintentionally reset key settings; there are also date-handling and optimistic UI rollback issues that can surface incorrect expiry dates.

PR Size: L

  • Lines changed: 810
  • Files changed: 11
  • Split suggestions:
    • PR 1: server actions (key renew action + revalidation)
    • PR 2: dashboard UI (switches/dialogs/optimistic state)
    • PR 3: i18n message updates across locales

Issues Found

Category Critical High Medium Low
Logic/Bugs 1 3 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

Critical Issues (Must Fix)

  • (Confidence 100) src/app/[locale]/dashboard/_components/user/key-row-item.tsx:229: calling editKey() for quick renew can overwrite key settings due to schema defaults.
  • (Confidence 95) src/app/[locale]/dashboard/_components/user/forms/quick-renew-key-dialog.tsx:128: new Date(customDate) + lack of end-of-day normalization can shift expiry date/time.

High Priority Issues (Should Fix)

  • (Confidence 90) src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx:124: optimistic expiry rollback leaves stale localExpiresAt after failures.
  • (Confidence 90) src/app/[locale]/dashboard/_components/user/forms/quick-renew-dialog.tsx:183: hardcoded Chinese strings break non-zh-CN locales (similar patterns in new key/user toggle UI).

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 #414 and applied label size/L.
  • Posted 4 inline review comments (1 Critical, 3 High) with concrete fix suggestions.
  • Submitted the required “Code Review Summary” via gh pr review --comment on PR #414.

@ding113 ding113 deleted the feat/user-key-quick-toggle-and-renew branch December 23, 2025 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:i18n area:UI enhancement New feature or request size/L Large PR (< 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants