@kyro-cms/admin 0.1.6 → 0.1.8

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.
Files changed (179) hide show
  1. package/README.md +149 -51
  2. package/package.json +54 -5
  3. package/src/collections/auth/index.ts +2 -2
  4. package/src/collections/portfolio/index.ts +343 -0
  5. package/src/components/ActionBar.tsx +153 -16
  6. package/src/components/Admin.tsx +137 -28
  7. package/src/components/ApiExplorer.tsx +325 -0
  8. package/src/components/ApiKeysManager.tsx +563 -0
  9. package/src/components/AuditLogsPage.tsx +664 -0
  10. package/src/components/AutoForm.tsx +2155 -770
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +4 -4
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +200 -58
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +890 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +192 -54
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +206 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/ThemeProvider.tsx +8 -2
  26. package/src/components/UserManagement.tsx +204 -0
  27. package/src/components/VersionHistoryPanel.tsx +3 -3
  28. package/src/components/WebhookManager.tsx +608 -0
  29. package/src/components/blocks/AccordionBlock.tsx +65 -0
  30. package/src/components/blocks/ArrayBlock.tsx +84 -0
  31. package/src/components/blocks/BlockEditModal.tsx +363 -0
  32. package/src/components/blocks/ButtonBlock.tsx +64 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +114 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +93 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +63 -0
  38. package/src/components/blocks/HeadingBlock.tsx +59 -0
  39. package/src/components/blocks/HeroBlock.tsx +99 -0
  40. package/src/components/blocks/ImageBlock.tsx +82 -0
  41. package/src/components/blocks/LinkBlock.tsx +65 -0
  42. package/src/components/blocks/ListBlock.tsx +60 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +72 -0
  45. package/src/components/blocks/RichTextBlock.tsx +66 -0
  46. package/src/components/blocks/VStackBlock.tsx +61 -0
  47. package/src/components/blocks/VideoBlock.tsx +65 -0
  48. package/src/components/blocks/index.ts +10 -0
  49. package/src/components/fields/AccordionField.tsx +213 -0
  50. package/src/components/fields/ArrayField.tsx +241 -0
  51. package/src/components/fields/BlocksField.tsx +323 -0
  52. package/src/components/fields/ButtonField.tsx +53 -0
  53. package/src/components/fields/CheckboxField.tsx +18 -8
  54. package/src/components/fields/ChildrenField.tsx +48 -0
  55. package/src/components/fields/CodeField.tsx +294 -0
  56. package/src/components/fields/ColumnsField.tsx +137 -0
  57. package/src/components/fields/DateField.tsx +24 -12
  58. package/src/components/fields/EditorClient.tsx +537 -0
  59. package/src/components/fields/HeadingField.tsx +31 -0
  60. package/src/components/fields/HeroField.tsx +101 -0
  61. package/src/components/fields/JSONField.tsx +341 -0
  62. package/src/components/fields/LinkField.tsx +81 -0
  63. package/src/components/fields/ListField.tsx +74 -0
  64. package/src/components/fields/MarkdownField.tsx +260 -0
  65. package/src/components/fields/NumberField.tsx +25 -13
  66. package/src/components/fields/PortableTextField.tsx +155 -0
  67. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  68. package/src/components/fields/RelationshipBlockField.tsx +233 -0
  69. package/src/components/fields/RelationshipField.tsx +278 -60
  70. package/src/components/fields/SelectField.tsx +28 -16
  71. package/src/components/fields/TextField.tsx +31 -15
  72. package/src/components/fields/UploadField.tsx +613 -0
  73. package/src/components/fields/VideoField.tsx +73 -0
  74. package/src/components/fields/extensions/blockComponents.tsx +247 -0
  75. package/src/components/fields/extensions/blocksStore.ts +273 -0
  76. package/src/components/fields/index.ts +24 -0
  77. package/src/components/index.ts +1 -2
  78. package/src/components/layout/Header.tsx +2 -2
  79. package/src/components/layout/Layout.tsx +3 -3
  80. package/src/components/ui/Badge.tsx +9 -4
  81. package/src/components/ui/BlockDrawer.tsx +79 -0
  82. package/src/components/ui/Button.tsx +1 -1
  83. package/src/components/ui/CommandPalette.tsx +362 -0
  84. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  85. package/src/components/ui/Dropdown.tsx +1 -1
  86. package/src/components/ui/Modal.tsx +37 -12
  87. package/src/components/ui/PromptModal.tsx +94 -0
  88. package/src/components/ui/SlidePanel.tsx +43 -16
  89. package/src/components/ui/Toast.tsx +80 -14
  90. package/src/env.d.ts +16 -0
  91. package/src/env.ts +20 -0
  92. package/src/index.ts +0 -1
  93. package/src/layouts/AdminLayout.astro +164 -170
  94. package/src/layouts/AuthLayout.astro +23 -6
  95. package/src/lib/MediaService.ts +541 -0
  96. package/src/lib/api.ts +163 -0
  97. package/src/lib/auth/sqlite-adapter.ts +319 -0
  98. package/src/lib/config.ts +23 -7
  99. package/src/lib/dataStore.ts +188 -73
  100. package/src/lib/date-utils.ts +69 -0
  101. package/src/lib/db/adapter.ts +54 -0
  102. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  103. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  104. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  105. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  106. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  107. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  108. package/src/lib/db/index.ts +449 -0
  109. package/src/lib/db/mongodb-adapter.ts +207 -0
  110. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  111. package/src/lib/db/schema/mysql-auth.ts +113 -0
  112. package/src/lib/db/schema/mysql-content.ts +20 -0
  113. package/src/lib/db/schema/postgres-auth.ts +116 -0
  114. package/src/lib/db/schema/postgres-content.ts +35 -0
  115. package/src/lib/db/schema/postgres-media.ts +52 -0
  116. package/src/lib/db/schema/postgres-settings.ts +11 -0
  117. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  118. package/src/lib/db/schema/sqlite-content.ts +20 -0
  119. package/src/lib/db/version-adapter.ts +248 -0
  120. package/src/lib/graphql/index.ts +1 -0
  121. package/src/lib/graphql/schema.ts +443 -0
  122. package/src/lib/i18n.tsx +353 -0
  123. package/src/lib/rate-limit.ts +267 -0
  124. package/src/lib/slugify.ts +15 -0
  125. package/src/lib/storage.ts +374 -0
  126. package/src/lib/store.ts +85 -0
  127. package/src/lib/validation.ts +250 -0
  128. package/src/middleware.ts +70 -11
  129. package/src/pages/[collection]/[id].astro +178 -122
  130. package/src/pages/[collection]/index.astro +24 -156
  131. package/src/pages/admin/api-explorer.astro +98 -0
  132. package/src/pages/admin/graphql-explorer.astro +40 -0
  133. package/src/pages/admin/graphql.astro +97 -0
  134. package/src/pages/admin/index.astro +200 -139
  135. package/src/pages/admin/keys.astro +8 -0
  136. package/src/pages/admin/rest-playground.astro +44 -0
  137. package/src/pages/admin/webhooks.astro +8 -0
  138. package/src/pages/api/[collection]/[id]/publish.ts +52 -0
  139. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  140. package/src/pages/api/[collection]/[id]/versions.ts +66 -0
  141. package/src/pages/api/[collection]/[id].ts +114 -159
  142. package/src/pages/api/[collection]/index.ts +150 -230
  143. package/src/pages/api/auth/[id].ts +48 -69
  144. package/src/pages/api/auth/audit-logs.ts +20 -43
  145. package/src/pages/api/auth/login.ts +159 -45
  146. package/src/pages/api/auth/logout.ts +42 -24
  147. package/src/pages/api/auth/refresh.ts +119 -0
  148. package/src/pages/api/auth/register.ts +110 -40
  149. package/src/pages/api/auth/users.ts +22 -97
  150. package/src/pages/api/collections.ts +59 -0
  151. package/src/pages/api/globals/[slug]/test.ts +172 -0
  152. package/src/pages/api/globals/[slug].ts +42 -0
  153. package/src/pages/api/graphql.ts +90 -0
  154. package/src/pages/api/health.ts +417 -40
  155. package/src/pages/api/keys/[id].ts +26 -0
  156. package/src/pages/api/keys/index.ts +75 -0
  157. package/src/pages/api/media/[id].ts +309 -0
  158. package/src/pages/api/media/folders.ts +609 -0
  159. package/src/pages/api/media/index.ts +146 -0
  160. package/src/pages/api/media/resize.ts +267 -0
  161. package/src/pages/api/search.ts +82 -0
  162. package/src/pages/api/slug-availability.ts +70 -0
  163. package/src/pages/api/storage-config.ts +20 -0
  164. package/src/pages/api/storage-status.ts +206 -0
  165. package/src/pages/api/upload.ts +334 -0
  166. package/src/pages/api/webhooks/index.ts +71 -0
  167. package/src/pages/audit/index.astro +2 -104
  168. package/src/pages/login.astro +11 -11
  169. package/src/pages/media.astro +10 -0
  170. package/src/pages/preview/[collection]/[id].astro +178 -0
  171. package/src/pages/register.astro +13 -13
  172. package/src/pages/roles/index.astro +21 -21
  173. package/src/pages/settings/[slug].astro +162 -0
  174. package/src/pages/settings/index.astro +9 -0
  175. package/src/pages/users/[id].astro +29 -21
  176. package/src/pages/users/index.astro +22 -17
  177. package/src/pages/users/new.astro +18 -17
  178. package/src/styles/main.css +563 -128
  179. package/src/components/layout/Sidebar.tsx +0 -497
@@ -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
+ }