@kyro-cms/admin 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -51
- package/package.json +52 -5
- package/src/collections/auth/index.ts +2 -2
- package/src/collections/portfolio/index.ts +343 -0
- package/src/components/ActionBar.tsx +153 -16
- package/src/components/Admin.tsx +136 -27
- package/src/components/ApiExplorer.tsx +325 -0
- package/src/components/ApiKeysManager.tsx +563 -0
- package/src/components/AuditLogsPage.tsx +664 -0
- package/src/components/AutoForm.tsx +1417 -661
- package/src/components/BrandingHub.tsx +267 -0
- package/src/components/BulkActionsBar.tsx +3 -3
- package/src/components/CreateView.tsx +3 -3
- package/src/components/Dashboard.tsx +393 -0
- package/src/components/DetailView.tsx +199 -57
- package/src/components/DeveloperCenter.tsx +403 -0
- package/src/components/EnhancedListView.tsx +786 -0
- package/src/components/GraphQLExplorer.tsx +675 -0
- package/src/components/GraphQLPlayground.tsx +627 -0
- package/src/components/ListView.tsx +191 -53
- package/src/components/MediaGallery.tsx +1569 -0
- package/src/components/Modal.tsx +149 -0
- package/src/components/RestPlayground.tsx +951 -0
- package/src/components/Sidebar.astro +237 -0
- package/src/components/UserManagement.tsx +204 -0
- package/src/components/VersionHistoryPanel.tsx +3 -3
- package/src/components/WebhookManager.tsx +608 -0
- package/src/components/blocks/AccordionBlock.tsx +97 -0
- package/src/components/blocks/ArrayBlock.tsx +75 -0
- package/src/components/blocks/BlockEditModal.MARKER +12 -0
- package/src/components/blocks/BlockEditModal.tsx +774 -0
- package/src/components/blocks/ButtonBlock.tsx +165 -0
- package/src/components/blocks/ChildBlocksTree.tsx +551 -0
- package/src/components/blocks/CodeBlock.tsx +66 -0
- package/src/components/blocks/ColumnsBlock.tsx +151 -0
- package/src/components/blocks/DividerBlock.tsx +43 -0
- package/src/components/blocks/FileBlock.tsx +64 -0
- package/src/components/blocks/HeadingBlock.tsx +81 -0
- package/src/components/blocks/HeroBlock.tsx +157 -0
- package/src/components/blocks/ImageBlock.tsx +83 -0
- package/src/components/blocks/LinkBlock.tsx +71 -0
- package/src/components/blocks/ListBlock.tsx +39 -0
- package/src/components/blocks/ParagraphBlock.tsx +61 -0
- package/src/components/blocks/RelationshipBlock.tsx +279 -0
- package/src/components/blocks/VStackBlock.tsx +75 -0
- package/src/components/blocks/VideoBlock.tsx +45 -0
- package/src/components/blocks/index.ts +10 -0
- package/src/components/fields/BlocksField.tsx +323 -0
- package/src/components/fields/CheckboxField.tsx +15 -9
- package/src/components/fields/CodeField.tsx +234 -0
- package/src/components/fields/DateField.tsx +38 -11
- package/src/components/fields/EditorClient.tsx +271 -0
- package/src/components/fields/FileField.tsx +390 -0
- package/src/components/fields/HybridContentField.tsx +109 -0
- package/src/components/fields/ImageField.tsx +429 -0
- package/src/components/fields/JSONField.tsx +361 -0
- package/src/components/fields/MarkdownField.tsx +282 -0
- package/src/components/fields/NumberField.tsx +42 -12
- package/src/components/fields/PortableTextField.tsx +143 -0
- package/src/components/fields/PortableTextRenderer.tsx +68 -0
- package/src/components/fields/RelationshipField.tsx +231 -59
- package/src/components/fields/SelectField.tsx +25 -15
- package/src/components/fields/TextField.tsx +45 -14
- package/src/components/fields/extensions/blockComponents.tsx +237 -0
- package/src/components/fields/extensions/blocksStore.ts +273 -0
- package/src/components/fields/index.ts +13 -0
- package/src/components/index.ts +1 -2
- package/src/components/layout/Header.tsx +2 -2
- package/src/components/layout/Layout.tsx +2 -2
- package/src/components/ui/Badge.tsx +9 -4
- package/src/components/ui/BlockDrawer.tsx +79 -0
- package/src/components/ui/Button.tsx +1 -1
- package/src/components/ui/CommandPalette.tsx +362 -0
- package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
- package/src/components/ui/Dropdown.tsx +1 -1
- package/src/components/ui/Modal.tsx +37 -12
- package/src/components/ui/PromptModal.tsx +94 -0
- package/src/components/ui/SlidePanel.tsx +43 -16
- package/src/components/ui/Toast.tsx +80 -14
- package/src/env.d.ts +16 -0
- package/src/env.ts +20 -0
- package/src/index.ts +0 -1
- package/src/layouts/AdminLayout.astro +164 -170
- package/src/layouts/AuthLayout.astro +50 -0
- package/src/lib/MediaService.ts +541 -0
- package/src/lib/auth/sqlite-adapter.ts +319 -0
- package/src/lib/config.ts +22 -6
- package/src/lib/dataStore.ts +132 -74
- package/src/lib/db/adapter.ts +54 -0
- package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
- package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
- package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
- package/src/lib/db/index.ts +449 -0
- package/src/lib/db/mongodb-adapter.ts +207 -0
- package/src/lib/db/mongodb-auth-adapter.ts +305 -0
- package/src/lib/db/schema/mysql-auth.ts +113 -0
- package/src/lib/db/schema/mysql-content.ts +20 -0
- package/src/lib/db/schema/postgres-auth.ts +116 -0
- package/src/lib/db/schema/postgres-content.ts +35 -0
- package/src/lib/db/schema/postgres-media.ts +52 -0
- package/src/lib/db/schema/postgres-settings.ts +11 -0
- package/src/lib/db/schema/sqlite-auth.ts +112 -0
- package/src/lib/db/schema/sqlite-content.ts +20 -0
- package/src/lib/graphql/index.ts +1 -0
- package/src/lib/graphql/schema.ts +443 -0
- package/src/lib/rate-limit.ts +267 -0
- package/src/lib/storage.ts +374 -0
- package/src/lib/store.ts +85 -0
- package/src/middleware.ts +116 -28
- package/src/pages/[collection]/[id].astro +178 -122
- package/src/pages/[collection]/index.astro +24 -156
- package/src/pages/admin/api-explorer.astro +98 -0
- package/src/pages/admin/graphql-explorer.astro +40 -0
- package/src/pages/admin/graphql.astro +97 -0
- package/src/pages/admin/index.astro +286 -0
- package/src/pages/admin/keys.astro +8 -0
- package/src/pages/admin/rest-playground.astro +44 -0
- package/src/pages/admin/webhooks.astro +8 -0
- package/src/pages/api/[collection]/[id]/publish.ts +44 -0
- package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
- package/src/pages/api/[collection]/[id]/versions.ts +36 -0
- package/src/pages/api/[collection]/[id].ts +102 -159
- package/src/pages/api/[collection]/index.ts +151 -230
- package/src/pages/api/auth/[id].ts +48 -69
- package/src/pages/api/auth/audit-logs.ts +20 -43
- package/src/pages/api/auth/login.ts +159 -45
- package/src/pages/api/auth/logout.ts +50 -20
- package/src/pages/api/auth/refresh.ts +119 -0
- package/src/pages/api/auth/register.ts +110 -40
- package/src/pages/api/auth/users.ts +22 -97
- package/src/pages/api/collections.ts +59 -0
- package/src/pages/api/globals/[slug]/test.ts +172 -0
- package/src/pages/api/globals/[slug].ts +42 -0
- package/src/pages/api/graphql.ts +90 -0
- package/src/pages/api/health.ts +417 -40
- package/src/pages/api/keys/[id].ts +26 -0
- package/src/pages/api/keys/index.ts +75 -0
- package/src/pages/api/media/[id].ts +309 -0
- package/src/pages/api/media/folders.ts +609 -0
- package/src/pages/api/media/index.ts +146 -0
- package/src/pages/api/media/resize.ts +267 -0
- package/src/pages/api/search.ts +82 -0
- package/src/pages/api/slug-availability.ts +70 -0
- package/src/pages/api/storage-config.ts +20 -0
- package/src/pages/api/storage-status.ts +206 -0
- package/src/pages/api/upload.ts +334 -0
- package/src/pages/api/webhooks/index.ts +71 -0
- package/src/pages/audit/index.astro +2 -104
- package/src/pages/login.astro +82 -0
- package/src/pages/media.astro +10 -0
- package/src/pages/preview/[collection]/[id].astro +178 -0
- package/src/pages/register.astro +102 -0
- package/src/pages/roles/index.astro +21 -21
- package/src/pages/settings/[slug].astro +162 -0
- package/src/pages/settings/index.astro +9 -0
- package/src/pages/users/[id].astro +29 -21
- package/src/pages/users/index.astro +22 -17
- package/src/pages/users/new.astro +18 -17
- package/src/styles/main.css +553 -128
- package/src/components/layout/Sidebar.tsx +0 -497
- package/src/pages/index.astro +0 -225
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { APIRoute } from "astro";
|
|
2
|
+
import { getDatabaseConfig } from "@/lib/db";
|
|
3
|
+
|
|
4
|
+
interface RawStorageSettings {
|
|
5
|
+
provider?: string;
|
|
6
|
+
local?: {
|
|
7
|
+
uploadDir?: string;
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
};
|
|
10
|
+
aws?: Record<string, any>;
|
|
11
|
+
r2?: Record<string, any>;
|
|
12
|
+
gcs?: Record<string, any>;
|
|
13
|
+
digitalocean?: Record<string, any>;
|
|
14
|
+
backblaze?: Record<string, any>;
|
|
15
|
+
wasabi?: Record<string, any>;
|
|
16
|
+
bunny?: Record<string, any>;
|
|
17
|
+
cloudinary?: Record<string, any>;
|
|
18
|
+
imgix?: Record<string, any>;
|
|
19
|
+
ftp?: Record<string, any>;
|
|
20
|
+
limits?: Record<string, any>;
|
|
21
|
+
[key: string]: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validateProviderConfig(
|
|
25
|
+
provider: string,
|
|
26
|
+
config: RawStorageSettings,
|
|
27
|
+
): { valid: boolean; missing: string[] } {
|
|
28
|
+
const missing: string[] = [];
|
|
29
|
+
|
|
30
|
+
switch (provider) {
|
|
31
|
+
case "local": {
|
|
32
|
+
const local = config.local || {};
|
|
33
|
+
if (!local.uploadDir || local.uploadDir.trim() === "") {
|
|
34
|
+
missing.push("Upload Directory");
|
|
35
|
+
}
|
|
36
|
+
if (
|
|
37
|
+
!local.baseUrl ||
|
|
38
|
+
local.baseUrl.trim() === "" ||
|
|
39
|
+
local.baseUrl === "/"
|
|
40
|
+
) {
|
|
41
|
+
missing.push("Base URL");
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
case "aws": {
|
|
47
|
+
const aws = config.aws || {};
|
|
48
|
+
if (!aws.bucket?.trim()) missing.push("Bucket Name");
|
|
49
|
+
if (!aws.accessKeyId?.trim()) missing.push("Access Key ID");
|
|
50
|
+
if (!aws.secretAccessKey?.trim()) missing.push("Secret Access Key");
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
case "r2": {
|
|
55
|
+
const r2 = config.r2 || {};
|
|
56
|
+
if (!r2.bucket?.trim()) missing.push("Bucket Name");
|
|
57
|
+
if (!r2.accountId?.trim()) missing.push("Account ID");
|
|
58
|
+
if (!r2.accessKeyId?.trim()) missing.push("Access Key ID");
|
|
59
|
+
if (!r2.secretAccessKey?.trim()) missing.push("Secret Access Key");
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case "gcs": {
|
|
64
|
+
const gcs = config.gcs || {};
|
|
65
|
+
if (!gcs.bucket?.trim()) missing.push("Bucket Name");
|
|
66
|
+
if (!gcs.projectId?.trim()) missing.push("Project ID");
|
|
67
|
+
if (!gcs.clientEmail?.trim()) missing.push("Client Email");
|
|
68
|
+
if (!gcs.privateKey?.trim()) missing.push("Private Key");
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case "digitalocean": {
|
|
73
|
+
const doConfig = config.digitalocean || {};
|
|
74
|
+
if (!doConfig.bucket?.trim()) missing.push("Space Name");
|
|
75
|
+
if (!doConfig.accessKeyId?.trim()) missing.push("Access Key ID");
|
|
76
|
+
if (!doConfig.secretAccessKey?.trim()) missing.push("Secret Access Key");
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case "backblaze": {
|
|
81
|
+
const b2 = config.backblaze || {};
|
|
82
|
+
if (!b2.bucket?.trim()) missing.push("Bucket Name");
|
|
83
|
+
if (!b2.accountId?.trim()) missing.push("Account ID");
|
|
84
|
+
if (!b2.applicationKeyId?.trim()) missing.push("Application Key ID");
|
|
85
|
+
if (!b2.applicationKey?.trim()) missing.push("Application Key");
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case "wasabi": {
|
|
90
|
+
const wasabi = config.wasabi || {};
|
|
91
|
+
if (!wasabi.bucket?.trim()) missing.push("Bucket Name");
|
|
92
|
+
if (!wasabi.accessKeyId?.trim()) missing.push("Access Key ID");
|
|
93
|
+
if (!wasabi.secretAccessKey?.trim()) missing.push("Secret Access Key");
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
case "bunny": {
|
|
98
|
+
const bunny = config.bunny || {};
|
|
99
|
+
if (!bunny.storageZone?.trim()) missing.push("Storage Zone");
|
|
100
|
+
if (!bunny.apiKey?.trim()) missing.push("API Key");
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case "cloudinary": {
|
|
105
|
+
const cloudinary = config.cloudinary || {};
|
|
106
|
+
if (!cloudinary.cloudName?.trim()) missing.push("Cloud Name");
|
|
107
|
+
if (!cloudinary.apiKey?.trim()) missing.push("API Key");
|
|
108
|
+
if (!cloudinary.apiSecret?.trim()) missing.push("API Secret");
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case "imgix": {
|
|
113
|
+
const imgix = config.imgix || {};
|
|
114
|
+
if (!imgix.domain?.trim()) missing.push("Imgix Domain");
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case "ftp": {
|
|
119
|
+
const ftp = config.ftp || {};
|
|
120
|
+
if (!ftp.host?.trim()) missing.push("Host");
|
|
121
|
+
if (!ftp.user?.trim()) missing.push("Username");
|
|
122
|
+
if (!ftp.password?.trim()) missing.push("Password");
|
|
123
|
+
if (!ftp.baseUrl?.trim()) missing.push("Base URL");
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
default:
|
|
128
|
+
missing.push("Unknown provider");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { valid: missing.length === 0, missing };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const GET: APIRoute = async () => {
|
|
135
|
+
const dbConfig = getDatabaseConfig();
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
if (dbConfig.type === "sqlite") {
|
|
139
|
+
const Database = (await import("better-sqlite3")).default;
|
|
140
|
+
const db = new Database(dbConfig.contentDbPath || "./data/content.db");
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const row = db
|
|
144
|
+
.prepare("SELECT data FROM globals WHERE slug = ?")
|
|
145
|
+
.get("storage-settings") as { data: string } | undefined;
|
|
146
|
+
|
|
147
|
+
if (!row) {
|
|
148
|
+
return new Response(
|
|
149
|
+
JSON.stringify({
|
|
150
|
+
configured: false,
|
|
151
|
+
reason: "no-settings",
|
|
152
|
+
provider: null,
|
|
153
|
+
missingFields: [],
|
|
154
|
+
}),
|
|
155
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const data: RawStorageSettings =
|
|
160
|
+
typeof row.data === "string" ? JSON.parse(row.data) : row.data;
|
|
161
|
+
const provider = data.provider || "local";
|
|
162
|
+
|
|
163
|
+
// Validate based on provider
|
|
164
|
+
const validation = validateProviderConfig(provider, data);
|
|
165
|
+
|
|
166
|
+
if (!validation.valid) {
|
|
167
|
+
return new Response(
|
|
168
|
+
JSON.stringify({
|
|
169
|
+
configured: false,
|
|
170
|
+
reason: "incomplete-config",
|
|
171
|
+
provider,
|
|
172
|
+
missingFields: validation.missing,
|
|
173
|
+
}),
|
|
174
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return new Response(
|
|
179
|
+
JSON.stringify({
|
|
180
|
+
configured: true,
|
|
181
|
+
reason: "configured",
|
|
182
|
+
provider,
|
|
183
|
+
missingFields: [],
|
|
184
|
+
}),
|
|
185
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
186
|
+
);
|
|
187
|
+
} finally {
|
|
188
|
+
db.close();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return new Response(
|
|
193
|
+
JSON.stringify({ configured: false, reason: "unsupported-db" }),
|
|
194
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
195
|
+
);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
return new Response(
|
|
198
|
+
JSON.stringify({
|
|
199
|
+
configured: false,
|
|
200
|
+
reason: "error",
|
|
201
|
+
details: String(error),
|
|
202
|
+
}),
|
|
203
|
+
{ status: 500, headers: { "Content-Type": "application/json" } },
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import type { APIRoute } from "astro";
|
|
2
|
+
import { MediaService, type Dialect } from "@kyro-cms/core";
|
|
3
|
+
import { getDatabaseConfig, runMigrations } from "@/lib/db";
|
|
4
|
+
|
|
5
|
+
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
|
6
|
+
|
|
7
|
+
const ALLOWED_TYPES = [
|
|
8
|
+
"image/jpeg",
|
|
9
|
+
"image/png",
|
|
10
|
+
"image/gif",
|
|
11
|
+
"image/webp",
|
|
12
|
+
"image/avif",
|
|
13
|
+
"image/svg+xml",
|
|
14
|
+
"video/mp4",
|
|
15
|
+
"video/webm",
|
|
16
|
+
"video/ogg",
|
|
17
|
+
"application/pdf",
|
|
18
|
+
"application/zip",
|
|
19
|
+
"text/plain",
|
|
20
|
+
"application/octet-stream",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
async function ensureMediaTable(db: any) {
|
|
24
|
+
try {
|
|
25
|
+
db.exec(`
|
|
26
|
+
CREATE TABLE IF NOT EXISTS media (
|
|
27
|
+
id TEXT PRIMARY KEY,
|
|
28
|
+
filename TEXT NOT NULL UNIQUE,
|
|
29
|
+
title TEXT,
|
|
30
|
+
original_name TEXT NOT NULL,
|
|
31
|
+
mime_type TEXT NOT NULL,
|
|
32
|
+
file_size INTEGER NOT NULL,
|
|
33
|
+
width INTEGER,
|
|
34
|
+
height INTEGER,
|
|
35
|
+
url TEXT NOT NULL UNIQUE,
|
|
36
|
+
thumbnail_url TEXT,
|
|
37
|
+
folder TEXT,
|
|
38
|
+
provider TEXT NOT NULL DEFAULT 'local',
|
|
39
|
+
alt TEXT,
|
|
40
|
+
caption TEXT,
|
|
41
|
+
metadata TEXT,
|
|
42
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
43
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
44
|
+
)
|
|
45
|
+
`);
|
|
46
|
+
} catch (e: any) {
|
|
47
|
+
console.warn("[upload] Media table error:", e.message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
52
|
+
const dbConfig = getDatabaseConfig();
|
|
53
|
+
|
|
54
|
+
let db: any = null;
|
|
55
|
+
let dialect: Dialect = "sqlite";
|
|
56
|
+
let genId = () => crypto.randomUUID();
|
|
57
|
+
|
|
58
|
+
// Connect to admin's database (same as admin UI uses)
|
|
59
|
+
try {
|
|
60
|
+
if (dbConfig.type === "sqlite") {
|
|
61
|
+
const Database = (await import("better-sqlite3")).default;
|
|
62
|
+
db = new Database(dbConfig.contentDbPath || "./data/content.db");
|
|
63
|
+
dialect = "sqlite";
|
|
64
|
+
} else if (dbConfig.type === "postgres") {
|
|
65
|
+
const { Pool } = await import("pg");
|
|
66
|
+
db = new Pool({
|
|
67
|
+
connectionString:
|
|
68
|
+
dbConfig.connectionString ||
|
|
69
|
+
`postgresql://${dbConfig.username}:${dbConfig.password}@${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`,
|
|
70
|
+
});
|
|
71
|
+
dialect = "postgres";
|
|
72
|
+
} else if (dbConfig.type === "mysql") {
|
|
73
|
+
const mysql = await import("mysql2/promise");
|
|
74
|
+
db = await mysql.createPool({
|
|
75
|
+
uri:
|
|
76
|
+
dbConfig.connectionString ||
|
|
77
|
+
`mysql://${dbConfig.username}:${dbConfig.password}@${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`,
|
|
78
|
+
});
|
|
79
|
+
dialect = "mysql";
|
|
80
|
+
}
|
|
81
|
+
} catch (e: any) {
|
|
82
|
+
return new Response(
|
|
83
|
+
JSON.stringify({ error: "Database connection failed: " + e.message }),
|
|
84
|
+
{ status: 500, headers: { "Content-Type": "application/json" } },
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Get storage config from globals table
|
|
89
|
+
let storageConfig: any = undefined;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
if (dbConfig.type === "sqlite") {
|
|
93
|
+
const stmt = db.prepare("SELECT * FROM globals WHERE slug = ?");
|
|
94
|
+
const row = stmt.get("storage-settings") as any;
|
|
95
|
+
if (row) {
|
|
96
|
+
storageConfig =
|
|
97
|
+
typeof row.data === "string" ? JSON.parse(row.data) : row.data;
|
|
98
|
+
if (storageConfig.provider && !storageConfig.type) {
|
|
99
|
+
storageConfig.type = storageConfig.provider;
|
|
100
|
+
}
|
|
101
|
+
console.log(
|
|
102
|
+
"[upload] Storage config loaded:",
|
|
103
|
+
JSON.stringify(
|
|
104
|
+
{
|
|
105
|
+
provider: storageConfig.provider,
|
|
106
|
+
type: storageConfig.type,
|
|
107
|
+
r2: storageConfig.r2
|
|
108
|
+
? {
|
|
109
|
+
bucket: storageConfig.r2.bucket,
|
|
110
|
+
accountId: storageConfig.r2.accountId,
|
|
111
|
+
publicDevUrl: storageConfig.r2.publicDevUrl,
|
|
112
|
+
prefix: storageConfig.r2.prefix,
|
|
113
|
+
}
|
|
114
|
+
: undefined,
|
|
115
|
+
},
|
|
116
|
+
null,
|
|
117
|
+
2,
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
} else if (dbConfig.type === "postgres" || dbConfig.type === "mysql") {
|
|
122
|
+
const result = await db.query("SELECT * FROM globals WHERE slug = ?", [
|
|
123
|
+
"storage-settings",
|
|
124
|
+
]);
|
|
125
|
+
const rows = result.rows || result[0] || [];
|
|
126
|
+
if (rows.length > 0) {
|
|
127
|
+
storageConfig = rows[0].data;
|
|
128
|
+
if (storageConfig.provider && !storageConfig.type) {
|
|
129
|
+
storageConfig.type = storageConfig.provider;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (e: any) {
|
|
134
|
+
console.warn("[upload] Error reading storage config:", e.message);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fallback to environment or default
|
|
138
|
+
if (!storageConfig && process.env.STORAGE_TYPE) {
|
|
139
|
+
storageConfig = {
|
|
140
|
+
type: process.env.STORAGE_TYPE,
|
|
141
|
+
[process.env.STORAGE_TYPE]: {
|
|
142
|
+
bucket: process.env.STORAGE_BUCKET,
|
|
143
|
+
region: process.env.STORAGE_REGION,
|
|
144
|
+
accessKeyId: process.env.STORAGE_ACCESS_KEY_ID,
|
|
145
|
+
secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!storageConfig) {
|
|
151
|
+
storageConfig = { type: "local" };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Recreate media table
|
|
155
|
+
if (dbConfig.type === "sqlite") {
|
|
156
|
+
await ensureMediaTable(db);
|
|
157
|
+
} else {
|
|
158
|
+
try {
|
|
159
|
+
await runMigrations();
|
|
160
|
+
} catch (e: any) {
|
|
161
|
+
console.warn("[upload] runMigrations:", e.message);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Initialize MediaService
|
|
166
|
+
let mediaService;
|
|
167
|
+
try {
|
|
168
|
+
console.log(
|
|
169
|
+
"[upload] About to init MediaService with config:",
|
|
170
|
+
JSON.stringify(
|
|
171
|
+
{
|
|
172
|
+
type: storageConfig.type,
|
|
173
|
+
r2: storageConfig.r2
|
|
174
|
+
? {
|
|
175
|
+
bucket: storageConfig.r2.bucket,
|
|
176
|
+
accountId: storageConfig.r2.accountId,
|
|
177
|
+
accessKeyId: storageConfig.r2.accessKeyId ? "SET" : "UNDEFINED",
|
|
178
|
+
secretAccessKey: storageConfig.r2.secretAccessKey
|
|
179
|
+
? "SET"
|
|
180
|
+
: "UNDEFINED",
|
|
181
|
+
publicDevUrl: storageConfig.r2.publicDevUrl,
|
|
182
|
+
prefix: storageConfig.r2.prefix,
|
|
183
|
+
}
|
|
184
|
+
: undefined,
|
|
185
|
+
},
|
|
186
|
+
null,
|
|
187
|
+
2,
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
mediaService = await MediaService.init(db, {
|
|
192
|
+
dialect,
|
|
193
|
+
genId,
|
|
194
|
+
storageConfig,
|
|
195
|
+
});
|
|
196
|
+
} catch (e: any) {
|
|
197
|
+
return new Response(
|
|
198
|
+
JSON.stringify({ error: "Failed to init media service: " + e.message }),
|
|
199
|
+
{ status: 500, headers: { "Content-Type": "application/json" } },
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Handle file upload
|
|
204
|
+
try {
|
|
205
|
+
const contentType = request.headers.get("Content-Type") || "";
|
|
206
|
+
|
|
207
|
+
if (contentType.includes("application/json")) {
|
|
208
|
+
const body = await request.json();
|
|
209
|
+
const { url, folder } = body;
|
|
210
|
+
|
|
211
|
+
if (url) {
|
|
212
|
+
try {
|
|
213
|
+
const isExternalUrl =
|
|
214
|
+
url.startsWith("http://") || url.startsWith("https://");
|
|
215
|
+
|
|
216
|
+
if (isExternalUrl) {
|
|
217
|
+
const response = await fetch(url);
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
return new Response(
|
|
220
|
+
JSON.stringify({
|
|
221
|
+
error: "Failed to fetch URL: " + response.statusText,
|
|
222
|
+
}),
|
|
223
|
+
{
|
|
224
|
+
status: 400,
|
|
225
|
+
headers: { "Content-Type": "application/json" },
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const blob = await response.blob();
|
|
230
|
+
const contentDisposition = response.headers.get(
|
|
231
|
+
"Content-Disposition",
|
|
232
|
+
);
|
|
233
|
+
const filenameMatch = contentDisposition?.match(
|
|
234
|
+
/filename[^;]*=([^";\s]+)/,
|
|
235
|
+
);
|
|
236
|
+
const filename =
|
|
237
|
+
filenameMatch?.[1] || url.split("/").pop() || "url-upload";
|
|
238
|
+
|
|
239
|
+
const file = new File([blob], filename, {
|
|
240
|
+
type: blob.type || "image/*",
|
|
241
|
+
});
|
|
242
|
+
const media = await mediaService.upload(file, folder || "");
|
|
243
|
+
return new Response(JSON.stringify(media), {
|
|
244
|
+
status: 200,
|
|
245
|
+
headers: { "Content-Type": "application/json" },
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const fileMatches = url.match(/\/uploads\/(.+)/);
|
|
250
|
+
if (fileMatches) {
|
|
251
|
+
const fullPath = fileMatches[1];
|
|
252
|
+
const fullUrl =
|
|
253
|
+
new URL(url, request.url).origin + "/uploads/" + fullPath;
|
|
254
|
+
const { default: fs } = await import("fs/promises");
|
|
255
|
+
const path = await import("path");
|
|
256
|
+
const filePath = path.join(
|
|
257
|
+
process.cwd(),
|
|
258
|
+
"public",
|
|
259
|
+
"uploads",
|
|
260
|
+
fullPath,
|
|
261
|
+
);
|
|
262
|
+
const data = await fs.readFile(filePath);
|
|
263
|
+
const file = new File([data], fullPath);
|
|
264
|
+
const media = await mediaService.upload(file, folder || "");
|
|
265
|
+
return new Response(JSON.stringify(media), {
|
|
266
|
+
status: 200,
|
|
267
|
+
headers: { "Content-Type": "application/json" },
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
} catch (fetchErr: any) {
|
|
271
|
+
return new Response(
|
|
272
|
+
JSON.stringify({ error: "Failed to add URL: " + fetchErr.message }),
|
|
273
|
+
{
|
|
274
|
+
status: 400,
|
|
275
|
+
headers: { "Content-Type": "application/json" },
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return new Response(
|
|
282
|
+
JSON.stringify({ error: "No file or URL provided" }),
|
|
283
|
+
{
|
|
284
|
+
status: 400,
|
|
285
|
+
headers: { "Content-Type": "application/json" },
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const formData = await request.formData();
|
|
291
|
+
const file = formData.get("file") as File | null;
|
|
292
|
+
const folder = (formData.get("folder") as string) || "";
|
|
293
|
+
|
|
294
|
+
if (!file) {
|
|
295
|
+
return new Response(JSON.stringify({ error: "No file provided" }), {
|
|
296
|
+
status: 400,
|
|
297
|
+
headers: { "Content-Type": "application/json" },
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!ALLOWED_TYPES.includes(file.type)) {
|
|
302
|
+
return new Response(
|
|
303
|
+
JSON.stringify({ error: `Invalid file type: ${file.type}` }),
|
|
304
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
309
|
+
return new Response(
|
|
310
|
+
JSON.stringify({
|
|
311
|
+
error: `File too large. Max size: ${MAX_FILE_SIZE / 1024 / 1024}MB`,
|
|
312
|
+
}),
|
|
313
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const media = await mediaService.upload(file, folder);
|
|
318
|
+
|
|
319
|
+
return new Response(JSON.stringify(media), {
|
|
320
|
+
status: 200,
|
|
321
|
+
headers: { "Content-Type": "application/json" },
|
|
322
|
+
});
|
|
323
|
+
} catch (error: any) {
|
|
324
|
+
console.error("Upload error:", error);
|
|
325
|
+
return new Response(
|
|
326
|
+
JSON.stringify({ error: error.message || "Upload failed" }),
|
|
327
|
+
{ status: 500, headers: { "Content-Type": "application/json" } },
|
|
328
|
+
);
|
|
329
|
+
} finally {
|
|
330
|
+
if (dbConfig.type === "sqlite" && db) {
|
|
331
|
+
db.close();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { APIRoute } from "astro";
|
|
2
|
+
import { dataStore } from "../../../lib/dataStore";
|
|
3
|
+
|
|
4
|
+
export const GET: APIRoute = async () => {
|
|
5
|
+
try {
|
|
6
|
+
const webhooks = await dataStore.find("_webhooks", { limit: 100, page: 1 });
|
|
7
|
+
|
|
8
|
+
const docs = webhooks.docs.map((w: any) => ({
|
|
9
|
+
id: w.id,
|
|
10
|
+
name: w.name,
|
|
11
|
+
url: w.url,
|
|
12
|
+
events: w.events || [],
|
|
13
|
+
secret: w.secret,
|
|
14
|
+
status: w.status || "active",
|
|
15
|
+
createdAt: w.createdAt,
|
|
16
|
+
lastTriggered: w.lastTriggered,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
return new Response(JSON.stringify(docs), {
|
|
20
|
+
status: 200,
|
|
21
|
+
headers: { "Content-Type": "application/json" },
|
|
22
|
+
});
|
|
23
|
+
} catch (error: any) {
|
|
24
|
+
return new Response(JSON.stringify({ error: error.message }), {
|
|
25
|
+
status: 500,
|
|
26
|
+
headers: { "Content-Type": "application/json" },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
32
|
+
try {
|
|
33
|
+
const body = await request.json();
|
|
34
|
+
const { name, url, events, secret } = body;
|
|
35
|
+
|
|
36
|
+
if (!name || !url) {
|
|
37
|
+
return new Response(
|
|
38
|
+
JSON.stringify({ error: "Name and URL are required" }),
|
|
39
|
+
{
|
|
40
|
+
status: 400,
|
|
41
|
+
headers: { "Content-Type": "application/json" },
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const doc = await dataStore.create("_webhooks", {
|
|
47
|
+
name,
|
|
48
|
+
url,
|
|
49
|
+
events: events || [],
|
|
50
|
+
secret: secret || "",
|
|
51
|
+
status: "active",
|
|
52
|
+
createdAt: new Date().toISOString(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return new Response(
|
|
56
|
+
JSON.stringify({
|
|
57
|
+
id: doc.id,
|
|
58
|
+
name: doc.name,
|
|
59
|
+
}),
|
|
60
|
+
{
|
|
61
|
+
status: 201,
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
} catch (error: any) {
|
|
66
|
+
return new Response(JSON.stringify({ error: error.message }), {
|
|
67
|
+
status: 500,
|
|
68
|
+
headers: { "Content-Type": "application/json" },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|