sanch1tx commited on
Commit
e16d70b
·
verified ·
1 Parent(s): 4686a1a

Update app/dashboard/api-keys/page.tsx

Browse files
Files changed (1) hide show
  1. app/dashboard/api-keys/page.tsx +489 -459
app/dashboard/api-keys/page.tsx CHANGED
@@ -1,459 +1,489 @@
1
- "use client";
2
-
3
- import React, { useState, useEffect } from 'react';
4
- import { useAuth } from '@/contexts/auth-context';
5
- import { Button } from '@/components/ui/button';
6
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
7
- import { Input } from '@/components/ui/input';
8
- import { Label } from '@/components/ui/label';
9
- import { Badge } from '@/components/ui/badge';
10
- import { Alert, AlertDescription } from '@/components/ui/alert';
11
- import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
12
- import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
13
- import { Progress } from '@/components/ui/progress';
14
- import { Loader2, Key, Plus, Trash2, Eye, EyeOff, Copy, Check, TrendingUp, Activity, Calendar } from 'lucide-react';
15
- import { ApiKey } from '@/lib/db/schema';
16
- import { toast } from 'sonner';
17
- import { useRouter } from 'next/navigation';
18
-
19
- export default function ApiKeysPage() {
20
- const { user, authLoading } = useAuth();
21
- const router = useRouter();
22
- const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
23
- const [userRequestsUsed, setUserRequestsUsed] = useState(0);
24
- const [userRequestsLimit, setUserRequestsLimit] = useState(1000);
25
- const [loading, setLoading] = useState(true);
26
- const [creating, setCreating] = useState(false);
27
- const [newKeyName, setNewKeyName] = useState('');
28
- const [isDialogOpen, setIsDialogOpen] = useState(false);
29
- const [error, setError] = useState('');
30
- const [visibleKeys, setVisibleKeys] = useState<Set<string>>(new Set());
31
- const [copiedKeyId, setCopiedKeyId] = useState<string | null>(null);
32
- const [isFirstTime, setIsFirstTime] = useState(false);
33
-
34
- const usagePercentage = Math.round((userRequestsUsed / userRequestsLimit) * 100) || 0;
35
- const remainingRequests = userRequestsLimit - userRequestsUsed;
36
-
37
- useEffect(() => {
38
- if (!authLoading && !user) {
39
- router.push("/login");
40
- return;
41
- }
42
- }, [user, authLoading, router]);
43
-
44
- useEffect(() => {
45
- if (user) {
46
- fetchApiKeys();
47
- }
48
- }, [user]);
49
-
50
- const fetchApiKeys = async () => {
51
- if (!user) return;
52
-
53
- try {
54
- setLoading(true);
55
- const response = await fetch(`/api/api-keys?userId=${user.uid}`);
56
- const data = await response.json();
57
-
58
- if (data.success) {
59
- setApiKeys(data.apiKeys || []);
60
- setUserRequestsUsed(Number(data.userRequestsUsed) || 0);
61
- setUserRequestsLimit(Number(data.userRequestsLimit) || 1000);
62
-
63
- // Check if this is first time (no API keys)
64
- setIsFirstTime(data.apiKeys?.length === 0);
65
- } else {
66
- setError(data.error || 'Failed to fetch API keys');
67
- }
68
- } catch (error) {
69
- console.error('Error fetching API keys:', error);
70
- setError('Failed to fetch API keys');
71
- } finally {
72
- setLoading(false);
73
- }
74
- };
75
-
76
- const createApiKey = async () => {
77
- if (!user || !newKeyName.trim()) return;
78
-
79
- try {
80
- setCreating(true);
81
- setError('');
82
-
83
- const response = await fetch('/api/api-keys', {
84
- method: 'POST',
85
- headers: { 'Content-Type': 'application/json' },
86
- body: JSON.stringify({
87
- userId: user.uid,
88
- keyName: newKeyName.trim(),
89
- }),
90
- });
91
-
92
- const data = await response.json();
93
-
94
- if (data.success) {
95
- setApiKeys([...apiKeys, data.apiKey]);
96
- setNewKeyName('');
97
- setIsDialogOpen(false);
98
- } else {
99
- setError(data.error || 'Failed to create API key');
100
- }
101
- } catch (error) {
102
- setError('Failed to create API key');
103
- } finally {
104
- setCreating(false);
105
- }
106
- };
107
-
108
- const deleteApiKey = async (keyId: string) => {
109
- if (!user) return;
110
-
111
- try {
112
- const response = await fetch(`/api/api-keys?userId=${user.uid}&keyId=${keyId}`, {
113
- method: 'DELETE',
114
- });
115
-
116
- const data = await response.json();
117
-
118
- if (data.success) {
119
- setApiKeys(apiKeys.filter(key => key.id !== keyId));
120
- } else {
121
- setError(data.error || 'Failed to delete API key');
122
- }
123
- } catch (error) {
124
- setError('Failed to delete API key');
125
- }
126
- };
127
-
128
- const toggleKeyVisibility = (keyId: string) => {
129
- const newVisibleKeys = new Set(visibleKeys);
130
- if (newVisibleKeys.has(keyId)) {
131
- newVisibleKeys.delete(keyId);
132
- } else {
133
- newVisibleKeys.add(keyId);
134
- }
135
- setVisibleKeys(newVisibleKeys);
136
- };
137
-
138
- const copyToClipboard = async (text: string, keyId: string) => {
139
- try {
140
- await navigator.clipboard.writeText(text);
141
- setCopiedKeyId(keyId);
142
- setTimeout(() => setCopiedKeyId(null), 2000);
143
-
144
- // Show success toast
145
- toast.success('API key copied successfully!', {
146
- description: 'The API key has been copied to your clipboard.',
147
- });
148
- } catch (error) {
149
- console.error('Failed to copy:', error);
150
-
151
- // Show error toast
152
- toast.error('Failed to copy API key', {
153
- description: 'Please try again or copy manually.',
154
- });
155
- }
156
- };
157
-
158
- const formatKeyValue = (keyValue: string, keyId: string) => {
159
- if (visibleKeys.has(keyId)) {
160
- return keyValue;
161
- }
162
- return `${keyValue.slice(0, 8)}${'*'.repeat(32)}${keyValue.slice(-8)}`;
163
- };
164
-
165
- if (!user) {
166
- return null;
167
- }
168
-
169
- return (
170
- <div className="flex flex-col min-h-screen">
171
- <div className="border-b">
172
- <div className="flex h-16 items-center px-4">
173
- <div className="flex items-center gap-3">
174
- <div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10">
175
- <Key className="w-4 h-4 text-primary" />
176
- </div>
177
- <div>
178
- <h1 className="text-lg font-semibold">API Keys</h1>
179
- <p className="text-sm text-muted-foreground">
180
- {isFirstTime ? 'Create your first API key to get started' : 'Manage your API access'}
181
- </p>
182
- </div>
183
- </div>
184
- </div>
185
- </div>
186
-
187
- <div className="flex flex-1 flex-col gap-6 p-6">
188
- {/* Show welcome card for first-time users */}
189
- {isFirstTime && (
190
- <Card className="border-dashed border-2 border-primary/20 bg-primary/5">
191
- <CardHeader>
192
- <CardTitle className="flex items-center gap-2">
193
- <Key className="w-5 h-5" />
194
- Welcome! Create Your First API Key
195
- </CardTitle>
196
- <CardDescription>
197
- API keys are required to access our scraping services. Create your first API key to start using our APIs for anime, movies, and other content.
198
- </CardDescription>
199
- </CardHeader>
200
- <CardContent>
201
- <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
202
- <DialogTrigger asChild>
203
- <Button size="lg">
204
- <Plus className="w-4 h-4 mr-2" />
205
- Create Your First API Key
206
- </Button>
207
- </DialogTrigger>
208
- <DialogContent>
209
- <DialogHeader>
210
- <DialogTitle>Create New API Key</DialogTitle>
211
- <DialogDescription>
212
- Give your API key a name to help you identify it later.
213
- </DialogDescription>
214
- </DialogHeader>
215
- <div className="grid gap-4 py-4">
216
- <div className="grid gap-2">
217
- <Label htmlFor="keyName">Key Name</Label>
218
- <Input
219
- id="keyName"
220
- value={newKeyName}
221
- onChange={(e) => setNewKeyName(e.target.value)}
222
- placeholder="My API Key"
223
- />
224
- </div>
225
- </div>
226
- <DialogFooter>
227
- <Button
228
- onClick={createApiKey}
229
- disabled={creating || !newKeyName.trim()}
230
- >
231
- {creating && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
232
- Create Key
233
- </Button>
234
- </DialogFooter>
235
- </DialogContent>
236
- </Dialog>
237
- </CardContent>
238
- </Card>
239
- )}
240
-
241
- {/* Usage Overview Section */}
242
- <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
243
- {/* Total Usage Card */}
244
- <Card>
245
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
246
- <CardTitle className="text-sm font-medium">Total Usage</CardTitle>
247
- <Activity className="h-4 w-4 text-muted-foreground" />
248
- </CardHeader>
249
- <CardContent>
250
- <div className="text-2xl font-bold">{userRequestsUsed.toLocaleString()}</div>
251
- <p className="text-xs text-muted-foreground">
252
- of {userRequestsLimit.toLocaleString()} requests
253
- </p>
254
- </CardContent>
255
- </Card>
256
-
257
- {/* Remaining Requests Card */}
258
- <Card>
259
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
260
- <CardTitle className="text-sm font-medium">Remaining</CardTitle>
261
- <TrendingUp className="h-4 w-4 text-muted-foreground" />
262
- </CardHeader>
263
- <CardContent>
264
- <div className="text-2xl font-bold text-green-600">{remainingRequests.toLocaleString()}</div>
265
- <p className="text-xs text-muted-foreground">requests left</p>
266
- </CardContent>
267
- </Card>
268
-
269
- {/* Usage Percentage Card */}
270
- <Card>
271
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
272
- <CardTitle className="text-sm font-medium">Usage Rate</CardTitle>
273
- <Activity className="h-4 w-4 text-muted-foreground" />
274
- </CardHeader>
275
- <CardContent>
276
- <div className="text-2xl font-bold">{usagePercentage}%</div>
277
- <Progress value={usagePercentage} className="mt-2" />
278
- </CardContent>
279
- </Card>
280
-
281
- {/* Active Keys Card */}
282
- <Card>
283
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
284
- <CardTitle className="text-sm font-medium">Active Keys</CardTitle>
285
- <Key className="h-4 w-4 text-muted-foreground" />
286
- </CardHeader>
287
- <CardContent>
288
- <div className="text-2xl font-bold">{apiKeys.filter(key => key.isActive).length}</div>
289
- <p className="text-xs text-muted-foreground">
290
- of {apiKeys.length} total keys
291
- </p>
292
- </CardContent>
293
- </Card>
294
- </div>
295
-
296
- {/* API Keys Table */}
297
- {!isFirstTime && (
298
- <Card>
299
- <CardHeader>
300
- <div className="flex items-center justify-between">
301
- <div>
302
- <CardTitle>Your API Keys</CardTitle>
303
- <CardDescription>
304
- Manage your API keys for accessing our services
305
- </CardDescription>
306
- </div>
307
- <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
308
- <DialogTrigger asChild>
309
- <Button>
310
- <Plus className="w-4 h-4 mr-2" />
311
- New API Key
312
- </Button>
313
- </DialogTrigger>
314
- <DialogContent>
315
- <DialogHeader>
316
- <DialogTitle>Create New API Key</DialogTitle>
317
- <DialogDescription>
318
- Give your API key a name to help you identify it later.
319
- </DialogDescription>
320
- </DialogHeader>
321
- <div className="grid gap-4 py-4">
322
- <div className="grid gap-2">
323
- <Label htmlFor="keyName">Key Name</Label>
324
- <Input
325
- id="keyName"
326
- value={newKeyName}
327
- onChange={(e) => setNewKeyName(e.target.value)}
328
- placeholder="My API Key"
329
- />
330
- </div>
331
- </div>
332
- <DialogFooter>
333
- <Button
334
- onClick={createApiKey}
335
- disabled={creating || !newKeyName.trim()}
336
- >
337
- {creating && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
338
- Create Key
339
- </Button>
340
- </DialogFooter>
341
- </DialogContent>
342
- </Dialog>
343
- </div>
344
- </CardHeader>
345
- <CardContent className="p-0">
346
- {error && (
347
- <Alert variant="destructive" className="mx-6 mb-6">
348
- <AlertDescription>{error}</AlertDescription>
349
- </Alert>
350
- )}
351
-
352
- {loading ? (
353
- <div className="flex items-center justify-center py-12">
354
- <Loader2 className="w-6 h-6 animate-spin mr-2" />
355
- <span className="text-muted-foreground">Loading API keys...</span>
356
- </div>
357
- ) : apiKeys.length === 0 ? (
358
- <div className="text-center py-12 px-6">
359
- <div className="w-20 h-20 mx-auto mb-4 bg-muted/50 rounded-full flex items-center justify-center">
360
- <Key className="w-10 h-10 text-muted-foreground" />
361
- </div>
362
- <h3 className="text-lg font-semibold mb-2">No API Keys</h3>
363
- <p className="text-muted-foreground mb-6 max-w-sm mx-auto">
364
- You haven't created any API keys yet. Create one to get started with our API services.
365
- </p>
366
- <Button
367
- onClick={() => setIsDialogOpen(true)}
368
- size="lg"
369
- disabled={userRequestsUsed >= userRequestsLimit}
370
- >
371
- <Plus className="w-4 h-4 mr-2" />
372
- Create Your First Key
373
- </Button>
374
- </div>
375
- ) : (
376
- <div className="overflow-x-auto">
377
- <Table>
378
- <TableHeader>
379
- <TableRow className="hover:bg-transparent">
380
- <TableHead className="h-12 px-6">Name</TableHead>
381
- <TableHead className="h-12">Key</TableHead>
382
- <TableHead className="h-12">Status</TableHead>
383
- <TableHead className="h-12">Created</TableHead>
384
- <TableHead className="h-12 text-right">Actions</TableHead>
385
- </TableRow>
386
- </TableHeader>
387
- <TableBody>
388
- {apiKeys.map((apiKey) => (
389
- <TableRow key={apiKey.id} className="hover:bg-muted/50">
390
- <TableCell className="font-medium px-6 py-4">{apiKey.keyName}</TableCell>
391
- <TableCell className="font-mono text-sm py-4">
392
- <div className="flex items-center gap-2">
393
- <span className="text-xs bg-muted px-2 py-1 rounded">
394
- {formatKeyValue(apiKey.keyValue, apiKey.id)}
395
- </span>
396
- <Button
397
- variant="ghost"
398
- size="sm"
399
- onClick={() => toggleKeyVisibility(apiKey.id)}
400
- className="h-8 w-8 p-0"
401
- >
402
- {visibleKeys.has(apiKey.id) ? (
403
- <EyeOff className="w-4 h-4" />
404
- ) : (
405
- <Eye className="w-4 h-4" />
406
- )}
407
- </Button>
408
- <Button
409
- variant="ghost"
410
- size="sm"
411
- onClick={() => copyToClipboard(apiKey.keyValue, apiKey.id)}
412
- className="h-8 w-8 p-0"
413
- >
414
- {copiedKeyId === apiKey.id ? (
415
- <Check className="w-4 h-4 text-green-500" />
416
- ) : (
417
- <Copy className="w-4 h-4" />
418
- )}
419
- </Button>
420
- </div>
421
- </TableCell>
422
- <TableCell className="py-4">
423
- <Badge variant={apiKey.isActive ? "default" : "secondary"} className="font-medium">
424
- {apiKey.isActive ? "Active" : "Inactive"}
425
- </Badge>
426
- </TableCell>
427
- <TableCell className="py-4 text-muted-foreground">
428
- <div className="flex items-center gap-1">
429
- <Calendar className="w-3 h-3" />
430
- {new Date(apiKey.createdAt).toLocaleDateString('en-US', {
431
- year: 'numeric',
432
- month: 'short',
433
- day: 'numeric'
434
- })}
435
- </div>
436
- </TableCell>
437
- <TableCell className="py-4 text-right">
438
- <Button
439
- variant="ghost"
440
- size="sm"
441
- onClick={() => deleteApiKey(apiKey.id)}
442
- className="text-destructive hover:text-destructive-foreground hover:bg-destructive/10 h-8 w-8 p-0"
443
- >
444
- <Trash2 className="w-4 h-4" />
445
- </Button>
446
- </TableCell>
447
- </TableRow>
448
- ))}
449
- </TableBody>
450
- </Table>
451
- </div>
452
- )}
453
- </CardContent>
454
- </Card>
455
- )}
456
- </div>
457
- </div>
458
- );
459
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+ import { useAuth } from '@/contexts/auth-context';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
7
+ import { Input } from '@/components/ui/input';
8
+ import { Label } from '@/components/ui/label';
9
+ import { Badge } from '@/components/ui/badge';
10
+ import { Alert, AlertDescription } from '@/components/ui/alert';
11
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
12
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
13
+ import { Progress } from '@/components/ui/progress';
14
+ import { Loader2, Key, Plus, Trash2, Eye, EyeOff, Copy, Check, TrendingUp, Activity, Calendar } from 'lucide-react';
15
+ import { ApiKey } from '@/lib/db/schema';
16
+ import { toast } from 'sonner';
17
+ import { useRouter } from 'next/navigation';
18
+
19
+ export default function ApiKeysPage() {
20
+ const { user, authLoading } = useAuth();
21
+ const router = useRouter();
22
+ const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
23
+ const [userRequestsUsed, setUserRequestsUsed] = useState(0);
24
+ const [userRequestsLimit, setUserRequestsLimit] = useState(1000);
25
+ const [loading, setLoading] = useState(true);
26
+ const [creating, setCreating] = useState(false);
27
+ const [newKeyName, setNewKeyName] = useState('');
28
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
29
+ const [error, setError] = useState('');
30
+ const [visibleKeys, setVisibleKeys] = useState<Set<string>>(new Set());
31
+ const [copiedKeyId, setCopiedKeyId] = useState<string | null>(null);
32
+ const [isFirstTime, setIsFirstTime] = useState(false);
33
+
34
+ const usagePercentage = Math.round((userRequestsUsed / userRequestsLimit) * 100) || 0;
35
+ const remainingRequests = userRequestsLimit - userRequestsUsed;
36
+
37
+ useEffect(() => {
38
+ if (!authLoading && !user) {
39
+ router.push("/login");
40
+ return;
41
+ }
42
+ }, [user, authLoading, router]);
43
+
44
+ useEffect(() => {
45
+ if (user) {
46
+ fetchApiKeys();
47
+ }
48
+ }, [user]);
49
+
50
+ // Helper to get auth headers with token
51
+ const getAuthHeaders = async () => {
52
+ if (!user) return {};
53
+ const token = await user.getIdToken();
54
+ return {
55
+ 'Authorization': `Bearer ${token}`,
56
+ 'Content-Type': 'application/json',
57
+ };
58
+ };
59
+
60
+ const fetchApiKeys = async () => {
61
+ if (!user) return;
62
+
63
+ try {
64
+ setLoading(true);
65
+ // Get headers with token
66
+ const headers = await getAuthHeaders();
67
+
68
+ // Pass headers to fetch
69
+ const response = await fetch(`/api/api-keys`, {
70
+ method: 'GET',
71
+ headers
72
+ });
73
+
74
+ if (response.status === 401) {
75
+ setError('Unauthorized: Please log in again');
76
+ return;
77
+ }
78
+
79
+ const data = await response.json();
80
+
81
+ // Check if response is array (API returns array of keys directly in GET)
82
+ if (Array.isArray(data)) {
83
+ setApiKeys(data);
84
+ // Defaults for usage if not returned in this specific endpoint yet
85
+ // Ideally API should return user stats too, or we fetch them separately
86
+ setUserRequestsUsed(0);
87
+ setUserRequestsLimit(1000);
88
+ setIsFirstTime(data.length === 0);
89
+ } else if (data.error) {
90
+ setError(data.error);
91
+ }
92
+ } catch (error) {
93
+ console.error('Error fetching API keys:', error);
94
+ setError('Failed to fetch API keys');
95
+ } finally {
96
+ setLoading(false);
97
+ }
98
+ };
99
+
100
+ const createApiKey = async () => {
101
+ if (!user || !newKeyName.trim()) return;
102
+
103
+ try {
104
+ setCreating(true);
105
+ setError('');
106
+
107
+ const headers = await getAuthHeaders();
108
+ const response = await fetch('/api/api-keys', {
109
+ method: 'POST',
110
+ headers, // Use auth headers
111
+ body: JSON.stringify({
112
+ name: newKeyName.trim(), // API expects 'name', not 'keyName' in body based on route.ts
113
+ }),
114
+ });
115
+
116
+ const data = await response.json();
117
+
118
+ if (response.ok) {
119
+ setApiKeys([data, ...apiKeys]); // Add new key to start of list
120
+ setNewKeyName('');
121
+ setIsDialogOpen(false);
122
+ toast.success('API Key Created');
123
+ } else {
124
+ setError(data.error || 'Failed to create API key');
125
+ }
126
+ } catch (error) {
127
+ setError('Failed to create API key');
128
+ } finally {
129
+ setCreating(false);
130
+ }
131
+ };
132
+
133
+ const deleteApiKey = async (keyId: string) => {
134
+ if (!user) return;
135
+
136
+ try {
137
+ const headers = await getAuthHeaders();
138
+ // Pass ID as query param, headers for auth
139
+ const response = await fetch(`/api/api-keys?id=${keyId}`, {
140
+ method: 'DELETE',
141
+ headers
142
+ });
143
+
144
+ const data = await response.json();
145
+
146
+ if (data.success) {
147
+ setApiKeys(apiKeys.filter(key => key.id !== keyId));
148
+ toast.success('API Key Deleted');
149
+ } else {
150
+ setError(data.error || 'Failed to delete API key');
151
+ toast.error(data.error || 'Failed to delete API key');
152
+ }
153
+ } catch (error) {
154
+ setError('Failed to delete API key');
155
+ }
156
+ };
157
+
158
+ const toggleKeyVisibility = (keyId: string) => {
159
+ const newVisibleKeys = new Set(visibleKeys);
160
+ if (newVisibleKeys.has(keyId)) {
161
+ newVisibleKeys.delete(keyId);
162
+ } else {
163
+ newVisibleKeys.add(keyId);
164
+ }
165
+ setVisibleKeys(newVisibleKeys);
166
+ };
167
+
168
+ const copyToClipboard = async (text: string, keyId: string) => {
169
+ try {
170
+ await navigator.clipboard.writeText(text);
171
+ setCopiedKeyId(keyId);
172
+ setTimeout(() => setCopiedKeyId(null), 2000);
173
+
174
+ // Show success toast
175
+ toast.success('API key copied successfully!', {
176
+ description: 'The API key has been copied to your clipboard.',
177
+ });
178
+ } catch (error) {
179
+ console.error('Failed to copy:', error);
180
+
181
+ // Show error toast
182
+ toast.error('Failed to copy API key', {
183
+ description: 'Please try again or copy manually.',
184
+ });
185
+ }
186
+ };
187
+
188
+ const formatKeyValue = (keyValue: string, keyId: string) => {
189
+ if (visibleKeys.has(keyId)) {
190
+ return keyValue;
191
+ }
192
+ return `${keyValue.slice(0, 8)}${'*'.repeat(32)}${keyValue.slice(-8)}`;
193
+ };
194
+
195
+ if (!user) {
196
+ return null;
197
+ }
198
+
199
+ return (
200
+ <div className="flex flex-col min-h-screen">
201
+ <div className="border-b">
202
+ <div className="flex h-16 items-center px-4">
203
+ <div className="flex items-center gap-3">
204
+ <div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10">
205
+ <Key className="w-4 h-4 text-primary" />
206
+ </div>
207
+ <div>
208
+ <h1 className="text-lg font-semibold">API Keys</h1>
209
+ <p className="text-sm text-muted-foreground">
210
+ {isFirstTime ? 'Create your first API key to get started' : 'Manage your API access'}
211
+ </p>
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </div>
216
+
217
+ <div className="flex flex-1 flex-col gap-6 p-6">
218
+ {/* Show welcome card for first-time users */}
219
+ {isFirstTime && (
220
+ <Card className="border-dashed border-2 border-primary/20 bg-primary/5">
221
+ <CardHeader>
222
+ <CardTitle className="flex items-center gap-2">
223
+ <Key className="w-5 h-5" />
224
+ Welcome! Create Your First API Key
225
+ </CardTitle>
226
+ <CardDescription>
227
+ API keys are required to access our scraping services. Create your first API key to start using our APIs for anime, movies, and other content.
228
+ </CardDescription>
229
+ </CardHeader>
230
+ <CardContent>
231
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
232
+ <DialogTrigger asChild>
233
+ <Button size="lg">
234
+ <Plus className="w-4 h-4 mr-2" />
235
+ Create Your First API Key
236
+ </Button>
237
+ </DialogTrigger>
238
+ <DialogContent>
239
+ <DialogHeader>
240
+ <DialogTitle>Create New API Key</DialogTitle>
241
+ <DialogDescription>
242
+ Give your API key a name to help you identify it later.
243
+ </DialogDescription>
244
+ </DialogHeader>
245
+ <div className="grid gap-4 py-4">
246
+ <div className="grid gap-2">
247
+ <Label htmlFor="keyName">Key Name</Label>
248
+ <Input
249
+ id="keyName"
250
+ value={newKeyName}
251
+ onChange={(e) => setNewKeyName(e.target.value)}
252
+ placeholder="My API Key"
253
+ />
254
+ </div>
255
+ </div>
256
+ <DialogFooter>
257
+ <Button
258
+ onClick={createApiKey}
259
+ disabled={creating || !newKeyName.trim()}
260
+ >
261
+ {creating && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
262
+ Create Key
263
+ </Button>
264
+ </DialogFooter>
265
+ </DialogContent>
266
+ </Dialog>
267
+ </CardContent>
268
+ </Card>
269
+ )}
270
+
271
+ {/* Usage Overview Section */}
272
+ <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
273
+ {/* Total Usage Card */}
274
+ <Card>
275
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
276
+ <CardTitle className="text-sm font-medium">Total Usage</CardTitle>
277
+ <Activity className="h-4 w-4 text-muted-foreground" />
278
+ </CardHeader>
279
+ <CardContent>
280
+ <div className="text-2xl font-bold">{userRequestsUsed.toLocaleString()}</div>
281
+ <p className="text-xs text-muted-foreground">
282
+ of {userRequestsLimit.toLocaleString()} requests
283
+ </p>
284
+ </CardContent>
285
+ </Card>
286
+
287
+ {/* Remaining Requests Card */}
288
+ <Card>
289
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
290
+ <CardTitle className="text-sm font-medium">Remaining</CardTitle>
291
+ <TrendingUp className="h-4 w-4 text-muted-foreground" />
292
+ </CardHeader>
293
+ <CardContent>
294
+ <div className="text-2xl font-bold text-green-600">{remainingRequests.toLocaleString()}</div>
295
+ <p className="text-xs text-muted-foreground">requests left</p>
296
+ </CardContent>
297
+ </Card>
298
+
299
+ {/* Usage Percentage Card */}
300
+ <Card>
301
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
302
+ <CardTitle className="text-sm font-medium">Usage Rate</CardTitle>
303
+ <Activity className="h-4 w-4 text-muted-foreground" />
304
+ </CardHeader>
305
+ <CardContent>
306
+ <div className="text-2xl font-bold">{usagePercentage}%</div>
307
+ <Progress value={usagePercentage} className="mt-2" />
308
+ </CardContent>
309
+ </Card>
310
+
311
+ {/* Active Keys Card */}
312
+ <Card>
313
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
314
+ <CardTitle className="text-sm font-medium">Active Keys</CardTitle>
315
+ <Key className="h-4 w-4 text-muted-foreground" />
316
+ </CardHeader>
317
+ <CardContent>
318
+ <div className="text-2xl font-bold">{apiKeys.filter(key => key.isActive).length}</div>
319
+ <p className="text-xs text-muted-foreground">
320
+ of {apiKeys.length} total keys
321
+ </p>
322
+ </CardContent>
323
+ </Card>
324
+ </div>
325
+
326
+ {/* API Keys Table */}
327
+ {!isFirstTime && (
328
+ <Card>
329
+ <CardHeader>
330
+ <div className="flex items-center justify-between">
331
+ <div>
332
+ <CardTitle>Your API Keys</CardTitle>
333
+ <CardDescription>
334
+ Manage your API keys for accessing our services
335
+ </CardDescription>
336
+ </div>
337
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
338
+ <DialogTrigger asChild>
339
+ <Button>
340
+ <Plus className="w-4 h-4 mr-2" />
341
+ New API Key
342
+ </Button>
343
+ </DialogTrigger>
344
+ <DialogContent>
345
+ <DialogHeader>
346
+ <DialogTitle>Create New API Key</DialogTitle>
347
+ <DialogDescription>
348
+ Give your API key a name to help you identify it later.
349
+ </DialogDescription>
350
+ </DialogHeader>
351
+ <div className="grid gap-4 py-4">
352
+ <div className="grid gap-2">
353
+ <Label htmlFor="keyName">Key Name</Label>
354
+ <Input
355
+ id="keyName"
356
+ value={newKeyName}
357
+ onChange={(e) => setNewKeyName(e.target.value)}
358
+ placeholder="My API Key"
359
+ />
360
+ </div>
361
+ </div>
362
+ <DialogFooter>
363
+ <Button
364
+ onClick={createApiKey}
365
+ disabled={creating || !newKeyName.trim()}
366
+ >
367
+ {creating && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
368
+ Create Key
369
+ </Button>
370
+ </DialogFooter>
371
+ </DialogContent>
372
+ </Dialog>
373
+ </div>
374
+ </CardHeader>
375
+ <CardContent className="p-0">
376
+ {error && (
377
+ <Alert variant="destructive" className="mx-6 mb-6">
378
+ <AlertDescription>{error}</AlertDescription>
379
+ </Alert>
380
+ )}
381
+
382
+ {loading ? (
383
+ <div className="flex items-center justify-center py-12">
384
+ <Loader2 className="w-6 h-6 animate-spin mr-2" />
385
+ <span className="text-muted-foreground">Loading API keys...</span>
386
+ </div>
387
+ ) : apiKeys.length === 0 ? (
388
+ <div className="text-center py-12 px-6">
389
+ <div className="w-20 h-20 mx-auto mb-4 bg-muted/50 rounded-full flex items-center justify-center">
390
+ <Key className="w-10 h-10 text-muted-foreground" />
391
+ </div>
392
+ <h3 className="text-lg font-semibold mb-2">No API Keys</h3>
393
+ <p className="text-muted-foreground mb-6 max-w-sm mx-auto">
394
+ You haven't created any API keys yet. Create one to get started with our API services.
395
+ </p>
396
+ <Button
397
+ onClick={() => setIsDialogOpen(true)}
398
+ size="lg"
399
+ disabled={userRequestsUsed >= userRequestsLimit}
400
+ >
401
+ <Plus className="w-4 h-4 mr-2" />
402
+ Create Your First Key
403
+ </Button>
404
+ </div>
405
+ ) : (
406
+ <div className="overflow-x-auto">
407
+ <Table>
408
+ <TableHeader>
409
+ <TableRow className="hover:bg-transparent">
410
+ <TableHead className="h-12 px-6">Name</TableHead>
411
+ <TableHead className="h-12">Key</TableHead>
412
+ <TableHead className="h-12">Status</TableHead>
413
+ <TableHead className="h-12">Created</TableHead>
414
+ <TableHead className="h-12 text-right">Actions</TableHead>
415
+ </TableRow>
416
+ </TableHeader>
417
+ <TableBody>
418
+ {apiKeys.map((apiKey) => (
419
+ <TableRow key={apiKey.id} className="hover:bg-muted/50">
420
+ <TableCell className="font-medium px-6 py-4">{apiKey.keyName}</TableCell>
421
+ <TableCell className="font-mono text-sm py-4">
422
+ <div className="flex items-center gap-2">
423
+ <span className="text-xs bg-muted px-2 py-1 rounded">
424
+ {formatKeyValue(apiKey.keyValue, apiKey.id)}
425
+ </span>
426
+ <Button
427
+ variant="ghost"
428
+ size="sm"
429
+ onClick={() => toggleKeyVisibility(apiKey.id)}
430
+ className="h-8 w-8 p-0"
431
+ >
432
+ {visibleKeys.has(apiKey.id) ? (
433
+ <EyeOff className="w-4 h-4" />
434
+ ) : (
435
+ <Eye className="w-4 h-4" />
436
+ )}
437
+ </Button>
438
+ <Button
439
+ variant="ghost"
440
+ size="sm"
441
+ onClick={() => copyToClipboard(apiKey.keyValue, apiKey.id)}
442
+ className="h-8 w-8 p-0"
443
+ >
444
+ {copiedKeyId === apiKey.id ? (
445
+ <Check className="w-4 h-4 text-green-500" />
446
+ ) : (
447
+ <Copy className="w-4 h-4" />
448
+ )}
449
+ </Button>
450
+ </div>
451
+ </TableCell>
452
+ <TableCell className="py-4">
453
+ <Badge variant={apiKey.isActive ? "default" : "secondary"} className="font-medium">
454
+ {apiKey.isActive ? "Active" : "Inactive"}
455
+ </Badge>
456
+ </TableCell>
457
+ <TableCell className="py-4 text-muted-foreground">
458
+ <div className="flex items-center gap-1">
459
+ <Calendar className="w-3 h-3" />
460
+ {new Date(apiKey.createdAt).toLocaleDateString('en-US', {
461
+ year: 'numeric',
462
+ month: 'short',
463
+ day: 'numeric'
464
+ })}
465
+ </div>
466
+ </TableCell>
467
+ <TableCell className="py-4 text-right">
468
+ <Button
469
+ variant="ghost"
470
+ size="sm"
471
+ onClick={() => deleteApiKey(apiKey.id)}
472
+ className="text-destructive hover:text-destructive-foreground hover:bg-destructive/10 h-8 w-8 p-0"
473
+ >
474
+ <Trash2 className="w-4 h-4" />
475
+ </Button>
476
+ </TableCell>
477
+ </TableRow>
478
+ ))}
479
+ </TableBody>
480
+ </Table>
481
+ </div>
482
+ )}
483
+ </CardContent>
484
+ </Card>
485
+ )}
486
+ </div>
487
+ </div>
488
+ );
489
+ }