feat: add quick toggle and renew for users and keys#414
Conversation
- 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
Summary of ChangesHello @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
Using Gemini Code AssistThe 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
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 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
|
🧪 测试结果
总体结果: ✅ 所有测试通过 |
There was a problem hiding this comment.
Code Review
这次的 Pull Request 质量很高,为用户和密钥管理页面增加了快捷启用/禁用开关和续期功能,并采用了乐观更新策略,极大地提升了用户体验。代码结构清晰,逻辑严谨。
我主要提出了一些关于国际化(i18n)的改进建议,以确保新添加的 UI 文本和错误信息能够支持多语言。具体来说:
- 在几个组件中发现了一些硬编码的中文字符串,建议将它们移至
messages翻译文件中。 - 发现了一个重复的辅助函数,建议提取以提高代码复用性。
整体而言,这是一次出色的功能增强。在解决上述小问题后,代码将更加完善。
src/app/[locale]/dashboard/_components/user/forms/quick-renew-dialog.tsx
Show resolved
Hide resolved
src/app/[locale]/dashboard/_components/user/forms/quick-renew-dialog.tsx
Show resolved
Hide resolved
src/app/[locale]/dashboard/_components/user/forms/quick-renew-key-dialog.tsx
Show resolved
Hide resolved
src/app/[locale]/dashboard/_components/user/forms/quick-renew-key-dialog.tsx
Show resolved
Hide resolved
| 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; | ||
| } |
There was a problem hiding this comment.
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:
- Creating a new translation namespace
dashboard.userManagement.togglefor toggle-related strings - Adding all missing translation keys to all 5 locale files (
en,ja,ru,zh-CN,zh-TW) - 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); |
There was a problem hiding this comment.
[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); |
There was a problem hiding this comment.
[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) { |
There was a problem hiding this comment.
[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 Date → undefined. 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)
src/app/[locale]/dashboard/_components/user/forms/quick-renew-dialog.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
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: callingeditKey()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 stalelocalExpiresAtafter failures. - (Confidence 90)
src/app/[locale]/dashboard/_components/user/forms/quick-renew-dialog.tsx:183: hardcoded Chinese strings break non-zh-CNlocales (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
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:
Related Issues:
Builds on:
Solution
Implemented inline quick actions with optimistic updates:
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
Testing
Manual Testing
Checklist
Description enhanced by Claude AI