|
@@ -0,0 +1,387 @@
|
|
|
|
|
+import { useState, useEffect } from "react";
|
|
|
|
|
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
|
+import { Button } from "@/components/ui/button";
|
|
|
|
|
+import { Badge } from "@/components/ui/badge";
|
|
|
|
|
+import { Input } from "@/components/ui/input";
|
|
|
|
|
+import { Label } from "@/components/ui/label";
|
|
|
|
|
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
|
|
|
|
+import { Plus, Key, Copy, Trash2, RefreshCw, Eye, EyeOff, AlertCircle } from "lucide-react";
|
|
|
|
|
+import { API_URL } from "@/lib/config";
|
|
|
|
|
+import { useToast } from "@/hooks/use-toast";
|
|
|
|
|
+import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
|
|
|
+
|
|
|
|
|
+interface APIKey {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ key_name: string;
|
|
|
|
|
+ api_key: string;
|
|
|
|
|
+ is_active: boolean;
|
|
|
|
|
+ last_used_at: string | null;
|
|
|
|
|
+ expires_at: string | null;
|
|
|
|
|
+ created_at: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function APIKeysContent() {
|
|
|
|
|
+ const [apiKeys, setApiKeys] = useState<APIKey[]>([]);
|
|
|
|
|
+ const [loading, setLoading] = useState(true);
|
|
|
|
|
+ const [showCreateDialog, setShowCreateDialog] = useState(false);
|
|
|
|
|
+ const [showKeyDialog, setShowKeyDialog] = useState(false);
|
|
|
|
|
+ const [newKeyName, setNewKeyName] = useState("");
|
|
|
|
|
+ const [newKeyExpireDays, setNewKeyExpireDays] = useState("365");
|
|
|
|
|
+ const [createdKey, setCreatedKey] = useState<string | null>(null);
|
|
|
|
|
+ const [creating, setCreating] = useState(false);
|
|
|
|
|
+ const { toast } = useToast();
|
|
|
|
|
+
|
|
|
|
|
+ const fetchAPIKeys = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const sessionData = localStorage.getItem('session_data');
|
|
|
|
|
+ if (!sessionData) {
|
|
|
|
|
+ throw new Error('No session data found');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const session = JSON.parse(sessionData);
|
|
|
|
|
+ const response = await fetch(`${API_URL}/api-key-management/list`, {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${session.access_token}`,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error('Failed to fetch API keys');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ setApiKeys(data.keys || []);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error fetching API keys:', error);
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: "Error",
|
|
|
|
|
+ description: "Failed to load API keys. Please try again.",
|
|
|
|
|
+ variant: "destructive"
|
|
|
|
|
+ });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ fetchAPIKeys();
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
|
|
+ const handleCreateKey = async () => {
|
|
|
|
|
+ if (!newKeyName.trim()) {
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: "Error",
|
|
|
|
|
+ description: "Please enter a key name.",
|
|
|
|
|
+ variant: "destructive"
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setCreating(true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ const sessionData = localStorage.getItem('session_data');
|
|
|
|
|
+ if (!sessionData) {
|
|
|
|
|
+ throw new Error('No session data found');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const session = JSON.parse(sessionData);
|
|
|
|
|
+ const response = await fetch(`${API_URL}/api-key-management/create`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${session.access_token}`,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ name: newKeyName.trim(),
|
|
|
|
|
+ expires_in_days: parseInt(newKeyExpireDays)
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ const errorData = await response.json();
|
|
|
|
|
+ throw new Error(errorData.error || 'Failed to create API key');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ setCreatedKey(data.api_key);
|
|
|
|
|
+ setShowCreateDialog(false);
|
|
|
|
|
+ setShowKeyDialog(true);
|
|
|
|
|
+ setNewKeyName("");
|
|
|
|
|
+ setNewKeyExpireDays("365");
|
|
|
|
|
+
|
|
|
|
|
+ // Refresh the list
|
|
|
|
|
+ fetchAPIKeys();
|
|
|
|
|
+
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: "Success",
|
|
|
|
|
+ description: "API key created successfully!",
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error creating API key:', error);
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: "Error",
|
|
|
|
|
+ description: error instanceof Error ? error.message : "Failed to create API key. Please try again.",
|
|
|
|
|
+ variant: "destructive"
|
|
|
|
|
+ });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setCreating(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleRevokeKey = async (keyId: string, keyName: string) => {
|
|
|
|
|
+ if (!confirm(`Are you sure you want to revoke the API key "${keyName}"? This action cannot be undone.`)) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const sessionData = localStorage.getItem('session_data');
|
|
|
|
|
+ if (!sessionData) {
|
|
|
|
|
+ throw new Error('No session data found');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const session = JSON.parse(sessionData);
|
|
|
|
|
+ const response = await fetch(`${API_URL}/api-key-management/revoke`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${session.access_token}`,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({ key_id: keyId })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error('Failed to revoke API key');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: "Success",
|
|
|
|
|
+ description: "API key revoked successfully.",
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Refresh the list
|
|
|
|
|
+ fetchAPIKeys();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error revoking API key:', error);
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: "Error",
|
|
|
|
|
+ description: "Failed to revoke API key. Please try again.",
|
|
|
|
|
+ variant: "destructive"
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const copyToClipboard = (text: string) => {
|
|
|
|
|
+ navigator.clipboard.writeText(text);
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: "Copied!",
|
|
|
|
|
+ description: "API key copied to clipboard.",
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const formatDate = (dateString: string | null) => {
|
|
|
|
|
+ if (!dateString) return 'Never';
|
|
|
|
|
+ return new Date(dateString).toLocaleString();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const isExpired = (expiresAt: string | null) => {
|
|
|
|
|
+ if (!expiresAt) return false;
|
|
|
|
|
+ return new Date(expiresAt) < new Date();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (loading) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="flex items-center justify-center h-64">
|
|
|
|
|
+ <RefreshCw className="h-8 w-8 animate-spin text-primary" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="p-6 space-y-6">
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h1 className="text-3xl font-bold">API Keys</h1>
|
|
|
|
|
+ <p className="text-muted-foreground mt-2">
|
|
|
|
|
+ Manage your API keys for accessing webshop data
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Button onClick={() => setShowCreateDialog(true)} disabled={apiKeys.length >= 10}>
|
|
|
|
|
+ <Plus className="h-4 w-4 mr-2" />
|
|
|
|
|
+ Create API Key
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <Alert>
|
|
|
|
|
+ <AlertCircle className="h-4 w-4" />
|
|
|
|
|
+ <AlertDescription>
|
|
|
|
|
+ <strong>Security Notice:</strong> API keys provide access to your webshop data via the Webshop Data API.
|
|
|
|
|
+ Keep them secure and never share them publicly. You can create up to 10 API keys per account.
|
|
|
|
|
+ </AlertDescription>
|
|
|
|
|
+ </Alert>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="grid gap-4">
|
|
|
|
|
+ {apiKeys.length === 0 ? (
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardContent className="flex flex-col items-center justify-center py-12">
|
|
|
|
|
+ <Key className="h-12 w-12 text-muted-foreground mb-4" />
|
|
|
|
|
+ <p className="text-lg font-medium">No API keys yet</p>
|
|
|
|
|
+ <p className="text-sm text-muted-foreground mb-4">
|
|
|
|
|
+ Create your first API key to start accessing webshop data
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <Button onClick={() => setShowCreateDialog(true)}>
|
|
|
|
|
+ <Plus className="h-4 w-4 mr-2" />
|
|
|
|
|
+ Create API Key
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ apiKeys.map((key) => (
|
|
|
|
|
+ <Card key={key.id} className={!key.is_active || isExpired(key.expires_at) ? "opacity-60" : ""}>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <div className="flex items-start justify-between">
|
|
|
|
|
+ <div className="flex-1">
|
|
|
|
|
+ <CardTitle className="flex items-center gap-2">
|
|
|
|
|
+ <Key className="h-5 w-5" />
|
|
|
|
|
+ {key.key_name}
|
|
|
|
|
+ {!key.is_active && (
|
|
|
|
|
+ <Badge variant="destructive">Revoked</Badge>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {key.is_active && isExpired(key.expires_at) && (
|
|
|
|
|
+ <Badge variant="destructive">Expired</Badge>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {key.is_active && !isExpired(key.expires_at) && (
|
|
|
|
|
+ <Badge variant="default">Active</Badge>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </CardTitle>
|
|
|
|
|
+ <CardDescription className="mt-2">
|
|
|
|
|
+ <div className="space-y-1">
|
|
|
|
|
+ <div>Created: {formatDate(key.created_at)}</div>
|
|
|
|
|
+ <div>Last used: {formatDate(key.last_used_at)}</div>
|
|
|
|
|
+ <div>Expires: {key.expires_at ? formatDate(key.expires_at) : 'Never'}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardDescription>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex gap-2">
|
|
|
|
|
+ {key.is_active && !isExpired(key.expires_at) && (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="destructive"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ onClick={() => handleRevokeKey(key.id, key.key_name)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Trash2 className="h-4 w-4 mr-2" />
|
|
|
|
|
+ Revoke
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <div className="flex items-center gap-2 font-mono text-sm bg-muted p-3 rounded">
|
|
|
|
|
+ <code className="flex-1">
|
|
|
|
|
+ {key.api_key.substring(0, 16)}...{key.api_key.substring(key.api_key.length - 8)}
|
|
|
|
|
+ </code>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ onClick={() => copyToClipboard(key.api_key)}
|
|
|
|
|
+ disabled={!key.is_active || isExpired(key.expires_at)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Copy className="h-4 w-4" />
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ ))
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Create API Key Dialog */}
|
|
|
|
|
+ <Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
|
|
|
|
+ <DialogContent>
|
|
|
|
|
+ <DialogHeader>
|
|
|
|
|
+ <DialogTitle>Create New API Key</DialogTitle>
|
|
|
|
|
+ <DialogDescription>
|
|
|
|
|
+ Give your API key a descriptive name and set an expiration period.
|
|
|
|
|
+ </DialogDescription>
|
|
|
|
|
+ </DialogHeader>
|
|
|
|
|
+ <div className="space-y-4 py-4">
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <Label htmlFor="keyName">Key Name</Label>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ id="keyName"
|
|
|
|
|
+ placeholder="e.g., Production API Key"
|
|
|
|
|
+ value={newKeyName}
|
|
|
|
|
+ onChange={(e) => setNewKeyName(e.target.value)}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <Label htmlFor="expireDays">Expires In (days)</Label>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ id="expireDays"
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ min="1"
|
|
|
|
|
+ max="3650"
|
|
|
|
|
+ value={newKeyExpireDays}
|
|
|
|
|
+ onChange={(e) => setNewKeyExpireDays(e.target.value)}
|
|
|
|
|
+ />
|
|
|
|
|
+ <p className="text-sm text-muted-foreground">
|
|
|
|
|
+ Default: 365 days (1 year)
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <DialogFooter>
|
|
|
|
|
+ <Button variant="outline" onClick={() => setShowCreateDialog(false)}>
|
|
|
|
|
+ Cancel
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button onClick={handleCreateKey} disabled={creating}>
|
|
|
|
|
+ {creating && <RefreshCw className="h-4 w-4 mr-2 animate-spin" />}
|
|
|
|
|
+ Create Key
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </DialogFooter>
|
|
|
|
|
+ </DialogContent>
|
|
|
|
|
+ </Dialog>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Show Created Key Dialog */}
|
|
|
|
|
+ <Dialog open={showKeyDialog} onOpenChange={setShowKeyDialog}>
|
|
|
|
|
+ <DialogContent>
|
|
|
|
|
+ <DialogHeader>
|
|
|
|
|
+ <DialogTitle>API Key Created!</DialogTitle>
|
|
|
|
|
+ <DialogDescription>
|
|
|
|
|
+ <Alert className="mt-4" variant="destructive">
|
|
|
|
|
+ <AlertCircle className="h-4 w-4" />
|
|
|
|
|
+ <AlertDescription>
|
|
|
|
|
+ <strong>Important:</strong> This is the only time you'll see the full API key.
|
|
|
|
|
+ Copy it now and store it securely.
|
|
|
|
|
+ </AlertDescription>
|
|
|
|
|
+ </Alert>
|
|
|
|
|
+ </DialogDescription>
|
|
|
|
|
+ </DialogHeader>
|
|
|
|
|
+ <div className="space-y-4 py-4">
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <Label>Your API Key</Label>
|
|
|
|
|
+ <div className="flex items-center gap-2 font-mono text-sm bg-muted p-3 rounded break-all">
|
|
|
|
|
+ <code className="flex-1">{createdKey}</code>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ onClick={() => createdKey && copyToClipboard(createdKey)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Copy className="h-4 w-4" />
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <DialogFooter>
|
|
|
|
|
+ <Button onClick={() => {
|
|
|
|
|
+ setShowKeyDialog(false);
|
|
|
|
|
+ setCreatedKey(null);
|
|
|
|
|
+ }}>
|
|
|
|
|
+ I've Saved My Key
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </DialogFooter>
|
|
|
|
|
+ </DialogContent>
|
|
|
|
|
+ </Dialog>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|