@kyro-cms/admin 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -51
- package/package.json +52 -5
- package/src/collections/auth/index.ts +2 -2
- package/src/collections/portfolio/index.ts +343 -0
- package/src/components/ActionBar.tsx +153 -16
- package/src/components/Admin.tsx +136 -27
- package/src/components/ApiExplorer.tsx +325 -0
- package/src/components/ApiKeysManager.tsx +563 -0
- package/src/components/AuditLogsPage.tsx +664 -0
- package/src/components/AutoForm.tsx +1417 -661
- package/src/components/BrandingHub.tsx +267 -0
- package/src/components/BulkActionsBar.tsx +3 -3
- package/src/components/CreateView.tsx +3 -3
- package/src/components/Dashboard.tsx +393 -0
- package/src/components/DetailView.tsx +199 -57
- package/src/components/DeveloperCenter.tsx +403 -0
- package/src/components/EnhancedListView.tsx +786 -0
- package/src/components/GraphQLExplorer.tsx +675 -0
- package/src/components/GraphQLPlayground.tsx +627 -0
- package/src/components/ListView.tsx +191 -53
- package/src/components/MediaGallery.tsx +1569 -0
- package/src/components/Modal.tsx +149 -0
- package/src/components/RestPlayground.tsx +951 -0
- package/src/components/Sidebar.astro +237 -0
- package/src/components/UserManagement.tsx +204 -0
- package/src/components/VersionHistoryPanel.tsx +3 -3
- package/src/components/WebhookManager.tsx +608 -0
- package/src/components/blocks/AccordionBlock.tsx +97 -0
- package/src/components/blocks/ArrayBlock.tsx +75 -0
- package/src/components/blocks/BlockEditModal.MARKER +12 -0
- package/src/components/blocks/BlockEditModal.tsx +774 -0
- package/src/components/blocks/ButtonBlock.tsx +165 -0
- package/src/components/blocks/ChildBlocksTree.tsx +551 -0
- package/src/components/blocks/CodeBlock.tsx +66 -0
- package/src/components/blocks/ColumnsBlock.tsx +151 -0
- package/src/components/blocks/DividerBlock.tsx +43 -0
- package/src/components/blocks/FileBlock.tsx +64 -0
- package/src/components/blocks/HeadingBlock.tsx +81 -0
- package/src/components/blocks/HeroBlock.tsx +157 -0
- package/src/components/blocks/ImageBlock.tsx +83 -0
- package/src/components/blocks/LinkBlock.tsx +71 -0
- package/src/components/blocks/ListBlock.tsx +39 -0
- package/src/components/blocks/ParagraphBlock.tsx +61 -0
- package/src/components/blocks/RelationshipBlock.tsx +279 -0
- package/src/components/blocks/VStackBlock.tsx +75 -0
- package/src/components/blocks/VideoBlock.tsx +45 -0
- package/src/components/blocks/index.ts +10 -0
- package/src/components/fields/BlocksField.tsx +323 -0
- package/src/components/fields/CheckboxField.tsx +15 -9
- package/src/components/fields/CodeField.tsx +234 -0
- package/src/components/fields/DateField.tsx +38 -11
- package/src/components/fields/EditorClient.tsx +271 -0
- package/src/components/fields/FileField.tsx +390 -0
- package/src/components/fields/HybridContentField.tsx +109 -0
- package/src/components/fields/ImageField.tsx +429 -0
- package/src/components/fields/JSONField.tsx +361 -0
- package/src/components/fields/MarkdownField.tsx +282 -0
- package/src/components/fields/NumberField.tsx +42 -12
- package/src/components/fields/PortableTextField.tsx +143 -0
- package/src/components/fields/PortableTextRenderer.tsx +68 -0
- package/src/components/fields/RelationshipField.tsx +231 -59
- package/src/components/fields/SelectField.tsx +25 -15
- package/src/components/fields/TextField.tsx +45 -14
- package/src/components/fields/extensions/blockComponents.tsx +237 -0
- package/src/components/fields/extensions/blocksStore.ts +273 -0
- package/src/components/fields/index.ts +13 -0
- package/src/components/index.ts +1 -2
- package/src/components/layout/Header.tsx +2 -2
- package/src/components/layout/Layout.tsx +2 -2
- package/src/components/ui/Badge.tsx +9 -4
- package/src/components/ui/BlockDrawer.tsx +79 -0
- package/src/components/ui/Button.tsx +1 -1
- package/src/components/ui/CommandPalette.tsx +362 -0
- package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
- package/src/components/ui/Dropdown.tsx +1 -1
- package/src/components/ui/Modal.tsx +37 -12
- package/src/components/ui/PromptModal.tsx +94 -0
- package/src/components/ui/SlidePanel.tsx +43 -16
- package/src/components/ui/Toast.tsx +80 -14
- package/src/env.d.ts +16 -0
- package/src/env.ts +20 -0
- package/src/index.ts +0 -1
- package/src/layouts/AdminLayout.astro +164 -170
- package/src/layouts/AuthLayout.astro +50 -0
- package/src/lib/MediaService.ts +541 -0
- package/src/lib/auth/sqlite-adapter.ts +319 -0
- package/src/lib/config.ts +22 -6
- package/src/lib/dataStore.ts +132 -74
- package/src/lib/db/adapter.ts +54 -0
- package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
- package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
- package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
- package/src/lib/db/index.ts +449 -0
- package/src/lib/db/mongodb-adapter.ts +207 -0
- package/src/lib/db/mongodb-auth-adapter.ts +305 -0
- package/src/lib/db/schema/mysql-auth.ts +113 -0
- package/src/lib/db/schema/mysql-content.ts +20 -0
- package/src/lib/db/schema/postgres-auth.ts +116 -0
- package/src/lib/db/schema/postgres-content.ts +35 -0
- package/src/lib/db/schema/postgres-media.ts +52 -0
- package/src/lib/db/schema/postgres-settings.ts +11 -0
- package/src/lib/db/schema/sqlite-auth.ts +112 -0
- package/src/lib/db/schema/sqlite-content.ts +20 -0
- package/src/lib/graphql/index.ts +1 -0
- package/src/lib/graphql/schema.ts +443 -0
- package/src/lib/rate-limit.ts +267 -0
- package/src/lib/storage.ts +374 -0
- package/src/lib/store.ts +85 -0
- package/src/middleware.ts +116 -28
- package/src/pages/[collection]/[id].astro +178 -122
- package/src/pages/[collection]/index.astro +24 -156
- package/src/pages/admin/api-explorer.astro +98 -0
- package/src/pages/admin/graphql-explorer.astro +40 -0
- package/src/pages/admin/graphql.astro +97 -0
- package/src/pages/admin/index.astro +286 -0
- package/src/pages/admin/keys.astro +8 -0
- package/src/pages/admin/rest-playground.astro +44 -0
- package/src/pages/admin/webhooks.astro +8 -0
- package/src/pages/api/[collection]/[id]/publish.ts +44 -0
- package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
- package/src/pages/api/[collection]/[id]/versions.ts +36 -0
- package/src/pages/api/[collection]/[id].ts +102 -159
- package/src/pages/api/[collection]/index.ts +151 -230
- package/src/pages/api/auth/[id].ts +48 -69
- package/src/pages/api/auth/audit-logs.ts +20 -43
- package/src/pages/api/auth/login.ts +159 -45
- package/src/pages/api/auth/logout.ts +50 -20
- package/src/pages/api/auth/refresh.ts +119 -0
- package/src/pages/api/auth/register.ts +110 -40
- package/src/pages/api/auth/users.ts +22 -97
- package/src/pages/api/collections.ts +59 -0
- package/src/pages/api/globals/[slug]/test.ts +172 -0
- package/src/pages/api/globals/[slug].ts +42 -0
- package/src/pages/api/graphql.ts +90 -0
- package/src/pages/api/health.ts +417 -40
- package/src/pages/api/keys/[id].ts +26 -0
- package/src/pages/api/keys/index.ts +75 -0
- package/src/pages/api/media/[id].ts +309 -0
- package/src/pages/api/media/folders.ts +609 -0
- package/src/pages/api/media/index.ts +146 -0
- package/src/pages/api/media/resize.ts +267 -0
- package/src/pages/api/search.ts +82 -0
- package/src/pages/api/slug-availability.ts +70 -0
- package/src/pages/api/storage-config.ts +20 -0
- package/src/pages/api/storage-status.ts +206 -0
- package/src/pages/api/upload.ts +334 -0
- package/src/pages/api/webhooks/index.ts +71 -0
- package/src/pages/audit/index.astro +2 -104
- package/src/pages/login.astro +82 -0
- package/src/pages/media.astro +10 -0
- package/src/pages/preview/[collection]/[id].astro +178 -0
- package/src/pages/register.astro +102 -0
- package/src/pages/roles/index.astro +21 -21
- package/src/pages/settings/[slug].astro +162 -0
- package/src/pages/settings/index.astro +9 -0
- package/src/pages/users/[id].astro +29 -21
- package/src/pages/users/index.astro +22 -17
- package/src/pages/users/new.astro +18 -17
- package/src/styles/main.css +553 -128
- package/src/components/layout/Sidebar.tsx +0 -497
- package/src/pages/index.astro +0 -225
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Key,
|
|
4
|
+
Plus,
|
|
5
|
+
Trash2,
|
|
6
|
+
Copy,
|
|
7
|
+
CheckCircle2,
|
|
8
|
+
Clock,
|
|
9
|
+
Shield,
|
|
10
|
+
Zap,
|
|
11
|
+
AlertTriangle,
|
|
12
|
+
X,
|
|
13
|
+
Info,
|
|
14
|
+
Terminal,
|
|
15
|
+
ExternalLink,
|
|
16
|
+
Code,
|
|
17
|
+
} from "lucide-react";
|
|
18
|
+
import { ConfirmModal, Modal, ModalContent, ModalActions } from "./ui/Modal";
|
|
19
|
+
|
|
20
|
+
interface ApiKeyItem {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
key?: string;
|
|
24
|
+
keyPrefix: string;
|
|
25
|
+
permissions?: string[];
|
|
26
|
+
lastUsed?: string;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ApiKeysManager() {
|
|
31
|
+
const [keys, setKeys] = useState<ApiKeyItem[]>([]);
|
|
32
|
+
const [loading, setLoading] = useState(false);
|
|
33
|
+
const [newKey, setNewKey] = useState<ApiKeyItem | null>(null);
|
|
34
|
+
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
35
|
+
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
36
|
+
const [showHelpModal, setShowHelpModal] = useState(false);
|
|
37
|
+
const [showAlertModal, setShowAlertModal] = useState(false);
|
|
38
|
+
const [alertMessage, setAlertMessage] = useState("");
|
|
39
|
+
const [deleteKeyId, setDeleteKeyId] = useState<string | null>(null);
|
|
40
|
+
const [newKeyName, setNewKeyName] = useState("");
|
|
41
|
+
const [copiedId, setCopiedId] = useState<string | null>(null);
|
|
42
|
+
const [createError, setCreateError] = useState("");
|
|
43
|
+
|
|
44
|
+
const loadKeys = async () => {
|
|
45
|
+
setLoading(true);
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch("/api/keys");
|
|
48
|
+
if (res.ok) {
|
|
49
|
+
const data = await res.json();
|
|
50
|
+
setKeys(
|
|
51
|
+
data.map((k: any) => ({
|
|
52
|
+
...k,
|
|
53
|
+
key: k.key,
|
|
54
|
+
keyPrefix: k.keyPrefix || k.key?.substring(0, 8) || "",
|
|
55
|
+
})),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error(e);
|
|
60
|
+
} finally {
|
|
61
|
+
setLoading(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
loadKeys();
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
const handleCreateKey = async () => {
|
|
70
|
+
if (!newKeyName.trim()) {
|
|
71
|
+
setCreateError("Please enter a name for the API key");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch("/api/keys", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Content-Type": "application/json" },
|
|
79
|
+
body: JSON.stringify({ name: newKeyName }),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (res.ok) {
|
|
83
|
+
const created = await res.json();
|
|
84
|
+
setNewKey(created);
|
|
85
|
+
setShowCreateModal(false);
|
|
86
|
+
setNewKeyName("");
|
|
87
|
+
setCreateError("");
|
|
88
|
+
loadKeys();
|
|
89
|
+
} else {
|
|
90
|
+
const error = await res.json();
|
|
91
|
+
setCreateError(error.error || "Failed to create API key");
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.error(e);
|
|
95
|
+
setCreateError("Failed to create API key");
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const handleDeleteKey = async (id: string) => {
|
|
100
|
+
setDeleteKeyId(id);
|
|
101
|
+
setShowDeleteModal(true);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const confirmDeleteKey = async () => {
|
|
105
|
+
if (!deleteKeyId) return;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const res = await fetch(`/api/keys/${deleteKeyId}`, { method: "DELETE" });
|
|
109
|
+
if (res.ok) {
|
|
110
|
+
loadKeys();
|
|
111
|
+
} else {
|
|
112
|
+
setAlertMessage("Failed to delete API key");
|
|
113
|
+
setShowAlertModal(true);
|
|
114
|
+
}
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.error(e);
|
|
117
|
+
setAlertMessage("Failed to delete API key");
|
|
118
|
+
setShowAlertModal(true);
|
|
119
|
+
}
|
|
120
|
+
setShowDeleteModal(false);
|
|
121
|
+
setDeleteKeyId(null);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const copyToClipboard = (key: string, id: string) => {
|
|
125
|
+
navigator.clipboard.writeText(key);
|
|
126
|
+
setCopiedId(id);
|
|
127
|
+
setTimeout(() => setCopiedId(null), 2000);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 px-8 pb-32">
|
|
132
|
+
{/* Header */}
|
|
133
|
+
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 pt-4">
|
|
134
|
+
<div>
|
|
135
|
+
<div className="flex items-center gap-3">
|
|
136
|
+
<h1 className="text-4xl font-black tracking-tighter text-[var(--kyro-text-primary)]">
|
|
137
|
+
API <span className="text-[var(--kyro-primary)]">Keys</span>
|
|
138
|
+
</h1>
|
|
139
|
+
<button type="button"
|
|
140
|
+
onClick={() => setShowHelpModal(true)}
|
|
141
|
+
className="p-2 rounded-lg text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] transition-all"
|
|
142
|
+
title="Learn how API keys work"
|
|
143
|
+
>
|
|
144
|
+
<Info className="w-5 h-5" />
|
|
145
|
+
</button>
|
|
146
|
+
</div>
|
|
147
|
+
<p className="text-[var(--kyro-text-secondary)] mt-1 font-medium opacity-70">
|
|
148
|
+
Secure tokens for authenticating API requests.
|
|
149
|
+
</p>
|
|
150
|
+
</div>
|
|
151
|
+
<button type="button"
|
|
152
|
+
onClick={() => {
|
|
153
|
+
setNewKeyName("");
|
|
154
|
+
setCreateError("");
|
|
155
|
+
setShowCreateModal(true);
|
|
156
|
+
}}
|
|
157
|
+
className="flex items-center gap-2 px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-full font-black text-sm shadow-xl hover:opacity-90 active:scale-95 transition-all"
|
|
158
|
+
>
|
|
159
|
+
<Plus className="w-4 h-4" />
|
|
160
|
+
Create API Key
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* How to Use Section */}
|
|
165
|
+
<div className="grid md:grid-cols-2 gap-4">
|
|
166
|
+
{/* Quick Start */}
|
|
167
|
+
<div className="surface-tile p-6">
|
|
168
|
+
<div className="flex items-center gap-2 mb-3">
|
|
169
|
+
<Terminal className="w-5 h-5 text-[var(--kyro-primary)]" />
|
|
170
|
+
<h3 className="font-bold">Quick Start</h3>
|
|
171
|
+
</div>
|
|
172
|
+
<p className="text-sm text-[var(--kyro-text-secondary)] opacity-70 mb-3">
|
|
173
|
+
Include your API key in requests:
|
|
174
|
+
</p>
|
|
175
|
+
<div className="space-y-2">
|
|
176
|
+
<div className="bg-[var(--kyro-bg-secondary)] rounded-lg p-3 font-mono text-xs border border-[var(--kyro-border)]">
|
|
177
|
+
<div className="text-[var(--kyro-text-muted)] mb-2">
|
|
178
|
+
Example request:
|
|
179
|
+
</div>
|
|
180
|
+
<div className="text-[var(--kyro-primary)]">curl -X GET \</div>
|
|
181
|
+
<div className="text-[var(--kyro-text-secondary)]">
|
|
182
|
+
{" "}
|
|
183
|
+
https://yoursite.com/api/posts \
|
|
184
|
+
</div>
|
|
185
|
+
<div className="text-[var(--kyro-text-secondary)]">
|
|
186
|
+
{" "}
|
|
187
|
+
-H "Authorization: ApiKey kyro_xxx"
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
<button type="button"
|
|
191
|
+
onClick={() => {
|
|
192
|
+
navigator.clipboard.writeText(
|
|
193
|
+
'curl -X GET https://yoursite.com/api/posts -H "Authorization: ApiKey YOUR_KEY"',
|
|
194
|
+
);
|
|
195
|
+
}}
|
|
196
|
+
className="text-xs text-[var(--kyro-primary)] hover:underline flex items-center gap-1"
|
|
197
|
+
>
|
|
198
|
+
<Copy className="w-3 h-3" /> Copy curl example
|
|
199
|
+
</button>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{/* JavaScript/Fetch */}
|
|
204
|
+
<div className="surface-tile p-6">
|
|
205
|
+
<div className="flex items-center gap-2 mb-3">
|
|
206
|
+
<Code className="w-5 h-5 text-[var(--kyro-primary)]" />
|
|
207
|
+
<h3 className="font-bold">JavaScript / Fetch</h3>
|
|
208
|
+
</div>
|
|
209
|
+
<div className="bg-[var(--kyro-bg-secondary)] rounded-lg p-3 font-mono text-xs border border-[var(--kyro-border)]">
|
|
210
|
+
<div className="text-[var(--kyro-text-muted)] mb-2">
|
|
211
|
+
Example code:
|
|
212
|
+
</div>
|
|
213
|
+
<div>
|
|
214
|
+
<span className="text-[var(--kyro-primary)]">await fetch</span>(
|
|
215
|
+
<span className="text-green-600">'https://api'</span>, {"{"}
|
|
216
|
+
</div>
|
|
217
|
+
<div> headers: {"{"}</div>
|
|
218
|
+
<div>
|
|
219
|
+
{" "}
|
|
220
|
+
<span className="text-green-600">'Authorization'</span>:{" "}
|
|
221
|
+
<span className="text-yellow-700">'ApiKey kyro_xxx'</span>,
|
|
222
|
+
</div>
|
|
223
|
+
<div> {"}"}</div>
|
|
224
|
+
<div>{"}"})</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* Best Practices */}
|
|
230
|
+
<div className="surface-tile p-4 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)]">
|
|
231
|
+
<div className="flex items-start gap-3">
|
|
232
|
+
<Shield className="w-5 h-5 text-amber-600 flex-shrink-0" />
|
|
233
|
+
<div>
|
|
234
|
+
<h4 className="font-bold text-[var(--kyro-text-primary)] mb-2">
|
|
235
|
+
Best Practices for API Keys
|
|
236
|
+
</h4>
|
|
237
|
+
<ul className="text-sm text-[var(--kyro-text-secondary)] grid md:grid-cols-2 gap-2">
|
|
238
|
+
<li className="flex items-center gap-2">
|
|
239
|
+
<span className="text-amber-600">•</span> Never commit to
|
|
240
|
+
version control
|
|
241
|
+
</li>
|
|
242
|
+
<li className="flex items-center gap-2">
|
|
243
|
+
<span className="text-amber-600">•</span> Store in environment
|
|
244
|
+
variables
|
|
245
|
+
</li>
|
|
246
|
+
<li className="flex items-center gap-2">
|
|
247
|
+
<span className="text-amber-600">•</span> Use separate keys per
|
|
248
|
+
app
|
|
249
|
+
</li>
|
|
250
|
+
<li className="flex items-center gap-2">
|
|
251
|
+
<span className="text-amber-600">•</span> Revoke unused keys
|
|
252
|
+
</li>
|
|
253
|
+
</ul>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
{/* New Key Alert */}
|
|
259
|
+
{newKey && (
|
|
260
|
+
<div className="surface-tile p-8 bg-green-500/10 border border-green-500/20">
|
|
261
|
+
<div className="flex items-start gap-4">
|
|
262
|
+
<div className="p-3 bg-green-500/20 rounded-2xl">
|
|
263
|
+
<CheckCircle2 className="w-6 h-6 text-green-500" />
|
|
264
|
+
</div>
|
|
265
|
+
<div className="flex-1">
|
|
266
|
+
<h3 className="text-xl font-black text-green-600 mb-2">
|
|
267
|
+
API Key Created Successfully
|
|
268
|
+
</h3>
|
|
269
|
+
<p className="text-sm text-[var(--kyro-text-secondary)] mb-4">
|
|
270
|
+
Copy this key now.{" "}
|
|
271
|
+
<span className="text-red-500 font-bold">
|
|
272
|
+
This is the only time it will be shown.
|
|
273
|
+
</span>
|
|
274
|
+
</p>
|
|
275
|
+
<div className="flex items-center gap-3">
|
|
276
|
+
<code className="flex-1 p-4 bg-[var(--kyro-bg)] border border-green-500/30 rounded-xl font-mono text-sm break-all">
|
|
277
|
+
{newKey.key}
|
|
278
|
+
</code>
|
|
279
|
+
<button type="button"
|
|
280
|
+
onClick={() => copyToClipboard(newKey.key!, newKey.id)}
|
|
281
|
+
className="p-4 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl hover:opacity-90 flex-shrink-0"
|
|
282
|
+
title="Copy to clipboard"
|
|
283
|
+
>
|
|
284
|
+
{copiedId === newKey.id ? (
|
|
285
|
+
<CheckCircle2 className="w-5 h-5" />
|
|
286
|
+
) : (
|
|
287
|
+
<Copy className="w-5 h-5" />
|
|
288
|
+
)}
|
|
289
|
+
</button>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
<button type="button"
|
|
294
|
+
onClick={() => setNewKey(null)}
|
|
295
|
+
className="mt-6 text-sm font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
|
|
296
|
+
>
|
|
297
|
+
I've copied my key - dismiss this message
|
|
298
|
+
</button>
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
{/* Keys List */}
|
|
303
|
+
<section className="space-y-6">
|
|
304
|
+
<div className="flex items-center gap-2 px-2">
|
|
305
|
+
<Key className="w-4 h-4 text-[var(--kyro-primary)] opacity-40" />
|
|
306
|
+
<span className="text-[10px] font-black uppercase tracking-widest opacity-40">
|
|
307
|
+
Your API Keys
|
|
308
|
+
</span>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
{loading ? (
|
|
312
|
+
<div className="surface-tile p-8 text-center">Loading...</div>
|
|
313
|
+
) : keys.length === 0 ? (
|
|
314
|
+
<div className="surface-tile p-12 text-center border-2 border-dashed border-[var(--kyro-border)]">
|
|
315
|
+
<div className="w-16 h-16 mx-auto mb-6 bg-[var(--kyro-surface-accent)] rounded-2xl flex items-center justify-center">
|
|
316
|
+
<Shield className="w-8 h-8 text-[var(--kyro-text-secondary)] opacity-20" />
|
|
317
|
+
</div>
|
|
318
|
+
<h3 className="text-lg font-black mb-2">No API Keys Yet</h3>
|
|
319
|
+
<p className="text-sm text-[var(--kyro-text-secondary)] opacity-60 mb-6">
|
|
320
|
+
Create your first API key to authenticate with the API.
|
|
321
|
+
</p>
|
|
322
|
+
<button type="button"
|
|
323
|
+
onClick={() => setShowCreateModal(true)}
|
|
324
|
+
className="px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-full font-black text-sm hover:opacity-90 transition-all"
|
|
325
|
+
>
|
|
326
|
+
<Plus className="w-4 h-4 inline mr-2" />
|
|
327
|
+
Create Your First Key
|
|
328
|
+
</button>
|
|
329
|
+
</div>
|
|
330
|
+
) : (
|
|
331
|
+
<div className="grid gap-4">
|
|
332
|
+
{keys.map((key) => (
|
|
333
|
+
<div
|
|
334
|
+
key={key.id}
|
|
335
|
+
className="surface-tile p-6 group hover:border-[var(--kyro-primary)] transition-all"
|
|
336
|
+
>
|
|
337
|
+
<div className="flex items-start justify-between gap-6">
|
|
338
|
+
<div className="flex-1">
|
|
339
|
+
<div className="flex items-center gap-3 mb-3">
|
|
340
|
+
<h3 className="text-lg font-black">{key.name}</h3>
|
|
341
|
+
<span className="px-2 py-0.5 bg-green-500/10 text-green-500 rounded-full text-[8px] font-black uppercase">
|
|
342
|
+
Active
|
|
343
|
+
</span>
|
|
344
|
+
</div>
|
|
345
|
+
<div className="flex items-center gap-2 text-sm font-mono text-[var(--kyro-text-secondary)] opacity-50 mb-4">
|
|
346
|
+
<Key className="w-3 h-3" />
|
|
347
|
+
<span>{key.keyPrefix}••••••••</span>
|
|
348
|
+
<span className="text-[10px] opacity-40">
|
|
349
|
+
(prefix shown)
|
|
350
|
+
</span>
|
|
351
|
+
</div>
|
|
352
|
+
<div className="flex items-center gap-6 text-[10px] font-black uppercase opacity-40">
|
|
353
|
+
<div className="flex items-center gap-2">
|
|
354
|
+
<Clock className="w-3 h-3" />
|
|
355
|
+
{key.lastUsed
|
|
356
|
+
? `Last used: ${new Date(key.lastUsed).toLocaleDateString()}`
|
|
357
|
+
: "Never used"}
|
|
358
|
+
</div>
|
|
359
|
+
<div className="flex items-center gap-2">
|
|
360
|
+
<Zap className="w-3 h-3" />
|
|
361
|
+
Created: {new Date(key.createdAt).toLocaleDateString()}
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
<div className="flex items-center gap-2">
|
|
366
|
+
<button type="button"
|
|
367
|
+
onClick={() =>
|
|
368
|
+
copyToClipboard(
|
|
369
|
+
key.key || `${key.keyPrefix}...`,
|
|
370
|
+
key.id,
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
className="p-3 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-xl hover:bg-[var(--kyro-surface)] flex items-center gap-2"
|
|
374
|
+
>
|
|
375
|
+
{copiedId === key.id ? (
|
|
376
|
+
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
|
377
|
+
) : (
|
|
378
|
+
<Copy className="w-4 h-4" />
|
|
379
|
+
)}
|
|
380
|
+
<span className="text-xs font-medium">
|
|
381
|
+
{copiedId === key.id ? "Copied!" : "Copy"}
|
|
382
|
+
</span>
|
|
383
|
+
</button>
|
|
384
|
+
<button type="button"
|
|
385
|
+
onClick={() => handleDeleteKey(key.id)}
|
|
386
|
+
className="p-3 text-red-500 bg-red-500/10 rounded-xl hover:bg-red-500/20"
|
|
387
|
+
title="Delete API key"
|
|
388
|
+
>
|
|
389
|
+
<Trash2 className="w-4 h-4" />
|
|
390
|
+
</button>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
))}
|
|
395
|
+
</div>
|
|
396
|
+
)}
|
|
397
|
+
</section>
|
|
398
|
+
|
|
399
|
+
{/* Create Modal */}
|
|
400
|
+
<Modal
|
|
401
|
+
open={showCreateModal}
|
|
402
|
+
onClose={() => setShowCreateModal(false)}
|
|
403
|
+
title="Create New API Key"
|
|
404
|
+
>
|
|
405
|
+
<ModalContent>
|
|
406
|
+
<p className="text-sm text-[var(--kyro-text-secondary)] mb-4">
|
|
407
|
+
Give your API key a descriptive name to identify its purpose. You
|
|
408
|
+
can create multiple keys for different use cases.
|
|
409
|
+
</p>
|
|
410
|
+
<input
|
|
411
|
+
type="text"
|
|
412
|
+
value={newKeyName}
|
|
413
|
+
onChange={(e) => {
|
|
414
|
+
setNewKeyName(e.target.value);
|
|
415
|
+
setCreateError("");
|
|
416
|
+
}}
|
|
417
|
+
placeholder="e.g., Production App, Staging, Mobile App"
|
|
418
|
+
className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
419
|
+
onKeyDown={(e) => e.key === "Enter" && handleCreateKey()}
|
|
420
|
+
/>
|
|
421
|
+
{createError && (
|
|
422
|
+
<p className="mt-2 text-sm text-red-500">{createError}</p>
|
|
423
|
+
)}
|
|
424
|
+
<div className="mt-4 p-4 bg-amber-500/10 border border-amber-500/20 rounded-xl flex items-start gap-3">
|
|
425
|
+
<AlertTriangle className="w-5 h-5 text-amber-500 flex-shrink-0 mt-0.5" />
|
|
426
|
+
<div>
|
|
427
|
+
<p className="text-xs font-bold text-amber-600 mb-1">Important</p>
|
|
428
|
+
<p className="text-xs text-[var(--kyro-text-secondary)]">
|
|
429
|
+
The API key will be shown only once after creation. Copy it
|
|
430
|
+
immediately and store it securely.
|
|
431
|
+
</p>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
</ModalContent>
|
|
435
|
+
<ModalActions>
|
|
436
|
+
<button type="button"
|
|
437
|
+
onClick={() => setShowCreateModal(false)}
|
|
438
|
+
className="px-4 py-2 rounded-lg font-medium text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
439
|
+
>
|
|
440
|
+
Cancel
|
|
441
|
+
</button>
|
|
442
|
+
<button type="button"
|
|
443
|
+
onClick={handleCreateKey}
|
|
444
|
+
className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90 transition-colors"
|
|
445
|
+
>
|
|
446
|
+
Generate Key
|
|
447
|
+
</button>
|
|
448
|
+
</ModalActions>
|
|
449
|
+
</Modal>
|
|
450
|
+
|
|
451
|
+
{/* Delete Confirmation Modal */}
|
|
452
|
+
<Modal
|
|
453
|
+
open={showDeleteModal}
|
|
454
|
+
onClose={() => setShowDeleteModal(false)}
|
|
455
|
+
title="Delete API Key"
|
|
456
|
+
variant="danger"
|
|
457
|
+
>
|
|
458
|
+
<ModalContent>
|
|
459
|
+
<p className="text-sm text-[var(--kyro-text-secondary)] mb-4">
|
|
460
|
+
Are you sure you want to delete this API key? This action cannot be
|
|
461
|
+
undone.
|
|
462
|
+
</p>
|
|
463
|
+
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-xl">
|
|
464
|
+
<p className="text-sm font-medium text-red-500">
|
|
465
|
+
Any applications or integrations using this key will immediately
|
|
466
|
+
lose access.
|
|
467
|
+
</p>
|
|
468
|
+
</div>
|
|
469
|
+
</ModalContent>
|
|
470
|
+
<ModalActions>
|
|
471
|
+
<button type="button"
|
|
472
|
+
onClick={() => setShowDeleteModal(false)}
|
|
473
|
+
className="px-4 py-2 rounded-lg font-medium text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
474
|
+
>
|
|
475
|
+
Keep Key
|
|
476
|
+
</button>
|
|
477
|
+
<button type="button"
|
|
478
|
+
onClick={confirmDeleteKey}
|
|
479
|
+
className="px-4 py-2 rounded-lg font-medium text-sm bg-red-500 text-white hover:bg-red-600 transition-colors"
|
|
480
|
+
>
|
|
481
|
+
Delete Permanently
|
|
482
|
+
</button>
|
|
483
|
+
</ModalActions>
|
|
484
|
+
</Modal>
|
|
485
|
+
|
|
486
|
+
{/* Help Modal */}
|
|
487
|
+
<Modal
|
|
488
|
+
open={showHelpModal}
|
|
489
|
+
onClose={() => setShowHelpModal(false)}
|
|
490
|
+
title="How API Keys Work"
|
|
491
|
+
>
|
|
492
|
+
<ModalContent>
|
|
493
|
+
<div className="space-y-6">
|
|
494
|
+
<div>
|
|
495
|
+
<h4 className="font-bold mb-2">What is an API key?</h4>
|
|
496
|
+
<p className="text-sm text-[var(--kyro-text-secondary)]">
|
|
497
|
+
An API key is a unique token that authenticates your requests to
|
|
498
|
+
the API. Think of it as a password that's specifically for
|
|
499
|
+
programmatic access.
|
|
500
|
+
</p>
|
|
501
|
+
</div>
|
|
502
|
+
<div>
|
|
503
|
+
<h4 className="font-bold mb-2">How to use it</h4>
|
|
504
|
+
<p className="text-sm text-[var(--kyro-text-secondary)] mb-3">
|
|
505
|
+
Add your API key to the Authorization header of your HTTP
|
|
506
|
+
requests:
|
|
507
|
+
</p>
|
|
508
|
+
<div className="bg-[var(--kyro-bg)] rounded-lg p-4 font-mono text-sm space-y-2">
|
|
509
|
+
<div>
|
|
510
|
+
<span className="text-[var(--kyro-text-secondary)]">
|
|
511
|
+
Authorization:
|
|
512
|
+
</span>{" "}
|
|
513
|
+
<span className="text-[var(--kyro-primary)]">ApiKey </span>
|
|
514
|
+
<span className="text-green-500">kyro_xxxxxxxxxxxx</span>
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
<div>
|
|
519
|
+
<h4 className="font-bold mb-2">Best practices</h4>
|
|
520
|
+
<ul className="text-sm text-[var(--kyro-text-secondary)] space-y-2 list-disc list-inside">
|
|
521
|
+
<li>Never share your API key publicly</li>
|
|
522
|
+
<li>
|
|
523
|
+
Store it securely (environment variables, secrets manager)
|
|
524
|
+
</li>
|
|
525
|
+
<li>Create separate keys for different applications</li>
|
|
526
|
+
<li>Revoke keys that are no longer in use</li>
|
|
527
|
+
</ul>
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
530
|
+
</ModalContent>
|
|
531
|
+
<ModalActions>
|
|
532
|
+
<button type="button"
|
|
533
|
+
onClick={() => setShowHelpModal(false)}
|
|
534
|
+
className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90 transition-colors"
|
|
535
|
+
>
|
|
536
|
+
Got it
|
|
537
|
+
</button>
|
|
538
|
+
</ModalActions>
|
|
539
|
+
</Modal>
|
|
540
|
+
|
|
541
|
+
{/* Alert Modal */}
|
|
542
|
+
<Modal
|
|
543
|
+
open={showAlertModal}
|
|
544
|
+
onClose={() => setShowAlertModal(false)}
|
|
545
|
+
title="Error"
|
|
546
|
+
>
|
|
547
|
+
<ModalContent>
|
|
548
|
+
<p className="text-sm text-[var(--kyro-text-secondary)]">
|
|
549
|
+
{alertMessage}
|
|
550
|
+
</p>
|
|
551
|
+
</ModalContent>
|
|
552
|
+
<ModalActions>
|
|
553
|
+
<button type="button"
|
|
554
|
+
onClick={() => setShowAlertModal(false)}
|
|
555
|
+
className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90 transition-colors"
|
|
556
|
+
>
|
|
557
|
+
OK
|
|
558
|
+
</button>
|
|
559
|
+
</ModalActions>
|
|
560
|
+
</Modal>
|
|
561
|
+
</div>
|
|
562
|
+
);
|
|
563
|
+
}
|