Seth commited on
Commit
d7fe8fb
·
1 Parent(s): 272f36f
frontend/src/App.jsx CHANGED
@@ -1,111 +1,3 @@
1
- <<<<<<< HEAD
2
- // frontend/src/App.jsx
3
-
4
- import React, { useEffect } from "react";
5
- import { Routes, Route, useNavigate, useSearchParams } from "react-router-dom";
6
- import { AuthProvider, useAuth } from "./contexts/AuthContext";
7
- import Layout from "./Layout";
8
- import Dashboard from "./pages/Dashboard";
9
- import History from "./pages/History";
10
- import ShareHandler from "./pages/ShareHandler";
11
- import LoginForm from "./components/auth/LoginForm";
12
-
13
- // Auth callback handler component
14
- function AuthCallback() {
15
- const [searchParams] = useSearchParams();
16
- const { handleAuthCallback } = useAuth();
17
- const navigate = useNavigate();
18
-
19
- useEffect(() => {
20
- const token = searchParams.get("token");
21
- if (token) {
22
- handleAuthCallback(token);
23
- navigate("/");
24
- } else {
25
- navigate("/");
26
- }
27
- }, [searchParams, handleAuthCallback, navigate]);
28
-
29
- return (
30
- <div className="min-h-screen flex items-center justify-center">
31
- <div className="text-center">
32
- <p className="text-slate-600">Completing authentication...</p>
33
- </div>
34
- </div>
35
- );
36
- }
37
-
38
- // Protected route wrapper
39
- function ProtectedRoute({ children }) {
40
- const { isAuthenticated, loading } = useAuth();
41
-
42
- if (loading) {
43
- return (
44
- <div className="min-h-screen flex items-center justify-center">
45
- <div className="text-center">
46
- <div className="h-16 w-16 mx-auto rounded-2xl bg-indigo-100 flex items-center justify-center mb-4 animate-pulse">
47
- <div className="h-8 w-8 rounded-lg bg-indigo-600"></div>
48
- </div>
49
- <p className="text-slate-600">Loading...</p>
50
- </div>
51
- </div>
52
- );
53
- }
54
-
55
- if (!isAuthenticated) {
56
- return <LoginForm />;
57
- }
58
-
59
- return children;
60
- }
61
-
62
- function AppRoutes() {
63
- return (
64
- <Routes>
65
- <Route
66
- path="/auth/callback"
67
- element={<AuthCallback />}
68
- />
69
- <Route
70
- path="/share/:token"
71
- element={
72
- <ProtectedRoute>
73
- <ShareHandler />
74
- </ProtectedRoute>
75
- }
76
- />
77
- <Route
78
- path="/"
79
- element={
80
- <ProtectedRoute>
81
- <Layout currentPageName="Dashboard">
82
- <Dashboard />
83
- </Layout>
84
- </ProtectedRoute>
85
- }
86
- />
87
- <Route
88
- path="/history"
89
- element={
90
- <ProtectedRoute>
91
- <Layout currentPageName="History">
92
- <History />
93
- </Layout>
94
- </ProtectedRoute>
95
- }
96
- />
97
- </Routes>
98
- );
99
- }
100
-
101
- export default function App() {
102
- return (
103
- <AuthProvider>
104
- <AppRoutes />
105
- </AuthProvider>
106
- );
107
- }
108
- =======
109
  // frontend/src/App.jsx
110
 
111
  import React, { useEffect } from "react";
@@ -114,6 +6,7 @@ import { AuthProvider, useAuth } from "./contexts/AuthContext";
114
  import Layout from "./Layout";
115
  import Dashboard from "./pages/Dashboard";
116
  import History from "./pages/History";
 
117
  import ShareHandler from "./pages/ShareHandler";
118
  import LoginForm from "./components/auth/LoginForm";
119
 
@@ -201,6 +94,16 @@ function AppRoutes() {
201
  </ProtectedRoute>
202
  }
203
  />
 
 
 
 
 
 
 
 
 
 
204
  </Routes>
205
  );
206
  }
@@ -212,4 +115,3 @@ export default function App() {
212
  </AuthProvider>
213
  );
214
  }
215
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  // frontend/src/App.jsx
2
 
3
  import React, { useEffect } from "react";
 
6
  import Layout from "./Layout";
7
  import Dashboard from "./pages/Dashboard";
8
  import History from "./pages/History";
9
+ import APIKeys from "./pages/APIKeys";
10
  import ShareHandler from "./pages/ShareHandler";
11
  import LoginForm from "./components/auth/LoginForm";
12
 
 
94
  </ProtectedRoute>
95
  }
96
  />
97
+ <Route
98
+ path="/api-keys"
99
+ element={
100
+ <ProtectedRoute>
101
+ <Layout currentPageName="API Keys">
102
+ <APIKeys />
103
+ </Layout>
104
+ </ProtectedRoute>
105
+ }
106
+ />
107
  </Routes>
108
  );
109
  }
 
115
  </AuthProvider>
116
  );
117
  }
 
frontend/src/Layout.jsx CHANGED
@@ -1,184 +1,3 @@
1
- <<<<<<< HEAD
2
- // frontend/src/Layout.jsx
3
-
4
- import React, { useState } from "react";
5
- import { Link } from "react-router-dom";
6
- import { createPageUrl } from "./utils";
7
- import {
8
- LayoutDashboard,
9
- History as HistoryIcon,
10
- ChevronLeft,
11
- Sparkles,
12
- LogOut,
13
- User,
14
- } from "lucide-react";
15
- import { cn } from "@/lib/utils";
16
- import { useAuth } from "./contexts/AuthContext";
17
-
18
- // Import logo - Vite will process this and handle the path correctly
19
- // For production, the logo should be in frontend/public/logo.png
20
- // Vite will copy it to dist/logo.png during build
21
- const logoPath = "/logo.png";
22
-
23
- export default function Layout({ children, currentPageName }) {
24
- const [collapsed, setCollapsed] = useState(false);
25
- const { user, logout } = useAuth();
26
-
27
- const navItems = [
28
- { name: "Dashboard", icon: LayoutDashboard, page: "Dashboard" },
29
- { name: "History", icon: HistoryIcon, page: "History" },
30
- ];
31
-
32
- return (
33
- <div className="min-h-screen bg-[#FAFAFA] flex">
34
- {/* Sidebar */}
35
- <aside
36
- className={cn(
37
- "fixed left-0 top-0 h-screen bg-white border-r border-slate-200/80 z-50 transition-all duration-300 ease-out flex flex-col",
38
- collapsed ? "w-[72px]" : "w-[260px]"
39
- )}
40
- >
41
- {/* Logo */}
42
- <div
43
- className={cn(
44
- "h-16 flex items-center border-b border-slate-100 px-4",
45
- collapsed ? "justify-center" : "justify-between"
46
- )}
47
- >
48
- <Link to={createPageUrl("Dashboard")} className="flex items-center gap-3">
49
- <div className="h-9 w-9 flex items-center justify-center flex-shrink-0">
50
- <img
51
- src={logoPath}
52
- alt="EZOFIS AI Logo"
53
- className="h-full w-full object-contain"
54
- onError={(e) => {
55
- // Fallback: hide image and show placeholder if logo not found
56
- e.target.style.display = 'none';
57
- }}
58
- />
59
- </div>
60
- {!collapsed && (
61
- <div className="flex flex-col">
62
- <span className="font-semibold text-slate-900 tracking-tight">EZOFIS AI</span>
63
- <span className="text-[10px] text-slate-400 font-medium tracking-wide uppercase">
64
- VRP Intelligence
65
- </span>
66
- </div>
67
- )}
68
- </Link>
69
- {!collapsed && (
70
- <button
71
- onClick={() => setCollapsed(true)}
72
- className="h-7 w-7 rounded-lg hover:bg-slate-100 flex items-center justify-center text-slate-400 hover:text-slate-600 transition-colors"
73
- >
74
- <ChevronLeft className="h-4 w-4" />
75
- </button>
76
- )}
77
- </div>
78
-
79
- {/* Navigation */}
80
- <nav className="flex-1 p-3 space-y-1">
81
- {navItems.map((item) => {
82
- const isActive = currentPageName === item.page;
83
- return (
84
- <Link
85
- key={item.name}
86
- to={createPageUrl(item.page)}
87
- className={cn(
88
- "flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200 group",
89
- isActive
90
- ? "bg-gradient-to-r from-indigo-50 to-violet-50 text-indigo-600"
91
- : "text-slate-500 hover:bg-slate-50 hover:text-slate-700"
92
- )}
93
- >
94
- <item.icon
95
- className={cn(
96
- "h-5 w-5 flex-shrink-0",
97
- isActive ? "text-indigo-600" : "text-slate-400 group-hover:text-slate-600"
98
- )}
99
- />
100
- {!collapsed && (
101
- <span className="font-medium text-sm">{item.name}</span>
102
- )}
103
- </Link>
104
- );
105
- })}
106
- </nav>
107
-
108
- {/* Collapse Toggle (when collapsed) */}
109
- {collapsed && (
110
- <button
111
- onClick={() => setCollapsed(false)}
112
- className="m-3 h-10 rounded-xl bg-slate-50 hover:bg-slate-100 flex items-center justify-center text-slate-400 hover:text-slate-600 transition-colors"
113
- >
114
- <ChevronLeft className="h-4 w-4 rotate-180" />
115
- </button>
116
- )}
117
-
118
- {/* Pro Badge */}
119
- {!collapsed && (
120
- <div className="p-3">
121
- <div className="p-4 rounded-2xl bg-gradient-to-br from-slate-900 to-slate-800 text-white">
122
- <div className="flex items-center gap-2 mb-2">
123
- <Sparkles className="h-4 w-4 text-amber-400" />
124
- <span className="text-xs font-semibold tracking-wide">DEPLOY CUSTOM AGENT</span>
125
- </div>
126
- <p className="text-xs text-slate-400 mb-3">
127
- Batch extractions, custom model, field mapping, complex lineitems, tables, workflows, &amp; API access
128
- </p>
129
- <button className="w-full py-2 px-3 rounded-lg bg-white text-slate-900 text-sm font-semibold hover:bg-slate-100 transition-colors">
130
- Book a Custom Demo
131
- </button>
132
- </div>
133
- </div>
134
- )}
135
-
136
- {/* User Profile */}
137
- {!collapsed && user && (
138
- <div className="p-3 border-t border-slate-200">
139
- <div className="flex items-center gap-3 p-3 rounded-xl bg-slate-50 hover:bg-slate-100 transition-colors">
140
- {user.picture ? (
141
- <img
142
- src={user.picture}
143
- alt={user.name || user.email}
144
- className="h-10 w-10 rounded-lg object-cover"
145
- />
146
- ) : (
147
- <div className="h-10 w-10 rounded-lg bg-indigo-100 flex items-center justify-center">
148
- <User className="h-5 w-5 text-indigo-600" />
149
- </div>
150
- )}
151
- <div className="flex-1 min-w-0">
152
- <p className="text-sm font-medium text-slate-900 truncate">
153
- {user.name || "User"}
154
- </p>
155
- <p className="text-xs text-slate-500 truncate">{user.email}</p>
156
- </div>
157
- </div>
158
- <button
159
- onClick={logout}
160
- className="mt-2 w-full flex items-center gap-2 px-3 py-2 rounded-xl text-sm text-slate-600 hover:bg-red-50 hover:text-red-600 transition-colors"
161
- >
162
- <LogOut className="h-4 w-4" />
163
- <span>Sign Out</span>
164
- </button>
165
- </div>
166
- )}
167
- </aside>
168
-
169
- {/* Main Content */}
170
- <main
171
- className={cn(
172
- "flex-1 transition-all duration-300",
173
- collapsed ? "ml-[72px]" : "ml-[260px]"
174
- )}
175
- >
176
- {children}
177
- </main>
178
- </div>
179
- );
180
- }
181
- =======
182
  // frontend/src/Layout.jsx
183
 
184
  import React, { useState } from "react";
@@ -187,6 +6,7 @@ import { createPageUrl } from "./utils";
187
  import {
188
  LayoutDashboard,
189
  History as HistoryIcon,
 
190
  ChevronLeft,
191
  Sparkles,
192
  LogOut,
@@ -207,6 +27,7 @@ export default function Layout({ children, currentPageName }) {
207
  const navItems = [
208
  { name: "Dashboard", icon: LayoutDashboard, page: "Dashboard" },
209
  { name: "History", icon: HistoryIcon, page: "History" },
 
210
  ];
211
 
212
  return (
@@ -358,4 +179,3 @@ export default function Layout({ children, currentPageName }) {
358
  </div>
359
  );
360
  }
361
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  // frontend/src/Layout.jsx
2
 
3
  import React, { useState } from "react";
 
6
  import {
7
  LayoutDashboard,
8
  History as HistoryIcon,
9
+ Key,
10
  ChevronLeft,
11
  Sparkles,
12
  LogOut,
 
27
  const navItems = [
28
  { name: "Dashboard", icon: LayoutDashboard, page: "Dashboard" },
29
  { name: "History", icon: HistoryIcon, page: "History" },
30
+ { name: "API Keys", icon: Key, page: "API Keys" },
31
  ];
32
 
33
  return (
 
179
  </div>
180
  );
181
  }
 
frontend/src/pages/APIKeys.jsx ADDED
@@ -0,0 +1,592 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from "react";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import {
4
+ Key,
5
+ Plus,
6
+ Trash2,
7
+ Copy,
8
+ Check,
9
+ Eye,
10
+ EyeOff,
11
+ Code,
12
+ FileText,
13
+ AlertCircle,
14
+ Clock,
15
+ Sparkles,
16
+ ExternalLink,
17
+ } from "lucide-react";
18
+ import { Button } from "@/components/ui/button";
19
+ import { Input } from "@/components/ui/input";
20
+ import { Badge } from "@/components/ui/badge";
21
+ import { cn } from "@/lib/utils";
22
+ import { createAPIKey, listAPIKeys, deleteAPIKey } from "@/services/api";
23
+
24
+ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "https://seth0330-ezofisocr.hf.space";
25
+
26
+ export default function APIKeys() {
27
+ const [apiKeys, setApiKeys] = useState([]);
28
+ const [isLoading, setIsLoading] = useState(true);
29
+ const [error, setError] = useState(null);
30
+ const [showCreateModal, setShowCreateModal] = useState(false);
31
+ const [newKeyName, setNewKeyName] = useState("");
32
+ const [isCreating, setIsCreating] = useState(false);
33
+ const [newlyCreatedKey, setNewlyCreatedKey] = useState(null);
34
+ const [copiedKeyId, setCopiedKeyId] = useState(null);
35
+ const [visibleKeys, setVisibleKeys] = useState(new Set());
36
+
37
+ // Fetch API keys on mount
38
+ useEffect(() => {
39
+ fetchAPIKeys();
40
+ }, []);
41
+
42
+ const fetchAPIKeys = async () => {
43
+ setIsLoading(true);
44
+ setError(null);
45
+ try {
46
+ const response = await listAPIKeys();
47
+ setApiKeys(response.api_keys || []);
48
+ } catch (err) {
49
+ setError(err.message || "Failed to load API keys");
50
+ } finally {
51
+ setIsLoading(false);
52
+ }
53
+ };
54
+
55
+ const handleCreateKey = async () => {
56
+ if (!newKeyName.trim()) {
57
+ setError("Please enter a name for your API key");
58
+ return;
59
+ }
60
+
61
+ setIsCreating(true);
62
+ setError(null);
63
+ try {
64
+ const response = await createAPIKey(newKeyName.trim());
65
+ setNewlyCreatedKey(response.api_key);
66
+ setNewKeyName("");
67
+ setShowCreateModal(false);
68
+ await fetchAPIKeys();
69
+ } catch (err) {
70
+ setError(err.message || "Failed to create API key");
71
+ } finally {
72
+ setIsCreating(false);
73
+ }
74
+ };
75
+
76
+ const handleDeleteKey = async (keyId) => {
77
+ if (!confirm("Are you sure you want to deactivate this API key? This action cannot be undone.")) {
78
+ return;
79
+ }
80
+
81
+ try {
82
+ await deleteAPIKey(keyId);
83
+ await fetchAPIKeys();
84
+ } catch (err) {
85
+ setError(err.message || "Failed to delete API key");
86
+ }
87
+ };
88
+
89
+ const copyToClipboard = (text, keyId = null) => {
90
+ navigator.clipboard.writeText(text);
91
+ if (keyId) {
92
+ setCopiedKeyId(keyId);
93
+ setTimeout(() => setCopiedKeyId(null), 2000);
94
+ }
95
+ };
96
+
97
+ const toggleKeyVisibility = (keyId) => {
98
+ const newVisible = new Set(visibleKeys);
99
+ if (newVisible.has(keyId)) {
100
+ newVisible.delete(keyId);
101
+ } else {
102
+ newVisible.add(keyId);
103
+ }
104
+ setVisibleKeys(newVisible);
105
+ };
106
+
107
+ const formatDate = (dateString) => {
108
+ if (!dateString) return "Never";
109
+ const date = new Date(dateString);
110
+ return date.toLocaleDateString("en-US", {
111
+ year: "numeric",
112
+ month: "short",
113
+ day: "numeric",
114
+ hour: "2-digit",
115
+ minute: "2-digit",
116
+ });
117
+ };
118
+
119
+ return (
120
+ <div className="min-h-screen bg-[#FAFAFA]">
121
+ {/* Header */}
122
+ <header className="bg-white border-b border-slate-200/80 sticky top-0 z-40 h-16">
123
+ <div className="px-8 h-full flex items-center justify-between">
124
+ <div>
125
+ <h1 className="text-xl font-bold text-slate-900 tracking-tight leading-tight">
126
+ API Keys
127
+ </h1>
128
+ <p className="text-sm text-slate-500 leading-tight">
129
+ Manage API keys for external application access
130
+ </p>
131
+ </div>
132
+ <Button
133
+ onClick={() => setShowCreateModal(true)}
134
+ className="h-10 px-6 rounded-xl font-semibold bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700 shadow-lg shadow-indigo-500/25 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-300"
135
+ >
136
+ <Plus className="h-4 w-4 mr-2" />
137
+ Create API Key
138
+ </Button>
139
+ </div>
140
+ </header>
141
+
142
+ {/* Main Content */}
143
+ <div className="p-8">
144
+ {/* Error Message */}
145
+ {error && (
146
+ <motion.div
147
+ initial={{ opacity: 0, y: -10 }}
148
+ animate={{ opacity: 1, y: 0 }}
149
+ className="max-w-4xl mx-auto mb-6"
150
+ >
151
+ <div className="bg-red-50 border border-red-200 rounded-2xl p-4 flex items-start gap-3">
152
+ <AlertCircle className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" />
153
+ <div className="flex-1">
154
+ <h3 className="font-semibold text-red-900 mb-1">Error</h3>
155
+ <p className="text-sm text-red-700">{error}</p>
156
+ </div>
157
+ <button
158
+ onClick={() => setError(null)}
159
+ className="text-red-400 hover:text-red-600 transition-colors"
160
+ >
161
+ ×
162
+ </button>
163
+ </div>
164
+ </motion.div>
165
+ )}
166
+
167
+ {/* Success Message for New Key */}
168
+ {newlyCreatedKey && (
169
+ <motion.div
170
+ initial={{ opacity: 0, y: -10 }}
171
+ animate={{ opacity: 1, y: 0 }}
172
+ className="max-w-4xl mx-auto mb-6"
173
+ >
174
+ <div className="bg-emerald-50 border border-emerald-200 rounded-2xl p-6">
175
+ <div className="flex items-start gap-3 mb-4">
176
+ <div className="h-10 w-10 rounded-xl bg-emerald-100 flex items-center justify-center flex-shrink-0">
177
+ <Check className="h-5 w-5 text-emerald-600" />
178
+ </div>
179
+ <div className="flex-1">
180
+ <h3 className="font-semibold text-emerald-900 mb-1">
181
+ API Key Created Successfully!
182
+ </h3>
183
+ <p className="text-sm text-emerald-700 mb-4">
184
+ ⚠️ Store this key securely - it will not be shown again.
185
+ </p>
186
+ <div className="bg-white rounded-xl p-4 border border-emerald-200">
187
+ <div className="flex items-center gap-2 mb-2">
188
+ <Key className="h-4 w-4 text-slate-500" />
189
+ <span className="text-xs font-medium text-slate-500 uppercase tracking-wide">
190
+ Your API Key
191
+ </span>
192
+ </div>
193
+ <div className="flex items-center gap-2">
194
+ <code className="flex-1 font-mono text-sm text-slate-900 break-all">
195
+ {newlyCreatedKey}
196
+ </code>
197
+ <Button
198
+ size="sm"
199
+ variant="outline"
200
+ onClick={() => copyToClipboard(newlyCreatedKey)}
201
+ className="flex-shrink-0"
202
+ >
203
+ {copiedKeyId === "new" ? (
204
+ <Check className="h-4 w-4 text-emerald-600" />
205
+ ) : (
206
+ <Copy className="h-4 w-4" />
207
+ )}
208
+ </Button>
209
+ </div>
210
+ </div>
211
+ </div>
212
+ </div>
213
+ <Button
214
+ onClick={() => setNewlyCreatedKey(null)}
215
+ className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
216
+ >
217
+ I've Saved My Key
218
+ </Button>
219
+ </div>
220
+ </motion.div>
221
+ )}
222
+
223
+ {/* API Keys List */}
224
+ <div className="max-w-4xl mx-auto">
225
+ {isLoading ? (
226
+ <div className="bg-white rounded-2xl border border-slate-200 p-12 text-center">
227
+ <div className="h-12 w-12 mx-auto rounded-2xl bg-indigo-100 flex items-center justify-center mb-4 animate-pulse">
228
+ <Key className="h-6 w-6 text-indigo-600" />
229
+ </div>
230
+ <p className="text-slate-600">Loading API keys...</p>
231
+ </div>
232
+ ) : apiKeys.length === 0 ? (
233
+ <motion.div
234
+ initial={{ opacity: 0, y: 20 }}
235
+ animate={{ opacity: 1, y: 0 }}
236
+ className="bg-white rounded-2xl border border-slate-200 p-12 text-center"
237
+ >
238
+ <div className="h-16 w-16 mx-auto rounded-2xl bg-slate-100 flex items-center justify-center mb-4">
239
+ <Key className="h-8 w-8 text-slate-400" />
240
+ </div>
241
+ <h3 className="text-lg font-semibold text-slate-900 mb-2">
242
+ No API Keys Yet
243
+ </h3>
244
+ <p className="text-slate-500 mb-6">
245
+ Create your first API key to start using the API from external applications
246
+ </p>
247
+ <Button
248
+ onClick={() => setShowCreateModal(true)}
249
+ className="h-11 px-6 rounded-xl font-semibold bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700"
250
+ >
251
+ <Plus className="h-4 w-4 mr-2" />
252
+ Create Your First API Key
253
+ </Button>
254
+ </motion.div>
255
+ ) : (
256
+ <div className="space-y-4">
257
+ {apiKeys.map((key) => (
258
+ <motion.div
259
+ key={key.id}
260
+ initial={{ opacity: 0, y: 20 }}
261
+ animate={{ opacity: 1, y: 0 }}
262
+ className="bg-white rounded-2xl border border-slate-200 p-6 hover:shadow-lg transition-shadow"
263
+ >
264
+ <div className="flex items-start justify-between mb-4">
265
+ <div className="flex-1">
266
+ <div className="flex items-center gap-3 mb-2">
267
+ <Key className="h-5 w-5 text-indigo-600" />
268
+ <h3 className="font-semibold text-slate-900">{key.name}</h3>
269
+ {key.is_active ? (
270
+ <Badge className="bg-emerald-50 text-emerald-700 border-emerald-200">
271
+ Active
272
+ </Badge>
273
+ ) : (
274
+ <Badge className="bg-slate-100 text-slate-600 border-slate-200">
275
+ Inactive
276
+ </Badge>
277
+ )}
278
+ </div>
279
+ <div className="ml-8 space-y-2">
280
+ <div className="flex items-center gap-2 text-sm text-slate-500">
281
+ <span className="font-mono">{key.key_prefix}</span>
282
+ <Button
283
+ size="sm"
284
+ variant="ghost"
285
+ onClick={() => toggleKeyVisibility(key.id)}
286
+ className="h-6 px-2"
287
+ >
288
+ {visibleKeys.has(key.id) ? (
289
+ <EyeOff className="h-3 w-3" />
290
+ ) : (
291
+ <Eye className="h-3 w-3" />
292
+ )}
293
+ </Button>
294
+ </div>
295
+ <div className="flex items-center gap-4 text-xs text-slate-400">
296
+ <div className="flex items-center gap-1">
297
+ <Clock className="h-3 w-3" />
298
+ <span>Created: {formatDate(key.created_at)}</span>
299
+ </div>
300
+ {key.last_used_at && (
301
+ <div className="flex items-center gap-1">
302
+ <Sparkles className="h-3 w-3" />
303
+ <span>Last used: {formatDate(key.last_used_at)}</span>
304
+ </div>
305
+ )}
306
+ </div>
307
+ </div>
308
+ </div>
309
+ <Button
310
+ size="sm"
311
+ variant="ghost"
312
+ onClick={() => handleDeleteKey(key.id)}
313
+ className="text-red-600 hover:text-red-700 hover:bg-red-50"
314
+ disabled={!key.is_active}
315
+ >
316
+ <Trash2 className="h-4 w-4" />
317
+ </Button>
318
+ </div>
319
+ </motion.div>
320
+ ))}
321
+ </div>
322
+ )}
323
+ </div>
324
+
325
+ {/* API Usage Guide */}
326
+ <motion.div
327
+ initial={{ opacity: 0, y: 20 }}
328
+ animate={{ opacity: 1, y: 0 }}
329
+ transition={{ delay: 0.2 }}
330
+ className="max-w-4xl mx-auto mt-12"
331
+ >
332
+ <div className="bg-white rounded-2xl border border-slate-200 p-8">
333
+ <div className="flex items-center gap-3 mb-6">
334
+ <div className="h-12 w-12 rounded-xl bg-indigo-50 flex items-center justify-center">
335
+ <Code className="h-6 w-6 text-indigo-600" />
336
+ </div>
337
+ <div>
338
+ <h2 className="text-xl font-bold text-slate-900">How to Use API Keys</h2>
339
+ <p className="text-sm text-slate-500">
340
+ Integrate document parsing into your external applications
341
+ </p>
342
+ </div>
343
+ </div>
344
+
345
+ <div className="space-y-6">
346
+ {/* Python Example */}
347
+ <div className="bg-slate-50 rounded-xl p-6 border border-slate-200">
348
+ <div className="flex items-center gap-2 mb-4">
349
+ <FileText className="h-5 w-5 text-slate-600" />
350
+ <h3 className="font-semibold text-slate-900">Python Example</h3>
351
+ </div>
352
+ <pre className="bg-slate-900 text-slate-100 rounded-lg p-4 overflow-x-auto text-sm">
353
+ <code>{`import requests
354
+
355
+ API_URL = "${API_BASE_URL}"
356
+ API_KEY = "sk_live_YOUR_API_KEY_HERE"
357
+
358
+ def extract_document(file_path, key_fields=None):
359
+ with open(file_path, 'rb') as f:
360
+ files = {'file': f}
361
+ data = {}
362
+ if key_fields:
363
+ data['key_fields'] = key_fields
364
+
365
+ response = requests.post(
366
+ f"{API_URL}/api/extract",
367
+ headers={"X-API-Key": API_KEY},
368
+ files=files,
369
+ data=data
370
+ )
371
+ return response.json()
372
+
373
+ # Usage
374
+ result = extract_document("invoice.pdf",
375
+ key_fields="Invoice Number,Invoice Date")
376
+ print(result)`}</code>
377
+ </pre>
378
+ <Button
379
+ size="sm"
380
+ variant="outline"
381
+ onClick={() => copyToClipboard(`import requests
382
+
383
+ API_URL = "${API_BASE_URL}"
384
+ API_KEY = "sk_live_YOUR_API_KEY_HERE"
385
+
386
+ def extract_document(file_path, key_fields=None):
387
+ with open(file_path, 'rb') as f:
388
+ files = {'file': f}
389
+ data = {}
390
+ if key_fields:
391
+ data['key_fields'] = key_fields
392
+
393
+ response = requests.post(
394
+ f"{API_URL}/api/extract",
395
+ headers={"X-API-Key": API_KEY},
396
+ files=files,
397
+ data=data
398
+ )
399
+ return response.json()
400
+
401
+ # Usage
402
+ result = extract_document("invoice.pdf",
403
+ key_fields="Invoice Number,Invoice Date")
404
+ print(result)`)}
405
+ className="mt-3"
406
+ >
407
+ <Copy className="h-3 w-3 mr-2" />
408
+ Copy Code
409
+ </Button>
410
+ </div>
411
+
412
+ {/* cURL Example */}
413
+ <div className="bg-slate-50 rounded-xl p-6 border border-slate-200">
414
+ <div className="flex items-center gap-2 mb-4">
415
+ <FileText className="h-5 w-5 text-slate-600" />
416
+ <h3 className="font-semibold text-slate-900">cURL Example</h3>
417
+ </div>
418
+ <pre className="bg-slate-900 text-slate-100 rounded-lg p-4 overflow-x-auto text-sm">
419
+ <code>{`curl -X POST ${API_BASE_URL}/api/extract \\
420
+ -H "X-API-Key: sk_live_YOUR_API_KEY_HERE" \\
421
+ -F "file=@document.pdf" \\
422
+ -F "key_fields=Invoice Number,Invoice Date,Total Amount"`}</code>
423
+ </pre>
424
+ <Button
425
+ size="sm"
426
+ variant="outline"
427
+ onClick={() => copyToClipboard(`curl -X POST ${API_BASE_URL}/api/extract \\
428
+ -H "X-API-Key: sk_live_YOUR_API_KEY_HERE" \\
429
+ -F "file=@document.pdf" \\
430
+ -F "key_fields=Invoice Number,Invoice Date,Total Amount"`)}
431
+ className="mt-3"
432
+ >
433
+ <Copy className="h-3 w-3 mr-2" />
434
+ Copy Code
435
+ </Button>
436
+ </div>
437
+
438
+ {/* JavaScript Example */}
439
+ <div className="bg-slate-50 rounded-xl p-6 border border-slate-200">
440
+ <div className="flex items-center gap-2 mb-4">
441
+ <FileText className="h-5 w-5 text-slate-600" />
442
+ <h3 className="font-semibold text-slate-900">JavaScript/Node.js Example</h3>
443
+ </div>
444
+ <pre className="bg-slate-900 text-slate-100 rounded-lg p-4 overflow-x-auto text-sm">
445
+ <code>{`const FormData = require('form-data');
446
+ const fs = require('fs');
447
+ const axios = require('axios');
448
+
449
+ const API_URL = '${API_BASE_URL}';
450
+ const API_KEY = 'sk_live_YOUR_API_KEY_HERE';
451
+
452
+ async function extractDocument(filePath, keyFields = null) {
453
+ const form = new FormData();
454
+ form.append('file', fs.createReadStream(filePath));
455
+ if (keyFields) {
456
+ form.append('key_fields', keyFields);
457
+ }
458
+
459
+ const response = await axios.post(
460
+ \`\${API_URL}/api/extract\`,
461
+ form,
462
+ {
463
+ headers: {
464
+ 'X-API-Key': API_KEY,
465
+ ...form.getHeaders()
466
+ }
467
+ }
468
+ );
469
+ return response.data;
470
+ }
471
+
472
+ // Usage
473
+ extractDocument('invoice.pdf', 'Invoice Number,Invoice Date')
474
+ .then(result => console.log(result));`}</code>
475
+ </pre>
476
+ <Button
477
+ size="sm"
478
+ variant="outline"
479
+ onClick={() => copyToClipboard(`const FormData = require('form-data');
480
+ const fs = require('fs');
481
+ const axios = require('axios');
482
+
483
+ const API_URL = '${API_BASE_URL}';
484
+ const API_KEY = 'sk_live_YOUR_API_KEY_HERE';
485
+
486
+ async function extractDocument(filePath, keyFields = null) {
487
+ const form = new FormData();
488
+ form.append('file', fs.createReadStream(filePath));
489
+ if (keyFields) {
490
+ form.append('key_fields', keyFields);
491
+ }
492
+
493
+ const response = await axios.post(
494
+ \`\${API_URL}/api/extract\`,
495
+ form,
496
+ {
497
+ headers: {
498
+ 'X-API-Key': API_KEY,
499
+ ...form.getHeaders()
500
+ }
501
+ }
502
+ );
503
+ return response.data;
504
+ }
505
+
506
+ // Usage
507
+ extractDocument('invoice.pdf', 'Invoice Number,Invoice Date')
508
+ .then(result => console.log(result));`)}
509
+ className="mt-3"
510
+ >
511
+ <Copy className="h-3 w-3 mr-2" />
512
+ Copy Code
513
+ </Button>
514
+ </div>
515
+ </div>
516
+
517
+ <div className="mt-6 p-4 bg-indigo-50 rounded-xl border border-indigo-200">
518
+ <div className="flex items-start gap-3">
519
+ <AlertCircle className="h-5 w-5 text-indigo-600 flex-shrink-0 mt-0.5" />
520
+ <div className="flex-1">
521
+ <h4 className="font-semibold text-indigo-900 mb-1">API Endpoint</h4>
522
+ <p className="text-sm text-indigo-700 mb-2">
523
+ <code className="bg-white px-2 py-1 rounded text-indigo-900">
524
+ POST {API_BASE_URL}/api/extract
525
+ </code>
526
+ </p>
527
+ <p className="text-xs text-indigo-600">
528
+ • Max file size: 4 MB • Supported formats: PDF, PNG, JPEG, TIFF
529
+ </p>
530
+ </div>
531
+ </div>
532
+ </div>
533
+ </div>
534
+ </motion.div>
535
+ </div>
536
+
537
+ {/* Create API Key Modal */}
538
+ <AnimatePresence>
539
+ {showCreateModal && (
540
+ <motion.div
541
+ initial={{ opacity: 0 }}
542
+ animate={{ opacity: 1 }}
543
+ exit={{ opacity: 0 }}
544
+ className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4"
545
+ onClick={() => !isCreating && setShowCreateModal(false)}
546
+ >
547
+ <motion.div
548
+ initial={{ scale: 0.95, opacity: 0 }}
549
+ animate={{ scale: 1, opacity: 1 }}
550
+ exit={{ scale: 0.95, opacity: 0 }}
551
+ onClick={(e) => e.stopPropagation()}
552
+ className="bg-white rounded-2xl p-6 max-w-md w-full shadow-2xl"
553
+ >
554
+ <h2 className="text-xl font-bold text-slate-900 mb-2">Create New API Key</h2>
555
+ <p className="text-sm text-slate-500 mb-6">
556
+ Give your API key a descriptive name to identify it later
557
+ </p>
558
+ <Input
559
+ placeholder="e.g., Production API, Test Environment"
560
+ value={newKeyName}
561
+ onChange={(e) => setNewKeyName(e.target.value)}
562
+ className="mb-6"
563
+ onKeyPress={(e) => e.key === "Enter" && handleCreateKey()}
564
+ />
565
+ <div className="flex gap-3">
566
+ <Button
567
+ variant="outline"
568
+ onClick={() => {
569
+ setShowCreateModal(false);
570
+ setNewKeyName("");
571
+ }}
572
+ disabled={isCreating}
573
+ className="flex-1"
574
+ >
575
+ Cancel
576
+ </Button>
577
+ <Button
578
+ onClick={handleCreateKey}
579
+ disabled={isCreating || !newKeyName.trim()}
580
+ className="flex-1 bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700"
581
+ >
582
+ {isCreating ? "Creating..." : "Create Key"}
583
+ </Button>
584
+ </div>
585
+ </motion.div>
586
+ </motion.div>
587
+ )}
588
+ </AnimatePresence>
589
+ </div>
590
+ );
591
+ }
592
+
frontend/src/services/api.js CHANGED
@@ -1,178 +1,3 @@
1
- <<<<<<< HEAD
2
- /**
3
- * API service for communicating with the FastAPI backend
4
- */
5
-
6
- const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "";
7
-
8
- /**
9
- * Get authorization headers with token
10
- */
11
- function getAuthHeaders() {
12
- const token = localStorage.getItem("auth_token");
13
- return token ? { Authorization: `Bearer ${token}` } : {};
14
- }
15
-
16
- /**
17
- * Extract data from a document
18
- * @param {File} file - The file to extract data from
19
- * @param {string} keyFields - Optional comma-separated list of fields to extract
20
- * @returns {Promise<Object>} Extraction result with fields, confidence, etc.
21
- */
22
- export async function extractDocument(file, keyFields = "") {
23
- const formData = new FormData();
24
- formData.append("file", file);
25
- if (keyFields && keyFields.trim()) {
26
- formData.append("key_fields", keyFields.trim());
27
- }
28
-
29
- const response = await fetch(`${API_BASE_URL}/api/extract`, {
30
- method: "POST",
31
- headers: getAuthHeaders(),
32
- body: formData,
33
- });
34
-
35
- if (!response.ok) {
36
- const errorData = await response.json().catch(() => ({
37
- error: `HTTP ${response.status}: ${response.statusText}`,
38
- }));
39
- throw new Error(errorData.error || errorData.detail || "Extraction failed");
40
- }
41
-
42
- return await response.json();
43
- }
44
-
45
- /**
46
- * Get extraction history
47
- * @returns {Promise<Array>} Array of extraction records
48
- */
49
- export async function getHistory() {
50
- const response = await fetch(`${API_BASE_URL}/api/history`, {
51
- headers: getAuthHeaders(),
52
- });
53
-
54
- if (!response.ok) {
55
- const errorData = await response.json().catch(() => ({
56
- error: `HTTP ${response.status}: ${response.statusText}`,
57
- }));
58
- throw new Error(errorData.error || errorData.detail || "Failed to fetch history");
59
- }
60
-
61
- return await response.json();
62
- }
63
-
64
- /**
65
- * Get a specific extraction by ID with full fields data
66
- * @param {number} extractionId - The extraction ID
67
- * @returns {Promise<Object>} Extraction result with fields
68
- */
69
- export async function getExtractionById(extractionId) {
70
- const response = await fetch(`${API_BASE_URL}/api/extraction/${extractionId}`, {
71
- headers: getAuthHeaders(),
72
- });
73
-
74
- if (!response.ok) {
75
- const errorData = await response.json().catch(() => ({
76
- error: `HTTP ${response.status}: ${response.statusText}`,
77
- }));
78
- throw new Error(errorData.error || errorData.detail || "Failed to fetch extraction");
79
- }
80
-
81
- return await response.json();
82
- }
83
-
84
- /**
85
- * Create a shareable link for an extraction
86
- * @param {number} extractionId - The extraction ID to share
87
- * @returns {Promise<Object>} Share link result with share_link
88
- */
89
- export async function createShareLink(extractionId) {
90
- const response = await fetch(`${API_BASE_URL}/api/share/link`, {
91
- method: "POST",
92
- headers: {
93
- "Content-Type": "application/json",
94
- ...getAuthHeaders(),
95
- },
96
- body: JSON.stringify({
97
- extraction_id: extractionId,
98
- }),
99
- });
100
-
101
- if (!response.ok) {
102
- const errorData = await response.json().catch(() => ({
103
- error: `HTTP ${response.status}: ${response.statusText}`,
104
- }));
105
- throw new Error(errorData.error || errorData.detail || "Failed to create share link");
106
- }
107
-
108
- return await response.json();
109
- }
110
-
111
- /**
112
- * Share an extraction with another user(s)
113
- * @param {number} extractionId - The extraction ID to share
114
- * @param {string|string[]} recipientEmails - Recipient email address(es) - can be a single email or array of emails
115
- * @returns {Promise<Object>} Share result
116
- */
117
- export async function shareExtraction(extractionId, recipientEmails) {
118
- // Ensure recipient_emails is always an array
119
- const emailsArray = Array.isArray(recipientEmails) ? recipientEmails : [recipientEmails];
120
-
121
- const response = await fetch(`${API_BASE_URL}/api/share`, {
122
- method: "POST",
123
- headers: {
124
- "Content-Type": "application/json",
125
- ...getAuthHeaders(),
126
- },
127
- body: JSON.stringify({
128
- extraction_id: extractionId,
129
- recipient_emails: emailsArray,
130
- }),
131
- });
132
-
133
- if (!response.ok) {
134
- const errorData = await response.json().catch(() => ({
135
- error: `HTTP ${response.status}: ${response.statusText}`,
136
- }));
137
- throw new Error(errorData.error || errorData.detail || "Failed to share extraction");
138
- }
139
-
140
- return await response.json();
141
- }
142
-
143
- /**
144
- * Access a shared extraction by token
145
- * @param {string} token - Share token
146
- * @returns {Promise<Object>} Share access result with extraction_id
147
- */
148
- export async function accessSharedExtraction(token) {
149
- const response = await fetch(`${API_BASE_URL}/api/share/${token}`, {
150
- headers: getAuthHeaders(),
151
- });
152
-
153
- if (!response.ok) {
154
- const errorData = await response.json().catch(() => ({
155
- error: `HTTP ${response.status}: ${response.statusText}`,
156
- }));
157
- throw new Error(errorData.error || errorData.detail || "Failed to access shared extraction");
158
- }
159
-
160
- return await response.json();
161
- }
162
-
163
- /**
164
- * Health check endpoint
165
- * @returns {Promise<Object>} Status object
166
- */
167
- export async function ping() {
168
- const response = await fetch(`${API_BASE_URL}/ping`);
169
- if (!response.ok) {
170
- throw new Error("Backend is not available");
171
- }
172
- return await response.json();
173
- }
174
-
175
- =======
176
  /**
177
  * API service for communicating with the FastAPI backend
178
  */
@@ -346,4 +171,67 @@ export async function ping() {
346
  return await response.json();
347
  }
348
 
349
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /**
2
  * API service for communicating with the FastAPI backend
3
  */
 
171
  return await response.json();
172
  }
173
 
174
+ /**
175
+ * Create a new API key
176
+ * @param {string} name - User-friendly name for the API key
177
+ * @returns {Promise<Object>} API key creation result with api_key
178
+ */
179
+ export async function createAPIKey(name) {
180
+ const response = await fetch(`${API_BASE_URL}/api/auth/api-key/create`, {
181
+ method: "POST",
182
+ headers: {
183
+ "Content-Type": "application/json",
184
+ ...getAuthHeaders(),
185
+ },
186
+ body: JSON.stringify({ name }),
187
+ });
188
+
189
+ if (!response.ok) {
190
+ const errorData = await response.json().catch(() => ({
191
+ error: `HTTP ${response.status}: ${response.statusText}`,
192
+ }));
193
+ throw new Error(errorData.error || errorData.detail || "Failed to create API key");
194
+ }
195
+
196
+ return await response.json();
197
+ }
198
+
199
+ /**
200
+ * List all API keys for the current user
201
+ * @returns {Promise<Object>} API keys list with api_keys array
202
+ */
203
+ export async function listAPIKeys() {
204
+ const response = await fetch(`${API_BASE_URL}/api/auth/api-keys`, {
205
+ headers: getAuthHeaders(),
206
+ });
207
+
208
+ if (!response.ok) {
209
+ const errorData = await response.json().catch(() => ({
210
+ error: `HTTP ${response.status}: ${response.statusText}`,
211
+ }));
212
+ throw new Error(errorData.error || errorData.detail || "Failed to fetch API keys");
213
+ }
214
+
215
+ return await response.json();
216
+ }
217
+
218
+ /**
219
+ * Delete (deactivate) an API key
220
+ * @param {number} keyId - The API key ID to delete
221
+ * @returns {Promise<Object>} Deletion result
222
+ */
223
+ export async function deleteAPIKey(keyId) {
224
+ const response = await fetch(`${API_BASE_URL}/api/auth/api-key/${keyId}`, {
225
+ method: "DELETE",
226
+ headers: getAuthHeaders(),
227
+ });
228
+
229
+ if (!response.ok) {
230
+ const errorData = await response.json().catch(() => ({
231
+ error: `HTTP ${response.status}: ${response.statusText}`,
232
+ }));
233
+ throw new Error(errorData.error || errorData.detail || "Failed to delete API key");
234
+ }
235
+
236
+ return await response.json();
237
+ }
frontend/src/utils.js CHANGED
@@ -4,5 +4,6 @@ export function createPageUrl(pageName) {
4
  if (!pageName) return "/";
5
  const lower = pageName.toLowerCase();
6
  if (lower === "dashboard") return "/";
7
- return `/${lower}`;
 
8
  }
 
4
  if (!pageName) return "/";
5
  const lower = pageName.toLowerCase();
6
  if (lower === "dashboard") return "/";
7
+ if (lower === "api keys") return "/api-keys";
8
+ return `/${lower.replace(/\s+/g, "-")}`;
9
  }