Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 105 additions & 66 deletions web/src/features/profile/components/dialogs/access-token-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.

For commercial licensing, please contact support@quantumnous.com
*/
import { RefreshCw, Loader2 } from 'lucide-react'
import { useEffect } from 'react'
import { RefreshCw, Loader2, KeyRound } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'

import { ConfirmDialog } from '@/components/confirm-dialog'
import { CopyButton } from '@/components/copy-button'
import { Dialog } from '@/components/dialog'
import { Button } from '@/components/ui/button'
Expand All @@ -43,76 +44,114 @@ export function AccessTokenDialog({
}: AccessTokenDialogProps) {
const { t } = useTranslation()
const { token, generating, generate } = useAccessToken()
const [confirmOpen, setConfirmOpen] = useState(false)

// Auto-generate token when dialog opens if no token exists
// Regenerating invalidates the previous token server-side and cannot be
// undone, so it must never happen as a side effect of opening this dialog.
// The confirmation state is reset on close so a reopened dialog starts idle.
useEffect(() => {
if (open && !token) {
generate()
if (!open) {
setConfirmOpen(false)
}
}, [open, token, generate])
}, [open])

const handleConfirm = async () => {
setConfirmOpen(false)
await generate()
}

return (
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Access Token')}
description={t(
"Your system access token for API authentication. Keep it secure and don't share it with others."
)}
contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
>
{t('Close')}
</Button>
<Button
type='button'
onClick={generate}
disabled={generating}
className='gap-2'
>
{generating ? (
<Loader2 className='h-4 w-4 animate-spin' />
) : (
<RefreshCw className='h-4 w-4' />
)}
{generating ? t('Generating...') : t('Regenerate')}
</Button>
</>
}
>
<div className='my-6 space-y-4'>
<div className='space-y-2'>
<Label htmlFor='token'>{t('Token')}</Label>
<div className='flex gap-2'>
<Input
id='token'
type='text'
value={token}
readOnly
className='font-mono text-xs'
placeholder={t('Click "Generate" to create a token')}
/>
<CopyButton
value={token}
<>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Access Token')}
description={t(
"Your system access token for API authentication. Keep it secure and don't share it with others."
)}
contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
className='size-9'
iconClassName='size-4'
tooltip={t('Copy token')}
aria-label={t('Copy token')}
/>
</div>
<p className='text-muted-foreground text-xs'>
{t('Use this token for API authentication')}
</p>
onClick={() => onOpenChange(false)}
>
{t('Close')}
</Button>
<Button
type='button'
variant={token ? 'default' : 'destructive'}
onClick={() => setConfirmOpen(true)}
disabled={generating}
className='gap-2'
>
{generating ? (
<Loader2 className='h-4 w-4 animate-spin' />
) : (
<RefreshCw className='h-4 w-4' />
)}
{generating ? t('Generating...') : t('Regenerate')}
</Button>
Comment on lines +84 to +97

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
fd -a 'access-token-dialog\.tsx|accessToken|useAccessToken' . | sed 's#^\./##' | head -200

echo
echo "Target file excerpt:"
cat -n web/src/features/profile/components/dialogs/access-token-dialog.tsx | sed -n '1,220p'

Repository: QuantumNous/new-api

Length of output: 6698


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Search access token usages and state:"
rg -n "useAccessToken|accessToken|accessTokenMutation|token" web/src -g '*.ts' -g '*.tsx' | head -300

Repository: QuantumNous/new-api

Length of output: 27609


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "use-access-token hook:"
cat -n web/src/features/profile/hooks/use-access-token.ts | sed -n '1,120p'

echo
echo "profile api token generation:"
cat -n web/src/features/profile/api.ts | sed -n '1,145p'

echo
echo "ConfirmDialog props/usages:"
fd -a 'confirm-dialog\.(tsx|ts)$' web/src | xargs -r cat -n | sed -n '1,220p'
rg -n "ConfirmDialog|destructive" web/src/components web/src/features -g '*.tsx' -g '*.ts' | head -120

echo
echo "profile context excerpt:"
cat -n web/src/features/profile/components/profile-security-card.tsx | sed -n '1,180p'

Repository: QuantumNous/new-api

Length of output: 45372


🌐 Web query:

i18next conditional pluralization ifelse zero one example

💡 Result:

In i18next, pluralization is handled via suffix-based naming conventions rather than explicit if-else logic [1][2]. The framework uses the count variable to determine which suffix to select based on the language's specific plural rules (powered by Intl.PluralRules) [1][3]. To handle specific cases like zero, one, and others, you define keys in your resource file with the appropriate suffixes [1]. Resource Example: { "key_zero": "No items", "key_one": "{{count}} item", "key_other": "{{count}} items" } Usage: i18next.t('key', { count: 0 }); // -> "No items" i18next.t('key', { count: 1 }); // -> "1 item" i18next.t('key', { count: 5 }); // -> "5 items" Key Concepts: - Suffix Resolution: i18next automatically appends suffixes like _zero, _one, or _other to your base key [1][4]. If a _zero entry is provided, i18next will prioritize it when count is 0, overriding the language's default plural rules [1][2]. - Intl.PluralRules: i18next relies on the native Intl.PluralRules API [1]. In environments without this support (e.g., some legacy environments or specific versions of React Native), you must use a polyfill [1][3]. - ICU Message Format: If you require more complex conditional logic (e.g., specific messages for non-plural numbers like 42 or 13), you can use ICU Message Format within your translation strings [2][5]. This allows for syntax like {count, plural, =0 {None} =42 {Answer} one {One} other {# items}} [2][5]. For simple pluralization, stick to the _one/_other or _zero/_one/_other suffix pattern as it is the standard, optimized approach for i18next [1][4].

Citations:


Fix the access token regenerate UI state and confirmation copy.

useAccessToken() starts with token === '' and never tracks whether a token was previously created, so the empty state is ambiguous. This causes the regenerate button to use the non-destructive 'default' variant when a token is present and the destructive 'destructive' variant at the first-generation state.

Also, useAccessToken() cannot distinguish “no token was ever created” from “a token exists but is hidden”, so the confirmation and empty-state copy should not claim that regenerating invalidates a current token. If token remains the only flag, use the absence of a token for the default confirm/empty messaging, and use destruction only when a token is known to exist or the backend reports an existing token.

🤖 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 `@web/src/features/profile/components/dialogs/access-token-dialog.tsx` around
lines 84 - 97, Update the access-token state handling around useAccessToken and
the regenerate Button so the first-generation empty state uses the destructive
variant, while a known existing token uses the default variant. Base
confirmation and empty-state messaging on whether a token is present or the
backend reports one exists, avoiding claims that regeneration invalidates a
current token when no token is known.

</>
}
>
<div className='my-6 space-y-4'>
{token ? (
<div className='space-y-2'>
<Label htmlFor='token'>{t('Token')}</Label>
<div className='flex gap-2'>
<Input
id='token'
type='text'
value={token}
readOnly
className='font-mono text-xs'
/>
<CopyButton
value={token}
variant='outline'
className='size-9'
iconClassName='size-4'
tooltip={t('Copy token')}
aria-label={t('Copy token')}
/>
</div>
<p className='text-muted-foreground text-xs'>
{t(
'This token is shown only once. Store it now — it cannot be retrieved later.'
)}
</p>
</div>
) : (
<div className='flex flex-col items-center gap-3 py-4 text-center'>
<div className='bg-muted flex size-10 items-center justify-center rounded-full'>
<KeyRound className='text-muted-foreground size-5' />
</div>
<p className='text-muted-foreground text-sm'>
{t(
'For security reasons the existing token cannot be displayed again. Regenerating creates a new token and invalidates the current one immediately.'
)}
</p>
</div>
)}
</div>
</div>
</Dialog>
</Dialog>

<ConfirmDialog
open={confirmOpen}
onOpenChange={setConfirmOpen}
destructive
isLoading={generating}
title={t('Regenerate access token?')}
desc={t(
'The current token stops working immediately. Any integration still using it will fail until you update it with the new token.'
)}
confirmText={t('Regenerate')}
handleConfirm={handleConfirm}
/>
</>
)
}
6 changes: 5 additions & 1 deletion web/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5248,6 +5248,10 @@
"Zero retention": "Zero retention",
"Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V4",
"Zoom": "Zoom"
"Zoom": "Zoom",
"Regenerate access token?": "Regenerate access token?",
"This token is shown only once. Store it now — it cannot be retrieved later.": "This token is shown only once. Store it now — it cannot be retrieved later.",
"For security reasons the existing token cannot be displayed again. Regenerating creates a new token and invalidates the current one immediately.": "For security reasons the existing token cannot be displayed again. Regenerating creates a new token and invalidates the current one immediately.",
"The current token stops working immediately. Any integration still using it will fail until you update it with the new token.": "The current token stops working immediately. Any integration still using it will fail until you update it with the new token."
}
}
6 changes: 5 additions & 1 deletion web/src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -5248,6 +5248,10 @@
"Zero retention": "Aucune rétention",
"Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V4",
"Zoom": "Zoom"
"Zoom": "Zoom",
"Regenerate access token?": "Régénérer le jeton d'accès ?",
"This token is shown only once. Store it now — it cannot be retrieved later.": "Ce jeton n'est affiché qu'une seule fois. Enregistrez-le maintenant, il ne pourra plus être récupéré ensuite.",
"For security reasons the existing token cannot be displayed again. Regenerating creates a new token and invalidates the current one immediately.": "Pour des raisons de sécurité, le jeton existant ne peut pas être affiché à nouveau. La régénération crée un nouveau jeton et invalide immédiatement l'actuel.",
"The current token stops working immediately. Any integration still using it will fail until you update it with the new token.": "Le jeton actuel cesse de fonctionner immédiatement. Toute intégration l'utilisant encore échouera jusqu'à ce que vous la mettiez à jour avec le nouveau jeton."
}
}
6 changes: 5 additions & 1 deletion web/src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -5248,6 +5248,10 @@
"Zero retention": "データ保持なし",
"Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V 4",
"Zoom": "ズーム"
"Zoom": "ズーム",
"Regenerate access token?": "アクセストークンを再生成しますか?",
"This token is shown only once. Store it now — it cannot be retrieved later.": "このトークンは一度しか表示されません。今すぐ保存してください。後から取得することはできません。",
"For security reasons the existing token cannot be displayed again. Regenerating creates a new token and invalidates the current one immediately.": "セキュリティ上の理由により、既存のトークンを再表示することはできません。再生成すると新しいトークンが作成され、現在のトークンは直ちに無効になります。",
"The current token stops working immediately. Any integration still using it will fail until you update it with the new token.": "現在のトークンは直ちに使用できなくなります。それを使用している連携は、新しいトークンに更新するまですべて失敗します。"
}
}
6 changes: 5 additions & 1 deletion web/src/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -5248,6 +5248,10 @@
"Zero retention": "Без хранения данных",
"Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V4",
"Zoom": "Zoom"
"Zoom": "Zoom",
"Regenerate access token?": "Перегенерировать токен доступа?",
"This token is shown only once. Store it now — it cannot be retrieved later.": "Этот токен отображается только один раз. Сохраните его сейчас — позже получить его будет невозможно.",
"For security reasons the existing token cannot be displayed again. Regenerating creates a new token and invalidates the current one immediately.": "В целях безопасности существующий токен нельзя показать повторно. Перегенерация создаёт новый токен и немедленно делает текущий недействительным.",
"The current token stops working immediately. Any integration still using it will fail until you update it with the new token.": "Текущий токен немедленно перестанет работать. Все интеграции, которые его используют, будут завершаться ошибкой, пока вы не обновите их новым токеном."
}
}
6 changes: 5 additions & 1 deletion web/src/i18n/locales/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -5248,6 +5248,10 @@
"Zero retention": "Không lưu dữ liệu",
"Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V4",
"Zoom": "Zoom"
"Zoom": "Zoom",
"Regenerate access token?": "Tạo lại token truy cập?",
"This token is shown only once. Store it now — it cannot be retrieved later.": "Token này chỉ hiển thị một lần. Hãy lưu lại ngay — sau này không thể xem lại được.",
"For security reasons the existing token cannot be displayed again. Regenerating creates a new token and invalidates the current one immediately.": "Vì lý do bảo mật, token hiện tại không thể hiển thị lại. Việc tạo lại sẽ sinh ra token mới và vô hiệu hóa ngay token hiện tại.",
"The current token stops working immediately. Any integration still using it will fail until you update it with the new token.": "Token hiện tại sẽ ngừng hoạt động ngay lập tức. Mọi tích hợp còn dùng nó sẽ lỗi cho đến khi bạn cập nhật sang token mới."
}
}
6 changes: 5 additions & 1 deletion web/src/i18n/locales/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -5248,6 +5248,10 @@
"Zero retention": "零數據保留",
"Zhipu": "智譜",
"Zhipu V4": "智譜 V4",
"Zoom": "縮放"
"Zoom": "縮放",
"Regenerate access token?": "重新生成存取令牌?",
"This token is shown only once. Store it now — it cannot be retrieved later.": "此令牌僅顯示一次,請立即儲存,之後無法再次查看。",
"For security reasons the existing token cannot be displayed again. Regenerating creates a new token and invalidates the current one immediately.": "出於安全考量,現有令牌無法再次顯示。重新生成會建立新令牌,並立即使目前的令牌失效。",
"The current token stops working immediately. Any integration still using it will fail until you update it with the new token.": "目前的令牌會立即停止運作。仍在使用它的整合將全部失敗,直到你更新為新令牌。"
}
}
6 changes: 5 additions & 1 deletion web/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -5248,6 +5248,10 @@
"Zero retention": "零数据保留",
"Zhipu": "智谱",
"Zhipu V4": "智谱 V4",
"Zoom": "缩放"
"Zoom": "缩放",
"Regenerate access token?": "重新生成访问令牌?",
"This token is shown only once. Store it now — it cannot be retrieved later.": "此令牌仅显示一次,请立即保存,之后无法再次查看。",
"For security reasons the existing token cannot be displayed again. Regenerating creates a new token and invalidates the current one immediately.": "出于安全考虑,现有令牌无法再次显示。重新生成会创建新令牌,并立即使当前令牌失效。",
"The current token stops working immediately. Any integration still using it will fail until you update it with the new token.": "当前令牌会立即停止工作。仍在使用它的集成将全部失败,直到你更新为新令牌。"
}
}
Loading