Skip to content
Merged
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
216 changes: 148 additions & 68 deletions web/src/features/profile/components/dialogs/access-token-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,21 @@ 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 { KeyRound, Loader2, RefreshCw } from 'lucide-react'
import { 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'
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'

Expand All @@ -42,77 +50,149 @@ export function AccessTokenDialog({
onOpenChange,
}: AccessTokenDialogProps) {
const { t } = useTranslation()
const { token, generating, generate } = useAccessToken()
const { token, generating, generate, clearToken } = useAccessToken()
const [confirmOpen, setConfirmOpen] = useState(false)

// Auto-generate token when dialog opens if no token exists
useEffect(() => {
if (open && !token) {
generate()
const handleOpenChange = (nextOpen: boolean) => {
if (generating) return

if (!nextOpen) {
setConfirmOpen(false)
clearToken()
}
onOpenChange(nextOpen)
}

const handleGenerate = async () => {
if (await generate()) {
setConfirmOpen(false)
}
}, [open, token, 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={handleOpenChange}
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={() => handleOpenChange(false)}
disabled={generating}
>
{t('Close')}
</Button>
<Button
type='button'
onClick={() => setConfirmOpen(true)}
disabled={generating}
className='gap-2'
>
{generating ? (
<Loader2 className='h-4 w-4 animate-spin' aria-hidden='true' />
) : (
<RefreshCw className='h-4 w-4' aria-hidden='true' />
)}
{generating ? t('Generating...') : t('Regenerate')}
</Button>
</>
}
>
<div className='my-6'>
{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(
"Save this token now. You won't be able to view it again after closing this dialog."
)}
</p>
</div>
) : (
<Empty className='border py-8'>
<EmptyHeader>
<EmptyMedia variant='icon'>
<KeyRound aria-hidden='true' />
</EmptyMedia>
<EmptyTitle>
{t('Access tokens are shown only once')}
</EmptyTitle>
<EmptyDescription>
{t(
'For security, existing access tokens cannot be displayed. Regenerate only when you need a new token.'
)}
</EmptyDescription>
<EmptyDescription>
{t(
'Regenerating immediately invalidates any existing token.'
)}
</EmptyDescription>
</EmptyHeader>
</Empty>
)}
</div>
</div>
</Dialog>
</Dialog>

<ConfirmDialog
open={confirmOpen}
onOpenChange={(nextOpen) => {
if (!generating) setConfirmOpen(nextOpen)
}}
title={t('Regenerate access token?')}
desc={
<div className='space-y-2'>
<p>
{t(
'This will immediately invalidate your existing access token. Any applications or scripts using it will stop working.'
)}
</p>
<p>
{t(
'The new token will only be shown once. Copy it and store it securely.'
)}
</p>
</div>
}
confirmText={
generating ? (
<>
<Loader2 className='h-4 w-4 animate-spin' aria-hidden='true' />
{t('Generating...')}
</>
) : (
t('Regenerate token')
)
}
destructive
isLoading={generating}
handleConfirm={handleGenerate}
/>
</>
)
}
5 changes: 5 additions & 0 deletions web/src/features/profile/hooks/use-access-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,14 @@ export function useAccessToken() {
}
}, [copyToClipboard])

const clearToken = useCallback(() => {
setToken('')
}, [])

return {
token,
generating,
generate,
clearToken,
}
}
8 changes: 8 additions & 0 deletions web/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
"Access Policy (JSON)": "Access Policy (JSON)",
"Access previous conversations and start new ones.": "Access previous conversations and start new ones.",
"Access Token": "Access Token",
"Access tokens are shown only once": "Access tokens are shown only once",
"AccessKey / SecretAccessKey": "AccessKey / SecretAccessKey",
"Account Binding Management": "Account Binding Management",
"Account Bindings": "Account Bindings",
Expand Down Expand Up @@ -2048,6 +2049,7 @@
"footer.new\u0061pi.projectAttributionSuffix": "All rights reserved. Designed and developed by the project contributors.",
"For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "For channels added after May 10, 2025, no need to remove \".\" from model names during deployment",
"For private deployments, format: https://fastgpt.run/api/openapi": "For private deployments, format: https://fastgpt.run/api/openapi",
"For security, existing access tokens cannot be displayed. Regenerate only when you need a new token.": "For security, existing access tokens cannot be displayed. Regenerate only when you need a new token.",
"Force a syntactically valid JSON response": "Force a syntactically valid JSON response",
"Force AUTH LOGIN": "Force AUTH LOGIN",
"Force Format": "Force Format",
Expand Down Expand Up @@ -3726,7 +3728,10 @@
"Refund": "Refund",
"Refund Details": "Refund Details",
"Regenerate": "Regenerate",
"Regenerate access token?": "Regenerate access token?",
"Regenerate Backup Codes": "Regenerate Backup Codes",
"Regenerate token": "Regenerate token",
"Regenerating immediately invalidates any existing token.": "Regenerating immediately invalidates any existing token.",
"Regex": "Regex",
"Regex Pattern": "Regex Pattern",
"Regex Replace": "Regex Replace",
Expand Down Expand Up @@ -4003,6 +4008,7 @@
"Save Stripe settings": "Save Stripe settings",
"Save these backup codes in a safe place. Each code can only be used once.": "Save these backup codes in a safe place. Each code can only be used once.",
"Save these codes in a safe place. Each code can only be used once.": "Save these codes in a safe place. Each code can only be used once.",
"Save this token now. You won't be able to view it again after closing this dialog.": "Save this token now. You won't be able to view it again after closing this dialog.",
"Save token limits": "Save token limits",
"Save tool prices": "Save tool prices",
"Save Waffo Pancake settings": "Save Waffo Pancake settings",
Expand Down Expand Up @@ -4528,6 +4534,7 @@
"The model that was requested": "The model that was requested",
"The model you're looking for doesn't exist.": "The model you're looking for doesn't exist.",
"The name displayed across the application": "The name displayed across the application",
"The new token will only be shown once. Copy it and store it securely.": "The new token will only be shown once. Copy it and store it securely.",
"The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations": "The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations",
"The requested chat preset does not exist or has been removed.": "The requested chat preset does not exist or has been removed.",
"The reset request stays disabled until a credit is available.": "The reset request stays disabled until a credit is available.",
Expand Down Expand Up @@ -4609,6 +4616,7 @@
"This will delete all channel affinity cache entries still in memory.": "This will delete all channel affinity cache entries still in memory.",
"This will delete temporary cache files that have not been used for more than 10 minutes": "This will delete temporary cache files that have not been used for more than 10 minutes",
"This will extend the deployment by the specified hours.": "This will extend the deployment by the specified hours.",
"This will immediately invalidate your existing access token. Any applications or scripts using it will stop working.": "This will immediately invalidate your existing access token. Any applications or scripts using it will stop working.",
"This will permanently delete all manually and automatically disabled channels. This action cannot be undone.": "This will permanently delete all manually and automatically disabled channels. This action cannot be undone.",
"This will permanently delete API key": "This will permanently delete API key",
"This will permanently delete redemption code": "This will permanently delete redemption code",
Expand Down
8 changes: 8 additions & 0 deletions web/src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
"Access Policy (JSON)": "Politique d'accès (JSON)",
"Access previous conversations and start new ones.": "Accéder aux conversations précédentes et en démarrer de nouvelles.",
"Access Token": "Jeton d'accès",
"Access tokens are shown only once": "Les jetons d'accès ne sont affichés qu'une seule fois",
"AccessKey / SecretAccessKey": "AccessKey / SecretAccessKey",
"Account Binding Management": "Gestion des liaisons de compte",
"Account Bindings": "Associations de compte",
Expand Down Expand Up @@ -2048,6 +2049,7 @@
"footer.new\u0061pi.projectAttributionSuffix": "Tous droits réservés. Conçu et développé par les contributeurs du projet.",
"For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "Pour les canaux ajoutés après le 10 mai 2025, pas besoin de supprimer \".\" des noms de modèles lors du déploiement",
"For private deployments, format: https://fastgpt.run/api/openapi": "Pour les déploiements privés, format : https://fastgpt.run/api/openapi",
"For security, existing access tokens cannot be displayed. Regenerate only when you need a new token.": "Pour des raisons de sécurité, les jetons d'accès existants ne peuvent pas être réaffichés. Ne les régénérez que si vous en avez besoin d'un nouveau.",
"Force a syntactically valid JSON response": "Imposer une réponse JSON syntaxiquement valide",
"Force AUTH LOGIN": "Forcer AUTH LOGIN",
"Force Format": "Forcer le format",
Expand Down Expand Up @@ -3726,7 +3728,10 @@
"Refund": "Remboursement",
"Refund Details": "Détails du remboursement",
"Regenerate": "Régénérer",
"Regenerate access token?": "Régénérer le jeton d'accès ?",
"Regenerate Backup Codes": "Régénérer les codes de secours",
"Regenerate token": "Régénérer le jeton",
"Regenerating immediately invalidates any existing token.": "La régénération invalide immédiatement tout jeton existant.",
"Regex": "Regex",
"Regex Pattern": "Expression régulière",
"Regex Replace": "Remplacement regex",
Expand Down Expand Up @@ -4003,6 +4008,7 @@
"Save Stripe settings": "Enregistrer les paramètres Stripe",
"Save these backup codes in a safe place. Each code can only be used once.": "Enregistrez ces codes de secours dans un endroit sûr. Chaque code ne peut être utilisé qu'une seule fois.",
"Save these codes in a safe place. Each code can only be used once.": "Enregistrez ces codes dans un endroit sûr. Chaque code ne peut être utilisé qu'une seule fois.",
"Save this token now. You won't be able to view it again after closing this dialog.": "Enregistrez ce jeton maintenant. Vous ne pourrez plus le consulter après la fermeture de cette boîte de dialogue.",
"Save token limits": "Enregistrer les limites de jetons",
"Save tool prices": "Enregistrer les prix des outils",
"Save Waffo Pancake settings": "Enregistrer les paramètres Waffo Pancake",
Expand Down Expand Up @@ -4528,6 +4534,7 @@
"The model that was requested": "Le modèle qui a été demandé",
"The model you're looking for doesn't exist.": "Le modèle que vous recherchez n'existe pas.",
"The name displayed across the application": "Le nom affiché dans l'application",
"The new token will only be shown once. Copy it and store it securely.": "Le nouveau jeton ne sera affiché qu’une seule fois. Copiez-le et conservez-le en lieu sûr.",
"The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations": "L'URL publique de votre serveur, utilisée pour les rappels OAuth, les webhooks et autres intégrations externes",
"The requested chat preset does not exist or has been removed.": "Le préréglage de discussion demandé n'existe pas ou a été supprimé.",
"The reset request stays disabled until a credit is available.": "La demande de réinitialisation reste désactivée tant qu’aucun crédit n’est disponible.",
Expand Down Expand Up @@ -4609,6 +4616,7 @@
"This will delete all channel affinity cache entries still in memory.": "Cela supprimera toutes les entrées de cache d'affinité de canal encore en mémoire.",
"This will delete temporary cache files that have not been used for more than 10 minutes": "Cela supprimera les fichiers de cache temporaires inutilisés depuis plus de 10 minutes",
"This will extend the deployment by the specified hours.": "Cela prolongera le déploiement du nombre d'heures spécifié.",
"This will immediately invalidate your existing access token. Any applications or scripts using it will stop working.": "Cela invalidera immédiatement votre jeton d'accès actuel. Les applications ou scripts qui l'utilisent cesseront de fonctionner.",
"This will permanently delete all manually and automatically disabled channels. This action cannot be undone.": "Cela supprimera définitivement tous les canaux désactivés manuellement et automatiquement. Cette action ne peut pas être annulée.",
"This will permanently delete API key": "Cela supprimera définitivement la clé API",
"This will permanently delete redemption code": "Cela supprimera définitivement le code d'échange",
Expand Down
Loading
Loading