@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,146 @@
1
+ import type { APIRoute } from "astro";
2
+ import { getMediaService, type MediaItem } from "../../../lib/MediaService";
3
+ import { constructMediaUrl, getStorageConfig } from "@/lib/storage";
4
+
5
+ export const GET: APIRoute = async ({ url }) => {
6
+ let mediaService: any = null;
7
+ try {
8
+ mediaService = await getMediaService();
9
+
10
+ const page = parseInt(url.searchParams.get("page") || "1");
11
+ const limit = parseInt(url.searchParams.get("limit") || "30");
12
+ const search = url.searchParams.get("search") || "";
13
+ const type = url.searchParams.get("type") || "";
14
+ const folder = url.searchParams.get("folder") || "";
15
+ const sortBy = url.searchParams.get("sortBy") || "createdAt";
16
+ const sortDir = (url.searchParams.get("sortDir") || "desc") as
17
+ | "asc"
18
+ | "desc";
19
+
20
+ const result = await mediaService.find({
21
+ page,
22
+ limit,
23
+ search: search || undefined,
24
+ type: type || undefined,
25
+ folder: folder || undefined,
26
+ sortBy,
27
+ sortDir,
28
+ });
29
+
30
+ // Get storage config to determine provider type
31
+ const storageConfig = await getStorageConfig();
32
+ const isLocalStorage = storageConfig.provider === "local";
33
+
34
+ // Compute URLs dynamically from storage settings
35
+ const isCloudStorage = !isLocalStorage;
36
+ const docs = await Promise.all(
37
+ result.docs.map(async (doc: MediaItem) => {
38
+ // For cloud storage, use the stored URL from DB directly
39
+ // For local storage, construct URL from baseUrl + filename
40
+ let mediaUrl: string;
41
+ if (isCloudStorage) {
42
+ // Cloudinary already stores full URL, use it directly from DB
43
+ mediaUrl = doc.url || (await constructMediaUrl(doc.filename, null));
44
+ } else {
45
+ mediaUrl = await constructMediaUrl(doc.filename, doc.folder);
46
+ }
47
+
48
+ // For local storage, use resize API. For cloud storage, use direct URL
49
+ let thumbnailUrl: string | undefined;
50
+ if (doc.mimeType?.startsWith("image/")) {
51
+ if (isLocalStorage) {
52
+ // Local storage: use resize API
53
+ thumbnailUrl = `/api/media/resize?url=${encodeURIComponent(mediaUrl)}&w=400&h=400`;
54
+ } else {
55
+ // Cloud storage: use the same URL
56
+ thumbnailUrl = mediaUrl;
57
+ }
58
+ }
59
+
60
+ return {
61
+ id: doc.id,
62
+ title: doc.title || doc.filename,
63
+ filename: doc.filename,
64
+ originalName: doc.originalName,
65
+ url: mediaUrl,
66
+ thumbnailUrl,
67
+ type: doc.mimeType?.split("/")[0] || "other",
68
+ mimeType: doc.mimeType,
69
+ fileSize: doc.fileSize,
70
+ folder: doc.folder,
71
+ alt: doc.alt,
72
+ caption: doc.caption,
73
+ createdAt: doc.createdAt,
74
+ updatedAt: doc.updatedAt,
75
+ };
76
+ }),
77
+ );
78
+
79
+ return new Response(
80
+ JSON.stringify({
81
+ docs,
82
+ totalDocs: result.totalDocs,
83
+ page: result.page,
84
+ limit: result.limit,
85
+ totalPages: result.totalPages,
86
+ }),
87
+ {
88
+ status: 200,
89
+ headers: { "Content-Type": "application/json" },
90
+ },
91
+ );
92
+ } catch (error: any) {
93
+ console.error("[media API] Error:", error?.message || error);
94
+ return new Response(
95
+ JSON.stringify({
96
+ error: error?.message || "Failed to fetch media",
97
+ docs: [],
98
+ totalDocs: 0,
99
+ }),
100
+ {
101
+ status: 500,
102
+ headers: { "Content-Type": "application/json" },
103
+ },
104
+ );
105
+ }
106
+ };
107
+
108
+ export const PATCH: APIRoute = async ({ request }) => {
109
+ try {
110
+ const mediaService = await getMediaService();
111
+ const body = await request.json();
112
+ const { id, ...data } = body;
113
+
114
+ const updated = await mediaService.update(id, data);
115
+
116
+ return new Response(JSON.stringify(updated), {
117
+ status: 200,
118
+ headers: { "Content-Type": "application/json" },
119
+ });
120
+ } catch (error: any) {
121
+ return new Response(
122
+ JSON.stringify({ error: error.message || "Failed to update media" }),
123
+ { status: 500, headers: { "Content-Type": "application/json" } },
124
+ );
125
+ }
126
+ };
127
+
128
+ export const DELETE: APIRoute = async ({ request }) => {
129
+ try {
130
+ const mediaService = await getMediaService();
131
+ const body = await request.json();
132
+ const { id } = body;
133
+
134
+ await mediaService.delete(id);
135
+
136
+ return new Response(JSON.stringify({ success: true }), {
137
+ status: 200,
138
+ headers: { "Content-Type": "application/json" },
139
+ });
140
+ } catch (error: any) {
141
+ return new Response(
142
+ JSON.stringify({ error: error.message || "Failed to delete media" }),
143
+ { status: 500, headers: { "Content-Type": "application/json" } },
144
+ );
145
+ }
146
+ };
@@ -0,0 +1,267 @@
1
+ import type { APIRoute } from "astro";
2
+ import sharp from "sharp";
3
+ import path from "path";
4
+ import fs from "fs/promises";
5
+ import fsSync from "fs";
6
+ import https from "https";
7
+ import { createHash } from "crypto";
8
+ import { getStorageConfig } from "@/lib/storage";
9
+
10
+ // Cache configuration
11
+ const CACHE_BASE = path.join(process.cwd(), ".cache", "kyro-media", "resize");
12
+ const MAX_CACHE_SIZE = 100 * 1024 * 1024; // 100MB
13
+ const MAX_CACHE_AGE_HOURS = 24 * 7; // 7 days
14
+
15
+ // Ensure cache directory exists
16
+ async function ensureCacheDir() {
17
+ try {
18
+ await fs.mkdir(CACHE_BASE, { recursive: true });
19
+ } catch {}
20
+ }
21
+
22
+ // Generate cache key from URL and parameters
23
+ function getCacheKey(
24
+ imageUrl: string,
25
+ width: number,
26
+ height: number,
27
+ quality: number,
28
+ format: string,
29
+ ): string {
30
+ const input = `${imageUrl}-${width}-${height}-${quality}-${format}`;
31
+ return createHash("md5").update(input).digest("hex") + `.${format}`;
32
+ }
33
+
34
+ // Get cache file path
35
+ function getCachePath(key: string): string {
36
+ return path.join(CACHE_BASE, key);
37
+ }
38
+
39
+ // Clean up old cache files when exceeding max size
40
+ async function cleanCache(): Promise<void> {
41
+ try {
42
+ const entries = await fs.readdir(CACHE_BASE, { withFileTypes: true });
43
+ const files = entries
44
+ .filter((e) => e.isFile())
45
+ .map((e) => ({
46
+ name: e.name,
47
+ path: path.join(CACHE_BASE, e.name),
48
+ mtime: fsSync.statSync(path.join(CACHE_BASE, e.name)).mtime,
49
+ size: fsSync.statSync(path.join(CACHE_BASE, e.name)).size,
50
+ }))
51
+ .sort((a, b) => a.mtime.getTime() - b.mtime.getTime()); // oldest first
52
+
53
+ let totalSize = files.reduce((sum, f) => sum + f.size, 0);
54
+ const maxSize = MAX_CACHE_SIZE;
55
+
56
+ // Delete oldest files until under limit
57
+ for (const file of files) {
58
+ if (totalSize < maxSize) break;
59
+ await fs.unlink(file.path);
60
+ totalSize -= file.size;
61
+ }
62
+ } catch {}
63
+ }
64
+
65
+ // Check if cache is valid (auto-invalidation on source modification)
66
+ async function isCacheValid(
67
+ cachePath: string,
68
+ sourcePath: string,
69
+ ): Promise<boolean> {
70
+ try {
71
+ const cacheStat = await fs.stat(cachePath);
72
+ const sourceStat = await fs.stat(sourcePath);
73
+
74
+ // Cache invalid if source is newer than cache
75
+ if (sourceStat.mtimeMs > cacheStat.mtimeMs) {
76
+ return false;
77
+ }
78
+
79
+ // Also check cache age
80
+ const cacheAgeHours = (Date.now() - cacheStat.mtimeMs) / (1000 * 60 * 60);
81
+ if (cacheAgeHours > MAX_CACHE_AGE_HOURS) {
82
+ return false;
83
+ }
84
+
85
+ return true;
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
90
+
91
+ // Get source file path for local files
92
+ async function getSourcePath(
93
+ imageUrl: string,
94
+ uploadDir: string,
95
+ baseUrl: string,
96
+ ): Promise<string | null> {
97
+ try {
98
+ let relativePath = imageUrl;
99
+ if (baseUrl !== "/" && imageUrl.startsWith(baseUrl)) {
100
+ relativePath = imageUrl.slice(baseUrl.length);
101
+ } else if (baseUrl === "/") {
102
+ relativePath = imageUrl.slice(1);
103
+ }
104
+ return path.join(uploadDir, relativePath);
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ // HTTP agent for remote files
111
+ const httpsAgent = new https.Agent({
112
+ keepAlive: true,
113
+ timeout: 30000,
114
+ });
115
+
116
+ // Fetch with custom agent
117
+ async function fetchWithAgent(url: string): Promise<Buffer> {
118
+ return new Promise((resolve, reject) => {
119
+ const parsed = new URL(url);
120
+ const options = {
121
+ hostname: parsed.hostname,
122
+ port: 443,
123
+ path: parsed.pathname + parsed.search,
124
+ method: "GET",
125
+ agent: httpsAgent,
126
+ };
127
+
128
+ const req = https.request(options, (res) => {
129
+ if (
130
+ res.statusCode &&
131
+ res.statusCode >= 300 &&
132
+ res.statusCode < 400 &&
133
+ res.headers.location
134
+ ) {
135
+ fetchWithAgent(res.headers.location).then(resolve).catch(reject);
136
+ return;
137
+ }
138
+ if (!res.statusCode || res.statusCode >= 400) {
139
+ reject(new Error(`HTTP ${res.statusCode}`));
140
+ return;
141
+ }
142
+
143
+ const chunks: Buffer[] = [];
144
+ res.on("data", (chunk) => chunks.push(chunk));
145
+ res.on("end", () => resolve(Buffer.concat(chunks)));
146
+ res.on("error", reject);
147
+ });
148
+
149
+ req.on("error", reject);
150
+ req.end();
151
+ });
152
+ }
153
+
154
+ export const GET: APIRoute = async ({ url }) => {
155
+ const refresh = url.searchParams.get("refresh") === "true";
156
+ let imageUrl = url.searchParams.get("url");
157
+ const width = parseInt(url.searchParams.get("w") || "0");
158
+ const height = parseInt(url.searchParams.get("h") || "0");
159
+ const quality = parseInt(url.searchParams.get("q") || "80");
160
+ const format = url.searchParams.get("f") || "webp";
161
+
162
+ if (!imageUrl) return new Response("Missing URL", { status: 400 });
163
+
164
+ // Prevent double-slash URLs
165
+ if (!imageUrl.startsWith("http")) {
166
+ imageUrl = imageUrl.replace(/^\/+/, "/");
167
+ }
168
+
169
+ const { uploadDir, baseUrl } = await getStorageConfig();
170
+ const cacheKey = getCacheKey(imageUrl, width, height, quality, format);
171
+ const cachePath = getCachePath(cacheKey);
172
+
173
+ // Ensure cache directory exists
174
+ await ensureCacheDir();
175
+
176
+ try {
177
+ let input: Buffer | null = null;
178
+ let useCache = false;
179
+
180
+ // Check cache first (if not refreshing)
181
+ if (!refresh) {
182
+ try {
183
+ const sourcePath = await getSourcePath(imageUrl, uploadDir, baseUrl);
184
+
185
+ if (sourcePath && (await isCacheValid(cachePath, sourcePath))) {
186
+ // Use cache
187
+ input = await fs.readFile(cachePath);
188
+ useCache = true;
189
+ console.log("[resize] Cache hit:", cacheKey);
190
+ }
191
+ } catch {}
192
+ }
193
+
194
+ // If not using cache, fetch and process
195
+ if (input === null) {
196
+ // Fetch original
197
+ if (imageUrl.startsWith("/")) {
198
+ // Local file
199
+ let relativePath = imageUrl;
200
+ if (baseUrl !== "/" && imageUrl.startsWith(baseUrl)) {
201
+ relativePath = imageUrl.slice(baseUrl.length);
202
+ } else if (baseUrl === "/") {
203
+ relativePath = imageUrl.slice(1);
204
+ }
205
+
206
+ const localPath = path.join(uploadDir, relativePath);
207
+ input = await fs.readFile(localPath);
208
+ } else {
209
+ // Remote file (S3/R2)
210
+ input = await fetchWithAgent(imageUrl);
211
+ }
212
+
213
+ // Process
214
+ let pipeline = sharp(input);
215
+ if (width > 0 || height > 0) {
216
+ pipeline = pipeline.resize(width || undefined, height || undefined, {
217
+ fit: "cover",
218
+ withoutEnlargement: true,
219
+ });
220
+ }
221
+
222
+ if (format === "webp") pipeline = pipeline.webp({ quality });
223
+ else if (format === "avif") pipeline = pipeline.avif({ quality });
224
+ else if (format === "jpg" || format === "jpeg")
225
+ pipeline = pipeline.jpeg({ quality });
226
+
227
+ const output = await pipeline.toBuffer();
228
+
229
+ // Write to cache
230
+ await fs.writeFile(cachePath, output);
231
+ console.log("[resize] Cached:", cacheKey);
232
+
233
+ // Clean cache if needed
234
+ await cleanCache();
235
+
236
+ input = output;
237
+ }
238
+
239
+ if (!input) {
240
+ throw new Error("Failed to process image");
241
+ }
242
+
243
+ return new Response(input as any, {
244
+ headers: {
245
+ "Content-Type": `image/${format}`,
246
+ "Cache-Control": "public, max-age=86400", // 1 day browser cache
247
+ },
248
+ });
249
+ } catch (error) {
250
+ console.error("Resize error:", error);
251
+
252
+ // Redirect to original on error
253
+ if (imageUrl) {
254
+ if (imageUrl.startsWith("http")) return Response.redirect(imageUrl, 302);
255
+ try {
256
+ const origin = url.origin;
257
+ return Response.redirect(
258
+ `${origin}${imageUrl.startsWith("/") ? "" : "/"}${imageUrl}`,
259
+ 302,
260
+ );
261
+ } catch {
262
+ return new Response("Error processing image", { status: 500 });
263
+ }
264
+ }
265
+ return new Response("Error processing image", { status: 500 });
266
+ }
267
+ };
@@ -0,0 +1,82 @@
1
+ import type { APIRoute } from "astro";
2
+
3
+ export const prerender = false;
4
+
5
+ export const GET: APIRoute = async ({ url }) => {
6
+ const query = url.searchParams.get("q") || "";
7
+ const limit = Math.min(parseInt(url.searchParams.get("limit") || "10"), 50);
8
+
9
+ if (!query || query.length < 2) {
10
+ return new Response(JSON.stringify({ results: [] }), {
11
+ status: 200,
12
+ headers: { "Content-Type": "application/json" },
13
+ });
14
+ }
15
+
16
+ const regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
17
+ const results: Array<{
18
+ collection: string;
19
+ label: string;
20
+ id: string;
21
+ title: string;
22
+ }> = [];
23
+
24
+ const demoResults = [
25
+ {
26
+ collection: "posts",
27
+ label: "Posts",
28
+ id: "1",
29
+ title: "Welcome to Kyro CMS",
30
+ },
31
+ {
32
+ collection: "posts",
33
+ label: "Posts",
34
+ id: "2",
35
+ title: "Getting Started Guide",
36
+ },
37
+ {
38
+ collection: "posts",
39
+ label: "Posts",
40
+ id: "3",
41
+ title: "Advanced Features",
42
+ },
43
+ { collection: "pages", label: "Pages", id: "about", title: "About Us" },
44
+ { collection: "pages", label: "Pages", id: "contact", title: "Contact" },
45
+ {
46
+ collection: "products",
47
+ label: "Products",
48
+ id: "prod-1",
49
+ title: "Sample Product",
50
+ },
51
+ {
52
+ collection: "products",
53
+ label: "Products",
54
+ id: "prod-2",
55
+ title: "Premium Plan",
56
+ },
57
+ {
58
+ collection: "categories",
59
+ label: "Categories",
60
+ id: "cat-1",
61
+ title: "Technology",
62
+ },
63
+ {
64
+ collection: "categories",
65
+ label: "Categories",
66
+ id: "cat-2",
67
+ title: "Business",
68
+ },
69
+ ];
70
+
71
+ for (const doc of demoResults) {
72
+ if (regex.test(doc.title)) {
73
+ results.push(doc);
74
+ if (results.length >= limit) break;
75
+ }
76
+ }
77
+
78
+ return new Response(JSON.stringify({ results: results.slice(0, limit) }), {
79
+ status: 200,
80
+ headers: { "Content-Type": "application/json" },
81
+ });
82
+ };
@@ -0,0 +1,70 @@
1
+ import type { APIRoute } from "astro";
2
+ import { dataStore } from "@/lib/dataStore";
3
+ import { collections } from "@/lib/config";
4
+
5
+ dataStore.initialize(collections);
6
+
7
+ export const POST: APIRoute = async ({ request }) => {
8
+ try {
9
+ const body = await request.json();
10
+ const { collection, slug } = body;
11
+
12
+ if (!collection || !slug) {
13
+ return new Response(
14
+ JSON.stringify({ error: "Collection and slug are required" }),
15
+ {
16
+ status: 400,
17
+ headers: { "Content-Type": "application/json" },
18
+ },
19
+ );
20
+ }
21
+
22
+ if (!collections[collection]) {
23
+ return new Response(JSON.stringify({ error: "Invalid collection" }), {
24
+ status: 404,
25
+ headers: { "Content-Type": "application/json" },
26
+ });
27
+ }
28
+
29
+ // Check if slug exists in collection
30
+ const existing = await dataStore.findOne(collection, { slug });
31
+
32
+ if (existing) {
33
+ // Generate a unique suggestion
34
+ const baseSlug = slug.replace(/-[a-z0-9]+$/i, "");
35
+ const suggested = `${baseSlug}-${Math.random().toString(36).slice(2, 6)}`;
36
+
37
+ return new Response(
38
+ JSON.stringify({
39
+ available: false,
40
+ slug,
41
+ suggested,
42
+ }),
43
+ {
44
+ status: 200,
45
+ headers: { "Content-Type": "application/json" },
46
+ },
47
+ );
48
+ }
49
+
50
+ return new Response(
51
+ JSON.stringify({
52
+ available: true,
53
+ slug,
54
+ }),
55
+ {
56
+ status: 200,
57
+ headers: { "Content-Type": "application/json" },
58
+ },
59
+ );
60
+ } catch (error) {
61
+ console.error("Slug availability check error:", error);
62
+ return new Response(
63
+ JSON.stringify({ error: "Failed to check slug availability" }),
64
+ {
65
+ status: 500,
66
+ headers: { "Content-Type": "application/json" },
67
+ },
68
+ );
69
+ }
70
+ };
@@ -0,0 +1,20 @@
1
+ import type { APIRoute } from "astro";
2
+ import { getStorageConfig } from "@/lib/storage";
3
+
4
+ export const GET: APIRoute = async () => {
5
+ try {
6
+ const config = await getStorageConfig();
7
+ return new Response(JSON.stringify(config), {
8
+ status: 200,
9
+ headers: { "Content-Type": "application/json" },
10
+ });
11
+ } catch (error) {
12
+ return new Response(
13
+ JSON.stringify({
14
+ error: "Failed to get storage config",
15
+ details: String(error),
16
+ }),
17
+ { status: 500, headers: { "Content-Type": "application/json" } },
18
+ );
19
+ }
20
+ };