@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.
Files changed (164) hide show
  1. package/README.md +149 -51
  2. package/package.json +52 -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 +136 -27
  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 +1417 -661
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +3 -3
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +199 -57
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +786 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +191 -53
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +149 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/UserManagement.tsx +204 -0
  26. package/src/components/VersionHistoryPanel.tsx +3 -3
  27. package/src/components/WebhookManager.tsx +608 -0
  28. package/src/components/blocks/AccordionBlock.tsx +97 -0
  29. package/src/components/blocks/ArrayBlock.tsx +75 -0
  30. package/src/components/blocks/BlockEditModal.MARKER +12 -0
  31. package/src/components/blocks/BlockEditModal.tsx +774 -0
  32. package/src/components/blocks/ButtonBlock.tsx +165 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +66 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +151 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +64 -0
  38. package/src/components/blocks/HeadingBlock.tsx +81 -0
  39. package/src/components/blocks/HeroBlock.tsx +157 -0
  40. package/src/components/blocks/ImageBlock.tsx +83 -0
  41. package/src/components/blocks/LinkBlock.tsx +71 -0
  42. package/src/components/blocks/ListBlock.tsx +39 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +279 -0
  45. package/src/components/blocks/VStackBlock.tsx +75 -0
  46. package/src/components/blocks/VideoBlock.tsx +45 -0
  47. package/src/components/blocks/index.ts +10 -0
  48. package/src/components/fields/BlocksField.tsx +323 -0
  49. package/src/components/fields/CheckboxField.tsx +15 -9
  50. package/src/components/fields/CodeField.tsx +234 -0
  51. package/src/components/fields/DateField.tsx +38 -11
  52. package/src/components/fields/EditorClient.tsx +271 -0
  53. package/src/components/fields/FileField.tsx +390 -0
  54. package/src/components/fields/HybridContentField.tsx +109 -0
  55. package/src/components/fields/ImageField.tsx +429 -0
  56. package/src/components/fields/JSONField.tsx +361 -0
  57. package/src/components/fields/MarkdownField.tsx +282 -0
  58. package/src/components/fields/NumberField.tsx +42 -12
  59. package/src/components/fields/PortableTextField.tsx +143 -0
  60. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  61. package/src/components/fields/RelationshipField.tsx +231 -59
  62. package/src/components/fields/SelectField.tsx +25 -15
  63. package/src/components/fields/TextField.tsx +45 -14
  64. package/src/components/fields/extensions/blockComponents.tsx +237 -0
  65. package/src/components/fields/extensions/blocksStore.ts +273 -0
  66. package/src/components/fields/index.ts +13 -0
  67. package/src/components/index.ts +1 -2
  68. package/src/components/layout/Header.tsx +2 -2
  69. package/src/components/layout/Layout.tsx +2 -2
  70. package/src/components/ui/Badge.tsx +9 -4
  71. package/src/components/ui/BlockDrawer.tsx +79 -0
  72. package/src/components/ui/Button.tsx +1 -1
  73. package/src/components/ui/CommandPalette.tsx +362 -0
  74. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  75. package/src/components/ui/Dropdown.tsx +1 -1
  76. package/src/components/ui/Modal.tsx +37 -12
  77. package/src/components/ui/PromptModal.tsx +94 -0
  78. package/src/components/ui/SlidePanel.tsx +43 -16
  79. package/src/components/ui/Toast.tsx +80 -14
  80. package/src/env.d.ts +16 -0
  81. package/src/env.ts +20 -0
  82. package/src/index.ts +0 -1
  83. package/src/layouts/AdminLayout.astro +164 -170
  84. package/src/layouts/AuthLayout.astro +50 -0
  85. package/src/lib/MediaService.ts +541 -0
  86. package/src/lib/auth/sqlite-adapter.ts +319 -0
  87. package/src/lib/config.ts +22 -6
  88. package/src/lib/dataStore.ts +132 -74
  89. package/src/lib/db/adapter.ts +54 -0
  90. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  91. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  92. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  93. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  94. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  95. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  96. package/src/lib/db/index.ts +449 -0
  97. package/src/lib/db/mongodb-adapter.ts +207 -0
  98. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  99. package/src/lib/db/schema/mysql-auth.ts +113 -0
  100. package/src/lib/db/schema/mysql-content.ts +20 -0
  101. package/src/lib/db/schema/postgres-auth.ts +116 -0
  102. package/src/lib/db/schema/postgres-content.ts +35 -0
  103. package/src/lib/db/schema/postgres-media.ts +52 -0
  104. package/src/lib/db/schema/postgres-settings.ts +11 -0
  105. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  106. package/src/lib/db/schema/sqlite-content.ts +20 -0
  107. package/src/lib/graphql/index.ts +1 -0
  108. package/src/lib/graphql/schema.ts +443 -0
  109. package/src/lib/rate-limit.ts +267 -0
  110. package/src/lib/storage.ts +374 -0
  111. package/src/lib/store.ts +85 -0
  112. package/src/middleware.ts +116 -28
  113. package/src/pages/[collection]/[id].astro +178 -122
  114. package/src/pages/[collection]/index.astro +24 -156
  115. package/src/pages/admin/api-explorer.astro +98 -0
  116. package/src/pages/admin/graphql-explorer.astro +40 -0
  117. package/src/pages/admin/graphql.astro +97 -0
  118. package/src/pages/admin/index.astro +286 -0
  119. package/src/pages/admin/keys.astro +8 -0
  120. package/src/pages/admin/rest-playground.astro +44 -0
  121. package/src/pages/admin/webhooks.astro +8 -0
  122. package/src/pages/api/[collection]/[id]/publish.ts +44 -0
  123. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  124. package/src/pages/api/[collection]/[id]/versions.ts +36 -0
  125. package/src/pages/api/[collection]/[id].ts +102 -159
  126. package/src/pages/api/[collection]/index.ts +151 -230
  127. package/src/pages/api/auth/[id].ts +48 -69
  128. package/src/pages/api/auth/audit-logs.ts +20 -43
  129. package/src/pages/api/auth/login.ts +159 -45
  130. package/src/pages/api/auth/logout.ts +50 -20
  131. package/src/pages/api/auth/refresh.ts +119 -0
  132. package/src/pages/api/auth/register.ts +110 -40
  133. package/src/pages/api/auth/users.ts +22 -97
  134. package/src/pages/api/collections.ts +59 -0
  135. package/src/pages/api/globals/[slug]/test.ts +172 -0
  136. package/src/pages/api/globals/[slug].ts +42 -0
  137. package/src/pages/api/graphql.ts +90 -0
  138. package/src/pages/api/health.ts +417 -40
  139. package/src/pages/api/keys/[id].ts +26 -0
  140. package/src/pages/api/keys/index.ts +75 -0
  141. package/src/pages/api/media/[id].ts +309 -0
  142. package/src/pages/api/media/folders.ts +609 -0
  143. package/src/pages/api/media/index.ts +146 -0
  144. package/src/pages/api/media/resize.ts +267 -0
  145. package/src/pages/api/search.ts +82 -0
  146. package/src/pages/api/slug-availability.ts +70 -0
  147. package/src/pages/api/storage-config.ts +20 -0
  148. package/src/pages/api/storage-status.ts +206 -0
  149. package/src/pages/api/upload.ts +334 -0
  150. package/src/pages/api/webhooks/index.ts +71 -0
  151. package/src/pages/audit/index.astro +2 -104
  152. package/src/pages/login.astro +82 -0
  153. package/src/pages/media.astro +10 -0
  154. package/src/pages/preview/[collection]/[id].astro +178 -0
  155. package/src/pages/register.astro +102 -0
  156. package/src/pages/roles/index.astro +21 -21
  157. package/src/pages/settings/[slug].astro +162 -0
  158. package/src/pages/settings/index.astro +9 -0
  159. package/src/pages/users/[id].astro +29 -21
  160. package/src/pages/users/index.astro +22 -17
  161. package/src/pages/users/new.astro +18 -17
  162. package/src/styles/main.css +553 -128
  163. package/src/components/layout/Sidebar.tsx +0 -497
  164. package/src/pages/index.astro +0 -225
@@ -0,0 +1,90 @@
1
+ import type { APIRoute } from "astro";
2
+ import { executeGraphQL } from "@/lib/graphql/schema";
3
+ import { dataStore } from "@/lib/dataStore";
4
+ import { collections } from "@/lib/config";
5
+
6
+ dataStore.initialize(collections);
7
+
8
+ export const ALL: APIRoute = async ({ request }) => {
9
+ const url = new URL(request.url);
10
+ const method = request.method.toUpperCase();
11
+
12
+ if (method === "GET") {
13
+ return new Response(
14
+ JSON.stringify({
15
+ message: "Kyro CMS GraphQL API",
16
+ version: "1.0",
17
+ endpoints: {
18
+ playground: "/admin/graphql",
19
+ graphql: "/graphql",
20
+ introspection: "POST with { __schema { types { name } } }",
21
+ },
22
+ }),
23
+ {
24
+ status: 200,
25
+ headers: { "Content-Type": "application/json" },
26
+ },
27
+ );
28
+ }
29
+
30
+ if (method === "POST") {
31
+ try {
32
+ const contentType = request.headers.get("content-type") || "";
33
+ let query: string;
34
+ let variables: Record<string, any> | undefined;
35
+
36
+ if (contentType.includes("application/json")) {
37
+ const body = await request.json();
38
+ query = body.query;
39
+ variables = body.variables;
40
+ } else {
41
+ const formData = await request.formData();
42
+ query = formData.get("query") as string;
43
+ const variablesStr = formData.get("variables") as string;
44
+ if (variablesStr) {
45
+ try {
46
+ variables = JSON.parse(variablesStr);
47
+ } catch {}
48
+ }
49
+ }
50
+
51
+ if (!query) {
52
+ return new Response(JSON.stringify({ error: "No query provided" }), {
53
+ status: 400,
54
+ headers: { "Content-Type": "application/json" },
55
+ });
56
+ }
57
+
58
+ const authHeader = request.headers.get("authorization");
59
+ const token = authHeader?.startsWith("Bearer ")
60
+ ? authHeader.slice(7)
61
+ : undefined;
62
+ const apiKey = authHeader?.startsWith("ApiKey ")
63
+ ? authHeader.slice(7).trim()
64
+ : request.headers.get("x-api-key")?.trim() || undefined;
65
+
66
+ const result = await executeGraphQL(query, variables, token, apiKey);
67
+
68
+ return new Response(JSON.stringify(result), {
69
+ status: 200,
70
+ headers: { "Content-Type": "application/json" },
71
+ });
72
+ } catch (error) {
73
+ console.error("GraphQL Error:", error);
74
+ return new Response(
75
+ JSON.stringify({
76
+ errors: [{ message: "Internal server error" }],
77
+ }),
78
+ {
79
+ status: 500,
80
+ headers: { "Content-Type": "application/json" },
81
+ },
82
+ );
83
+ }
84
+ }
85
+
86
+ return new Response(JSON.stringify({ error: "Method not allowed" }), {
87
+ status: 405,
88
+ headers: { "Content-Type": "application/json" },
89
+ });
90
+ };
@@ -1,49 +1,426 @@
1
1
  import type { APIRoute } from "astro";
2
- import { SQLiteAuthAdapter } from "@kyro-cms/core";
3
-
4
- async function getAuthApi() {
5
- return new SQLiteAuthAdapter({
6
- path: process.env.KYRO_AUTH_DB_PATH || "./data/auth.db",
7
- });
8
- }
2
+ import { dataStore } from "../../lib/dataStore";
3
+ import { collections as adminCollections } from "../../lib/config";
9
4
 
10
5
  export const GET: APIRoute = async () => {
11
6
  try {
12
- const adapter = await getAuthApi();
13
- await adapter.connect();
14
-
15
- const stats = await adapter.getStats();
16
- await adapter.disconnect();
17
-
18
- return new Response(
19
- JSON.stringify({
20
- status: "healthy",
21
- auth: {
22
- storage: "sqlite",
23
- userCount: stats.userCount,
24
- activeSessionCount: stats.activeSessionCount,
25
- auditLogCount: stats.auditLogCount,
26
- },
27
- uptime: process.uptime(),
28
- memory: process.memoryUsage(),
29
- timestamp: new Date().toISOString(),
30
- }),
31
- {
32
- status: 200,
33
- headers: { "Content-Type": "application/json" },
7
+ const startTime = Date.now();
8
+
9
+ const collectionNames = Object.keys(adminCollections);
10
+
11
+ let documentCount = 0;
12
+ const collectionCounts: Record<string, number> = {};
13
+
14
+ for (const slug of collectionNames) {
15
+ try {
16
+ const result = await dataStore.find(slug, { limit: 1 });
17
+ const count = result.totalDocs || 0;
18
+ collectionCounts[slug] = count;
19
+ documentCount += count;
20
+ } catch (e) {
21
+ collectionCounts[slug] = 0;
22
+ }
23
+ }
24
+
25
+ const responseTime = Date.now() - startTime;
26
+ const uptime = Math.floor(process.uptime());
27
+ const uptimeDays = Math.floor(uptime / 86400);
28
+ const uptimeHours = Math.floor((uptime % 86400) / 3600);
29
+ const uptimeMinutes = Math.floor((uptime % 3600) / 60);
30
+
31
+ const html = `<!DOCTYPE html>
32
+ <html lang="en">
33
+ <head>
34
+ <meta charset="UTF-8">
35
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
36
+ <title>System Health - Kyro CMS</title>
37
+ <link rel="preconnect" href="https://fonts.googleapis.com">
38
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
39
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
40
+ <style>
41
+ :root {
42
+ --bg: #eaeff2;
43
+ --surface: #ffffff;
44
+ --surface-accent: #f9fafb;
45
+ --text-primary: #0b1222;
46
+ --text-secondary: #64748b;
47
+ --border: #e5e7eb;
48
+ --primary: #0b1222;
49
+ --accent: #0b1222;
50
+ --accent-text: #ffffff;
51
+ --success: #10b981;
52
+ }
53
+ @media (prefers-color-scheme: dark) {
54
+ :root {
55
+ --bg: #030712;
56
+ --surface: #0b1222;
57
+ --surface-accent: #111a2e;
58
+ --text-primary: #f8fafc;
59
+ --text-secondary: #94a3b8;
60
+ --border: #1e293b;
61
+ --primary: #ffffff;
62
+ --accent: #ffffff;
63
+ --accent-text: #0b1222;
64
+ }
65
+ }
66
+ * { margin: 0; padding: 0; box-sizing: border-box; }
67
+ body {
68
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
69
+ background: var(--bg);
70
+ color: var(--text-primary);
71
+ min-height: 100vh;
72
+ padding: 2rem;
73
+ }
74
+ .container {
75
+ max-width: 720px;
76
+ margin: 0 auto;
77
+ }
78
+ .header {
79
+ text-align: center;
80
+ margin-bottom: 2.5rem;
81
+ }
82
+ .logo {
83
+ display: inline-flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ width: 72px;
87
+ height: 72px;
88
+ background: var(--accent);
89
+ color: var(--accent-text);
90
+ border-radius: 20px;
91
+ font-size: 28px;
92
+ font-weight: 900;
93
+ margin-bottom: 1.5rem;
94
+ box-shadow: 0 8px 32px rgba(0,0,0,0.15);
95
+ }
96
+ h1 {
97
+ font-size: 2rem;
98
+ font-weight: 800;
99
+ letter-spacing: -0.03em;
100
+ margin-bottom: 0.5rem;
101
+ }
102
+ .subtitle {
103
+ color: var(--text-secondary);
104
+ font-size: 0.9375rem;
105
+ font-weight: 500;
106
+ }
107
+ .status-badge {
108
+ display: inline-flex;
109
+ align-items: center;
110
+ gap: 0.5rem;
111
+ background: var(--success);
112
+ color: white;
113
+ padding: 0.625rem 1.25rem;
114
+ border-radius: 9999px;
115
+ font-size: 0.875rem;
116
+ font-weight: 700;
117
+ margin-top: 1.25rem;
118
+ box-shadow: 0 4px 16px rgba(16, 185, 129, 0.3);
119
+ }
120
+ .status-badge::before {
121
+ content: '';
122
+ width: 8px;
123
+ height: 8px;
124
+ background: white;
125
+ border-radius: 50%;
126
+ animation: pulse 2s infinite;
127
+ }
128
+ @keyframes pulse {
129
+ 0%, 100% { opacity: 1; transform: scale(1); }
130
+ 50% { opacity: 0.6; transform: scale(0.9); }
131
+ }
132
+ .stats-grid {
133
+ display: grid;
134
+ grid-template-columns: repeat(2, 1fr);
135
+ gap: 1rem;
136
+ }
137
+ .stat-card {
138
+ background: var(--surface);
139
+ border: 1px solid var(--border);
140
+ border-radius: 16px;
141
+ padding: 1.5rem;
142
+ transition: all 0.2s ease;
143
+ }
144
+ .stat-card:hover {
145
+ border-color: var(--text-secondary);
146
+ transform: translateY(-2px);
147
+ box-shadow: 0 8px 24px rgba(0,0,0,0.08);
148
+ }
149
+ .stat-card.wide {
150
+ grid-column: span 2;
151
+ }
152
+ .stat-label {
153
+ font-size: 0.75rem;
154
+ font-weight: 600;
155
+ color: var(--text-secondary);
156
+ text-transform: uppercase;
157
+ letter-spacing: 0.05em;
158
+ margin-bottom: 0.75rem;
159
+ }
160
+ .stat-value {
161
+ font-size: 2rem;
162
+ font-weight: 800;
163
+ letter-spacing: -0.03em;
164
+ line-height: 1;
165
+ }
166
+ .stat-meta {
167
+ font-size: 0.8125rem;
168
+ color: var(--text-secondary);
169
+ margin-top: 0.5rem;
170
+ font-weight: 500;
171
+ }
172
+ .collection-list {
173
+ display: flex;
174
+ flex-wrap: wrap;
175
+ gap: 0.5rem;
176
+ margin-top: 0.5rem;
177
+ }
178
+ .collection-item {
179
+ display: inline-flex;
180
+ align-items: center;
181
+ gap: 0.375rem;
182
+ padding: 0.375rem 0.75rem;
183
+ background: var(--surface-accent);
184
+ border-radius: 8px;
185
+ font-size: 0.8125rem;
186
+ font-weight: 600;
187
+ color: var(--text-secondary);
188
+ }
189
+ .collection-count {
190
+ background: var(--accent);
191
+ color: var(--accent-text);
192
+ padding: 0.125rem 0.5rem;
193
+ border-radius: 6px;
194
+ font-size: 0.75rem;
195
+ font-weight: 700;
196
+ }
197
+ .nav {
198
+ display: flex;
199
+ justify-content: center;
200
+ gap: 0.75rem;
201
+ margin-top: 2rem;
202
+ flex-wrap: wrap;
203
+ }
204
+ .nav-link {
205
+ color: var(--text-secondary);
206
+ text-decoration: none;
207
+ font-size: 0.875rem;
208
+ font-weight: 600;
209
+ padding: 0.75rem 1.25rem;
210
+ border-radius: 10px;
211
+ background: var(--surface);
212
+ border: 1px solid var(--border);
213
+ transition: all 0.2s ease;
214
+ }
215
+ .nav-link:hover {
216
+ background: var(--surface-accent);
217
+ color: var(--text-primary);
218
+ border-color: var(--text-secondary);
219
+ }
220
+ .timestamp {
221
+ text-align: center;
222
+ margin-top: 2rem;
223
+ font-size: 0.75rem;
224
+ color: var(--text-secondary);
225
+ font-weight: 500;
226
+ }
227
+ @media (max-width: 640px) {
228
+ .stats-grid {
229
+ grid-template-columns: 1fr;
230
+ }
231
+ .stat-card.wide {
232
+ grid-column: span 1;
233
+ }
234
+ }
235
+ </style>
236
+ </head>
237
+ <body>
238
+ <div class="container">
239
+ <div class="header">
240
+ <div class="logo">K</div>
241
+ <h1>System Health</h1>
242
+ <p class="subtitle">Kyro CMS is running optimally</p>
243
+ <div class="status-badge">All Systems Operational</div>
244
+ </div>
245
+
246
+ <div class="stats-grid">
247
+ <div class="stat-card">
248
+ <div class="stat-label">Response Time</div>
249
+ <div class="stat-value">${responseTime}<span style="font-size: 1rem; font-weight: 500; color: var(--text-secondary);">ms</span></div>
250
+ <div class="stat-meta">Lightning fast</div>
251
+ </div>
252
+
253
+ <div class="stat-card">
254
+ <div class="stat-label">Server Uptime</div>
255
+ <div class="stat-value">
256
+ ${uptimeDays > 0 ? `${uptimeDays}d ` : ""}${uptimeHours}h <span style="font-size: 0.875rem; font-weight: 500; color: var(--text-secondary);">${uptimeMinutes}m</span>
257
+ </div>
258
+ <div class="stat-meta">Since last restart</div>
259
+ </div>
260
+
261
+ <div class="stat-card">
262
+ <div class="stat-label">Collections</div>
263
+ <div class="stat-value">${collectionNames.length}</div>
264
+ <div class="stat-meta">Active content types</div>
265
+ </div>
266
+
267
+ <div class="stat-card">
268
+ <div class="stat-label">Total Documents</div>
269
+ <div class="stat-value">${documentCount.toLocaleString()}</div>
270
+ <div class="stat-meta">Across all collections</div>
271
+ </div>
272
+
273
+ <div class="stat-card wide">
274
+ <div class="stat-label">Document Distribution</div>
275
+ <div class="collection-list">
276
+ ${collectionNames
277
+ .map(
278
+ (slug: string) => `
279
+ <div class="collection-item">
280
+ ${slug}
281
+ <span class="collection-count">${collectionCounts[slug] || 0}</span>
282
+ </div>
283
+ `,
284
+ )
285
+ .join("")}
286
+ </div>
287
+ </div>
288
+ </div>
289
+
290
+ <div class="nav">
291
+ <a href="/admin" class="nav-link">Dashboard</a>
292
+ <a href="/admin/api-explorer" class="nav-link">API Explorer</a>
293
+ <a href="/admin/graphql" class="nav-link">GraphQL</a>
294
+ <a href="/admin/keys" class="nav-link">API Keys</a>
295
+ <a href="/admin/webhooks" class="nav-link">Webhooks</a>
296
+ </div>
297
+
298
+ <div class="timestamp">
299
+ Last checked: ${new Date().toLocaleString()} • Kyro CMS v1.0
300
+ </div>
301
+ </div>
302
+ </body>
303
+ </html>`;
304
+
305
+ return new Response(html, {
306
+ status: 200,
307
+ headers: {
308
+ "Content-Type": "text/html",
34
309
  },
35
- );
310
+ });
36
311
  } catch (error) {
37
- return new Response(
38
- JSON.stringify({
39
- status: "unhealthy",
40
- error: error instanceof Error ? error.message : "Unknown error",
41
- timestamp: new Date().toISOString(),
42
- }),
43
- {
44
- status: 503,
45
- headers: { "Content-Type": "application/json" },
312
+ const errorMessage =
313
+ error instanceof Error ? error.message : "Unknown error";
314
+
315
+ const html = `<!DOCTYPE html>
316
+ <html lang="en">
317
+ <head>
318
+ <meta charset="UTF-8">
319
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
320
+ <title>System Health - Kyro CMS</title>
321
+ <link rel="preconnect" href="https://fonts.googleapis.com">
322
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
323
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
324
+ <style>
325
+ :root {
326
+ --bg: #eaeff2;
327
+ --surface: #ffffff;
328
+ --text-primary: #0b1222;
329
+ --text-secondary: #64748b;
330
+ --border: #e5e7eb;
331
+ --error: #ef4444;
332
+ }
333
+ @media (prefers-color-scheme: dark) {
334
+ :root {
335
+ --bg: #030712;
336
+ --surface: #0b1222;
337
+ --text-primary: #f8fafc;
338
+ --text-secondary: #94a3b8;
339
+ --border: #1e293b;
340
+ }
341
+ }
342
+ * { margin: 0; padding: 0; box-sizing: border-box; }
343
+ body {
344
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
345
+ background: var(--bg);
346
+ color: var(--text-primary);
347
+ min-height: 100vh;
348
+ display: flex;
349
+ align-items: center;
350
+ justify-content: center;
351
+ padding: 2rem;
352
+ }
353
+ .container {
354
+ max-width: 480px;
355
+ width: 100%;
356
+ text-align: center;
357
+ }
358
+ .logo {
359
+ display: inline-flex;
360
+ align-items: center;
361
+ justify-content: center;
362
+ width: 72px;
363
+ height: 72px;
364
+ background: var(--error);
365
+ color: white;
366
+ border-radius: 20px;
367
+ font-size: 28px;
368
+ font-weight: 900;
369
+ margin-bottom: 1.5rem;
370
+ }
371
+ h1 {
372
+ font-size: 2rem;
373
+ font-weight: 800;
374
+ letter-spacing: -0.03em;
375
+ margin-bottom: 0.5rem;
376
+ }
377
+ .error-badge {
378
+ display: inline-flex;
379
+ align-items: center;
380
+ gap: 0.5rem;
381
+ background: var(--error);
382
+ color: white;
383
+ padding: 0.625rem 1.25rem;
384
+ border-radius: 9999px;
385
+ font-size: 0.875rem;
386
+ font-weight: 700;
387
+ margin-top: 1.25rem;
388
+ }
389
+ .error-card {
390
+ background: var(--surface);
391
+ border: 1px solid var(--border);
392
+ border-radius: 16px;
393
+ padding: 1.5rem;
394
+ text-align: left;
395
+ margin-top: 1.5rem;
396
+ }
397
+ .error-code {
398
+ font-family: 'SF Mono', 'Monaco', monospace;
399
+ font-size: 0.8125rem;
400
+ color: var(--error);
401
+ word-break: break-all;
402
+ }
403
+ </style>
404
+ </head>
405
+ <body>
406
+ <div class="container">
407
+ <div class="header">
408
+ <div class="logo">!</div>
409
+ <h1>System Health</h1>
410
+ <div class="error-badge">System Error</div>
411
+ </div>
412
+ <div class="error-card">
413
+ <div class="error-code">${errorMessage}</div>
414
+ </div>
415
+ </div>
416
+ </body>
417
+ </html>`;
418
+
419
+ return new Response(html, {
420
+ status: 503,
421
+ headers: {
422
+ "Content-Type": "text/html",
46
423
  },
47
- );
424
+ });
48
425
  }
49
426
  };
@@ -0,0 +1,26 @@
1
+ import type { APIRoute } from "astro";
2
+ import { dataStore } from "../../../lib/dataStore";
3
+
4
+ export const DELETE: APIRoute = async ({ params }) => {
5
+ try {
6
+ const { id } = params;
7
+ if (!id) {
8
+ return new Response(JSON.stringify({ error: "ID is required" }), {
9
+ status: 400,
10
+ headers: { "Content-Type": "application/json" },
11
+ });
12
+ }
13
+
14
+ await dataStore.delete("_api_keys", id);
15
+
16
+ return new Response(JSON.stringify({ success: true }), {
17
+ status: 200,
18
+ headers: { "Content-Type": "application/json" },
19
+ });
20
+ } catch (error: any) {
21
+ return new Response(JSON.stringify({ error: error.message }), {
22
+ status: 500,
23
+ headers: { "Content-Type": "application/json" },
24
+ });
25
+ }
26
+ };
@@ -0,0 +1,75 @@
1
+ import type { APIRoute } from "astro";
2
+ import { dataStore } from "../../../lib/dataStore";
3
+
4
+ export const GET: APIRoute = async () => {
5
+ try {
6
+ const keys = await dataStore.find("_api_keys", { limit: 100, page: 1 });
7
+
8
+ const docs = keys.docs.map((k: any) => ({
9
+ id: k.id,
10
+ name: k.name,
11
+ key: k.key,
12
+ keyPrefix: k.key?.substring(0, 8) || "",
13
+ createdAt: k.createdAt,
14
+ lastUsedAt: k.lastUsedAt,
15
+ }));
16
+
17
+ return new Response(JSON.stringify(docs), {
18
+ status: 200,
19
+ headers: { "Content-Type": "application/json" },
20
+ });
21
+ } catch (error: any) {
22
+ return new Response(JSON.stringify({ error: error.message }), {
23
+ status: 500,
24
+ headers: { "Content-Type": "application/json" },
25
+ });
26
+ }
27
+ };
28
+
29
+ export const POST: APIRoute = async ({ request }) => {
30
+ try {
31
+ const body = await request.json();
32
+ const { name } = body;
33
+
34
+ if (!name) {
35
+ return new Response(JSON.stringify({ error: "Name is required" }), {
36
+ status: 400,
37
+ headers: { "Content-Type": "application/json" },
38
+ });
39
+ }
40
+
41
+ // Generate API key
42
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
43
+ let suffix = "";
44
+ for (let i = 0; i < 28; i++) {
45
+ suffix += chars[Math.floor(Math.random() * chars.length)];
46
+ }
47
+ const key = `kyro_${suffix}`;
48
+
49
+ const now = new Date().toISOString();
50
+ const doc = await dataStore.create("_api_keys", {
51
+ name,
52
+ key,
53
+ keyPrefix: key.substring(0, 8),
54
+ createdAt: now,
55
+ });
56
+
57
+ return new Response(
58
+ JSON.stringify({
59
+ id: doc.id,
60
+ name: doc.name,
61
+ key: doc.key,
62
+ createdAt: doc.createdAt,
63
+ }),
64
+ {
65
+ status: 201,
66
+ headers: { "Content-Type": "application/json" },
67
+ },
68
+ );
69
+ } catch (error: any) {
70
+ return new Response(JSON.stringify({ error: error.message }), {
71
+ status: 500,
72
+ headers: { "Content-Type": "application/json" },
73
+ });
74
+ }
75
+ };