Sfoglia il codice sorgente

feat: add API Keys management page and UI #48

- Created APIKeysContent component with full CRUD functionality
- Added /api-keys route (protected)
- Added API Keys menu item in sidebar
- Features:
  - List all API keys with status (active/revoked/expired)
  - Create new API keys with custom expiration
  - One-time display of full key on creation
  - Copy to clipboard functionality
  - Revoke keys
  - Security warnings and best practices
  - Rate limit: 10 keys per user
Claude 5 mesi fa
parent
commit
e619117418

+ 2 - 0
shopcall.ai-main/src/App.tsx

@@ -15,6 +15,7 @@ import Webshops from "./pages/Webshops";
 import PhoneNumbers from "./pages/PhoneNumbers";
 import PhoneNumbers from "./pages/PhoneNumbers";
 import AIConfig from "./pages/AIConfig";
 import AIConfig from "./pages/AIConfig";
 import ManageStoreData from "./pages/ManageStoreData";
 import ManageStoreData from "./pages/ManageStoreData";
+import APIKeys from "./pages/APIKeys";
 import Onboarding from "./pages/Onboarding";
 import Onboarding from "./pages/Onboarding";
 import About from "./pages/About";
 import About from "./pages/About";
 import Privacy from "./pages/Privacy";
 import Privacy from "./pages/Privacy";
@@ -47,6 +48,7 @@ const App = () => (
               <Route path="/phone-numbers" element={<PhoneNumbers />} />
               <Route path="/phone-numbers" element={<PhoneNumbers />} />
               <Route path="/ai-config" element={<AIConfig />} />
               <Route path="/ai-config" element={<AIConfig />} />
               <Route path="/manage-store-data" element={<ManageStoreData />} />
               <Route path="/manage-store-data" element={<ManageStoreData />} />
+              <Route path="/api-keys" element={<APIKeys />} />
               <Route path="/onboarding" element={<Onboarding />} />
               <Route path="/onboarding" element={<Onboarding />} />
             </Route>
             </Route>
             <Route path="/about" element={<About />} />
             <Route path="/about" element={<About />} />

+ 387 - 0
shopcall.ai-main/src/components/APIKeysContent.tsx

@@ -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>
+  );
+}

+ 6 - 1
shopcall.ai-main/src/components/AppSidebar.tsx

@@ -11,7 +11,7 @@ import {
   SidebarFooter,
   SidebarFooter,
 } from "@/components/ui/sidebar";
 } from "@/components/ui/sidebar";
 import { useNavigate } from "react-router-dom";
 import { useNavigate } from "react-router-dom";
-import { LayoutDashboard, Phone, BarChart3, Settings, CreditCard, Layers3, PhoneCall } from "lucide-react";
+import { LayoutDashboard, Phone, BarChart3, Settings, CreditCard, Layers3, PhoneCall, Key } from "lucide-react";
 
 
 const menuItems = [
 const menuItems = [
   {
   {
@@ -52,6 +52,11 @@ const configItems = [
     icon: Settings,
     icon: Settings,
     url: "/ai-config",
     url: "/ai-config",
   },
   },
+  {
+    title: "API Keys",
+    icon: Key,
+    url: "/api-keys",
+  },
   {
   {
     title: "Billing & Plan",
     title: "Billing & Plan",
     icon: CreditCard,
     icon: CreditCard,

+ 20 - 0
shopcall.ai-main/src/pages/APIKeys.tsx

@@ -0,0 +1,20 @@
+import { APIKeysContent } from "@/components/APIKeysContent";
+import { AppSidebar } from "@/components/AppSidebar";
+import { DashboardHeader } from "@/components/DashboardHeader";
+import { SidebarProvider } from "@/components/ui/sidebar";
+
+const APIKeys = () => {
+  return (
+    <SidebarProvider>
+      <div className="flex min-h-screen w-full">
+        <AppSidebar />
+        <div className="flex-1">
+          <DashboardHeader />
+          <APIKeysContent />
+        </div>
+      </div>
+    </SidebarProvider>
+  );
+};
+
+export default APIKeys;