@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,309 @@
1
+ import type { APIRoute } from "astro";
2
+ import { getMediaService } from "../../../lib/MediaService";
3
+ import { MediaService } from "@kyro-cms/core";
4
+ import { getStorageConfig, constructMediaUrl } from "../../../lib/storage";
5
+ import { getDatabaseConfig } from "../../../lib/db";
6
+ import path from "path";
7
+
8
+ export const GET: APIRoute = async ({ params }) => {
9
+ try {
10
+ const mediaService = await getMediaService();
11
+ const { id } = params;
12
+
13
+ if (!id) {
14
+ return new Response(JSON.stringify({ error: "No file ID provided" }), {
15
+ status: 400,
16
+ headers: { "Content-Type": "application/json" },
17
+ });
18
+ }
19
+
20
+ const doc = await mediaService.findById(id);
21
+ if (!doc) {
22
+ return new Response(JSON.stringify({ error: "Media not found" }), {
23
+ status: 404,
24
+ headers: { "Content-Type": "application/json" },
25
+ });
26
+ }
27
+
28
+ // Compute URLs dynamically from current storage settings
29
+ const storageConfig = await getStorageConfig();
30
+ const isLocalStorage = storageConfig.provider === "local";
31
+ const isCloudStorage = !isLocalStorage;
32
+
33
+ // For cloud storage, use stored URL from DB; for local, construct from config
34
+ const mediaUrl = isCloudStorage
35
+ ? doc.url || (await constructMediaUrl(doc.filename, null))
36
+ : await constructMediaUrl(doc.filename, doc.folder);
37
+
38
+ // For local storage use resize API, for cloud use direct URL
39
+ let thumbnailUrl: string | undefined;
40
+ if (doc.mimeType?.startsWith("image/")) {
41
+ if (isLocalStorage) {
42
+ thumbnailUrl = `/api/media/resize?url=${encodeURIComponent(mediaUrl)}&w=400&h=400`;
43
+ } else {
44
+ thumbnailUrl = mediaUrl;
45
+ }
46
+ }
47
+
48
+ return new Response(
49
+ JSON.stringify({
50
+ id: doc.id,
51
+ title: doc.title || doc.filename,
52
+ filename: doc.filename,
53
+ originalName: doc.originalName,
54
+ url: mediaUrl,
55
+ thumbnailUrl,
56
+ type: doc.mimeType?.split("/")[0] || "other",
57
+ mimeType: doc.mimeType,
58
+ fileSize: doc.fileSize,
59
+ width: doc.width,
60
+ height: doc.height,
61
+ folder: doc.folder,
62
+ alt: doc.alt,
63
+ caption: doc.caption,
64
+ createdAt: doc.createdAt,
65
+ updatedAt: doc.updatedAt,
66
+ }),
67
+ {
68
+ status: 200,
69
+ headers: { "Content-Type": "application/json" },
70
+ },
71
+ );
72
+ } catch (error) {
73
+ console.error("Get media error:", error);
74
+ return new Response(JSON.stringify({ error: "Failed to fetch media" }), {
75
+ status: 500,
76
+ headers: { "Content-Type": "application/json" },
77
+ });
78
+ }
79
+ };
80
+
81
+ export const PATCH: APIRoute = async ({ params, request }) => {
82
+ try {
83
+ const mediaService = await getMediaService();
84
+ const { id } = params;
85
+
86
+ if (!id) {
87
+ return new Response(JSON.stringify({ error: "No file ID provided" }), {
88
+ status: 400,
89
+ headers: { "Content-Type": "application/json" },
90
+ });
91
+ }
92
+
93
+ const body = await request.json();
94
+
95
+ // Fetch current record to compare
96
+ const current = await mediaService.findById(id);
97
+ if (!current) {
98
+ return new Response(JSON.stringify({ error: "Media not found" }), {
99
+ status: 404,
100
+ headers: { "Content-Type": "application/json" },
101
+ });
102
+ }
103
+
104
+ const updateData: any = {
105
+ alt: body.alt,
106
+ caption: body.caption,
107
+ title: body.title,
108
+ };
109
+
110
+ // If the title changed, rename the physical file and update url/filename
111
+ const newTitle: string | undefined = body.title;
112
+ if (
113
+ newTitle &&
114
+ newTitle !== current.title &&
115
+ newTitle !== current.filename
116
+ ) {
117
+ const storageConfig = await getStorageConfig();
118
+ const isLocalStorage = storageConfig.provider === "local";
119
+
120
+ // Preserve original extension, apply new name
121
+ const ext = path.extname(current.filename);
122
+ const sanitized = newTitle
123
+ .trim()
124
+ .toLowerCase()
125
+ .replace(/[^a-z0-9._-]/g, "_")
126
+ .replace(/_+/g, "_");
127
+ const newFilename = sanitized.endsWith(ext)
128
+ ? sanitized
129
+ : `${sanitized}${ext}`;
130
+
131
+ const folder = current.folder ? `${current.folder}/` : "";
132
+ const newKey = `${folder}${newFilename}`;
133
+
134
+ if (isLocalStorage) {
135
+ const uploadDir = storageConfig.uploadDir || "./public/uploads";
136
+ const baseUrl = storageConfig.baseUrl || "/uploads";
137
+ const folderPath = folder.replace("/", "");
138
+
139
+ const oldPhysical = path.join(uploadDir, folderPath, current.filename);
140
+ const newPhysical = path.join(uploadDir, folderPath, newFilename);
141
+
142
+ const fs = await import("fs/promises");
143
+ try {
144
+ await fs.rename(oldPhysical, newPhysical);
145
+ } catch (e: any) {
146
+ console.warn("[rename] Could not rename physical file:", e.message);
147
+ }
148
+
149
+ const normalizedBase = baseUrl.endsWith("/")
150
+ ? baseUrl.slice(0, -1)
151
+ : baseUrl;
152
+ updateData.url = `${normalizedBase}/${folder}${newFilename}`;
153
+ } else {
154
+ // Cloud storage - use MediaService rename
155
+ try {
156
+ const dbConfig = getDatabaseConfig();
157
+ let db: any = null;
158
+
159
+ if (dbConfig.type === "sqlite") {
160
+ const Database = (await import("better-sqlite3")).default;
161
+ db = new Database(dbConfig.contentDbPath || "./data/content.db");
162
+ } else if (dbConfig.type === "postgres") {
163
+ const { Pool } = await import("pg");
164
+ db = new Pool({ connectionString: dbConfig.connectionString });
165
+ } else if (dbConfig.type === "mysql") {
166
+ const mysql = await import("mysql2/promise");
167
+ db = await mysql.createPool({ uri: dbConfig.connectionString });
168
+ }
169
+
170
+ // Transform storageConfig to have 'type' field (for resolveProviderWithConfig)
171
+ const cloudStorageConfig = {
172
+ type: storageConfig.provider,
173
+ [storageConfig.provider]: storageConfig.config,
174
+ };
175
+
176
+ const coreMediaService = await MediaService.init(db, {
177
+ dialect: dbConfig.type as any,
178
+ storageConfig: cloudStorageConfig,
179
+ });
180
+
181
+ const renamed = await coreMediaService.rename(id, newKey);
182
+ if (renamed) {
183
+ updateData.url = renamed.url;
184
+ updateData.filename = renamed.filename;
185
+ if (renamed.thumbnailUrl) {
186
+ updateData.thumbnailUrl = renamed.thumbnailUrl;
187
+ }
188
+ }
189
+
190
+ if (db && dbConfig.type === "sqlite") db.close();
191
+ } catch (e: any) {
192
+ console.error(
193
+ "[rename] Cloud storage rename error:",
194
+ e.message,
195
+ e.stack,
196
+ );
197
+ // Continue anyway - still update DB metadata
198
+ }
199
+ }
200
+
201
+ if (!updateData.filename) {
202
+ updateData.filename = newFilename;
203
+ }
204
+ if (!updateData.url) {
205
+ const { baseUrl } = await getStorageConfig();
206
+ const normalizedBase = baseUrl.endsWith("/")
207
+ ? baseUrl.slice(0, -1)
208
+ : baseUrl;
209
+ updateData.url = `${normalizedBase}/${folder}${newFilename}`;
210
+ }
211
+ }
212
+
213
+ const updated = await mediaService.update(id, updateData);
214
+
215
+ return new Response(JSON.stringify({ success: true, media: updated }), {
216
+ status: 200,
217
+ headers: { "Content-Type": "application/json" },
218
+ });
219
+ } catch (error) {
220
+ console.error("Patch media error:", error);
221
+ return new Response(
222
+ JSON.stringify({ error: "Failed to update metadata" }),
223
+ {
224
+ status: 500,
225
+ headers: { "Content-Type": "application/json" },
226
+ },
227
+ );
228
+ }
229
+ };
230
+
231
+ export const DELETE: APIRoute = async ({ params }) => {
232
+ try {
233
+ const mediaService = await getMediaService();
234
+ const { id } = params;
235
+
236
+ if (!id) {
237
+ return new Response(JSON.stringify({ error: "No file ID provided" }), {
238
+ status: 400,
239
+ headers: { "Content-Type": "application/json" },
240
+ });
241
+ }
242
+
243
+ // Get the media item first to delete from storage
244
+ const item = await mediaService.findById(id);
245
+ if (!item) {
246
+ return new Response(JSON.stringify({ error: "Media not found" }), {
247
+ status: 404,
248
+ headers: { "Content-Type": "application/json" },
249
+ });
250
+ }
251
+
252
+ // Delete from cloud storage if not local
253
+ const storageConfig = await getStorageConfig();
254
+ if (storageConfig.provider !== "local") {
255
+ try {
256
+ const dbConfig = getDatabaseConfig();
257
+ let db: any = null;
258
+
259
+ if (dbConfig.type === "sqlite") {
260
+ const Database = (await import("better-sqlite3")).default;
261
+ db = new Database(dbConfig.contentDbPath || "./data/content.db");
262
+ } else if (dbConfig.type === "postgres") {
263
+ const { Pool } = await import("pg");
264
+ db = new Pool({ connectionString: dbConfig.connectionString });
265
+ } else if (dbConfig.type === "mysql") {
266
+ const mysql = await import("mysql2/promise");
267
+ db = await mysql.createPool({ uri: dbConfig.connectionString });
268
+ }
269
+
270
+ // Transform storageConfig to have 'type' field
271
+ const cloudStorageConfig = {
272
+ type: storageConfig.provider,
273
+ [storageConfig.provider]: storageConfig.config,
274
+ };
275
+
276
+ const coreMediaService = await MediaService.init(db, {
277
+ dialect: dbConfig.type as any,
278
+ storageConfig: cloudStorageConfig,
279
+ });
280
+
281
+ // Delete the file from storage
282
+ await coreMediaService.deleteFile(item.url);
283
+ if (item.thumbnailUrl) {
284
+ try {
285
+ await coreMediaService.deleteFile(item.thumbnailUrl);
286
+ } catch {}
287
+ }
288
+
289
+ if (db && dbConfig.type === "sqlite") db.close();
290
+ } catch (e: any) {
291
+ console.error("[delete] Cloud storage delete error:", e.message);
292
+ }
293
+ }
294
+
295
+ // Delete from database
296
+ await mediaService.delete(id);
297
+
298
+ return new Response(JSON.stringify({ success: true }), {
299
+ status: 200,
300
+ headers: { "Content-Type": "application/json" },
301
+ });
302
+ } catch (error) {
303
+ console.error("Delete media error:", error);
304
+ return new Response(JSON.stringify({ error: "Failed to delete file" }), {
305
+ status: 500,
306
+ headers: { "Content-Type": "application/json" },
307
+ });
308
+ }
309
+ };