feat: revue de code, doc CODE_REVIEW, forfaits 2026, traduction LLM, providers avec modèle
Made-with: Cursor
This commit is contained in:
217
frontend/src/app/dashboard/api-keys/page.tsx
Normal file
217
frontend/src/app/dashboard/api-keys/page.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Zap, Plus, AlertCircle } from 'lucide-react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { useUser } from '@/app/dashboard/useUser';
|
||||
import { useApiKeys } from './useApiKeys';
|
||||
import { MAX_API_KEYS, type ApiKey } from './types';
|
||||
import { ProUpgradePrompt } from './ProUpgradePrompt';
|
||||
import { ApiKeyTable } from './ApiKeyTable';
|
||||
import { GenerateKeyDialog } from './GenerateKeyDialog';
|
||||
import { RevokeKeyDialog } from './RevokeKeyDialog';
|
||||
import { WebhookSnippet } from './WebhookSnippet';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
|
||||
export default function ApiKeysPage() {
|
||||
const { data: user, isLoading: isLoadingUser } = useUser();
|
||||
const {
|
||||
keys,
|
||||
total,
|
||||
isLoading: isLoadingKeys,
|
||||
isGenerating,
|
||||
isRevoking,
|
||||
generateKey,
|
||||
revokeKey,
|
||||
errorDetails,
|
||||
parseGenerateError,
|
||||
parseRevokeError,
|
||||
} = useApiKeys();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [generateDialogOpen, setGenerateDialogOpen] = useState(false);
|
||||
const [revokeDialogOpen, setRevokeDialogOpen] = useState(false);
|
||||
const [keyToRevoke, setKeyToRevoke] = useState<{ id: string; name: string } | null>(null);
|
||||
const [apiError, setApiError] = useState<string | null>(null);
|
||||
|
||||
const isPro = user?.tier === 'pro';
|
||||
const maxKeysReached = total >= MAX_API_KEYS;
|
||||
const isLoading = isLoadingUser || isLoadingKeys;
|
||||
|
||||
// Handle API errors with specific error codes
|
||||
useEffect(() => {
|
||||
if (errorDetails?.code === 'PRO_FEATURE_REQUIRED') {
|
||||
// Redirect to upgrade prompt will happen via isPro check
|
||||
setApiError(null);
|
||||
} else if (errorDetails?.code === 'API_KEY_LIMIT_REACHED') {
|
||||
setApiError('You have reached the maximum of 10 API keys. Revoke an existing key to generate a new one.');
|
||||
} else if (errorDetails) {
|
||||
setApiError(errorDetails.message);
|
||||
} else {
|
||||
setApiError(null);
|
||||
}
|
||||
}, [errorDetails]);
|
||||
|
||||
const handleRevokeClick = (key: ApiKey) => {
|
||||
setKeyToRevoke({ id: key.id, name: key.name });
|
||||
setRevokeDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleRevokeConfirm = async () => {
|
||||
if (!keyToRevoke) return;
|
||||
try {
|
||||
await revokeKey(keyToRevoke.id);
|
||||
setRevokeDialogOpen(false);
|
||||
setKeyToRevoke(null);
|
||||
toast({
|
||||
title: 'Key revoked',
|
||||
description: 'The API key has been revoked successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
const revokeError = parseRevokeError();
|
||||
if (revokeError?.code === 'API_KEY_NOT_FOUND') {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Key Not Found',
|
||||
description: 'The API key no longer exists. It may have already been revoked.',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Error',
|
||||
description: revokeError?.message || 'Failed to revoke the API key. Please try again.',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateKey = async (name?: string) => {
|
||||
try {
|
||||
const result = await generateKey(name);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const genError = parseGenerateError();
|
||||
if (genError?.code === 'API_KEY_LIMIT_REACHED') {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Limit Reached',
|
||||
description: 'You have reached the maximum of 10 API keys. Revoke an existing key to generate a new one.',
|
||||
});
|
||||
} else if (genError?.code === 'PRO_FEATURE_REQUIRED') {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Pro Feature Required',
|
||||
description: 'API keys are a Pro feature. Please upgrade your account.',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Error',
|
||||
description: genError?.message || 'Failed to generate API key. Please try again.',
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-4 border-muted border-t-foreground mx-auto"></div>
|
||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isPro) {
|
||||
return <ProUpgradePrompt />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">API Keys</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Manage your API keys for programmatic access to the translation API.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{apiError && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{apiError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex size-8 items-center justify-center rounded-lg bg-accent/10">
|
||||
<Zap className="size-4 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base">API & Automation</CardTitle>
|
||||
<CardDescription>Generate and manage your API keys for automation workflows</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium">
|
||||
{total} of {MAX_API_KEYS} keys used
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{maxKeysReached ? (
|
||||
<span className="text-amber-600">Maximum keys reached. Revoke a key to generate a new one.</span>
|
||||
) : (
|
||||
`You can generate ${MAX_API_KEYS - total} more key${MAX_API_KEYS - total !== 1 ? 's' : ''}.`
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setGenerateDialogOpen(true)}
|
||||
disabled={maxKeysReached || isGenerating}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
Generate New Key
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ApiKeyTable
|
||||
keys={keys}
|
||||
onRevoke={handleRevokeClick}
|
||||
isRevoking={isRevoking}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Separator />
|
||||
|
||||
<WebhookSnippet />
|
||||
|
||||
<GenerateKeyDialog
|
||||
open={generateDialogOpen}
|
||||
onOpenChange={setGenerateDialogOpen}
|
||||
onGenerate={handleGenerateKey}
|
||||
isGenerating={isGenerating}
|
||||
maxKeysReached={maxKeysReached}
|
||||
/>
|
||||
|
||||
<RevokeKeyDialog
|
||||
open={revokeDialogOpen}
|
||||
onOpenChange={setRevokeDialogOpen}
|
||||
onConfirm={handleRevokeConfirm}
|
||||
isRevoking={isRevoking}
|
||||
keyName={keyToRevoke?.name}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user