@sonicjs-cms/core 2.0.3 → 2.0.5
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/dist/{chunk-LEG4KNFP.cjs → chunk-3JMOWGUU.cjs} +20 -2
- package/dist/chunk-3JMOWGUU.cjs.map +1 -0
- package/dist/{chunk-LH4Z7QID.js → chunk-6FR25MPC.js} +111 -3
- package/dist/chunk-6FR25MPC.js.map +1 -0
- package/dist/{chunk-3NVJ6W27.cjs → chunk-DOR2IU73.cjs} +111 -2
- package/dist/chunk-DOR2IU73.cjs.map +1 -0
- package/dist/{chunk-M6FPVS7E.js → chunk-G5KY3WJV.js} +16 -29
- package/dist/chunk-G5KY3WJV.js.map +1 -0
- package/dist/{chunk-CDBVZEWR.js → chunk-HSRPDEQQ.js} +20 -2
- package/dist/chunk-HSRPDEQQ.js.map +1 -0
- package/dist/{chunk-4BJGEGX5.cjs → chunk-IM5SDXOE.cjs} +19 -32
- package/dist/chunk-IM5SDXOE.cjs.map +1 -0
- package/dist/{chunk-PPUKPNTP.js → chunk-LGC3TNCY.js} +293 -101
- package/dist/chunk-LGC3TNCY.js.map +1 -0
- package/dist/{chunk-5B3VMVEX.cjs → chunk-NPWWR6RI.cjs} +400 -208
- package/dist/chunk-NPWWR6RI.cjs.map +1 -0
- package/dist/{chunk-UL32L2KV.cjs → chunk-TRSHFTF6.cjs} +123 -3
- package/dist/chunk-TRSHFTF6.cjs.map +1 -0
- package/dist/{chunk-XJETEIRU.js → chunk-VSLEA22M.js} +123 -4
- package/dist/chunk-VSLEA22M.js.map +1 -0
- package/dist/index.cjs +876 -131
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +759 -13
- package/dist/index.js.map +1 -1
- package/dist/middleware.cjs +23 -23
- package/dist/middleware.js +2 -2
- package/dist/routes.cjs +25 -25
- package/dist/routes.js +5 -5
- package/dist/services.cjs +25 -21
- package/dist/services.js +2 -2
- package/dist/utils.cjs +11 -11
- package/dist/utils.js +1 -1
- package/migrations/006_plugin_system.sql +2 -2
- package/migrations/011_config_managed_collections.sql +1 -0
- package/migrations/018_settings_table.sql +23 -0
- package/package.json +1 -1
- package/dist/chunk-3NVJ6W27.cjs.map +0 -1
- package/dist/chunk-4BJGEGX5.cjs.map +0 -1
- package/dist/chunk-5B3VMVEX.cjs.map +0 -1
- package/dist/chunk-CDBVZEWR.js.map +0 -1
- package/dist/chunk-LEG4KNFP.cjs.map +0 -1
- package/dist/chunk-LH4Z7QID.js.map +0 -1
- package/dist/chunk-M6FPVS7E.js.map +0 -1
- package/dist/chunk-PPUKPNTP.js.map +0 -1
- package/dist/chunk-UL32L2KV.cjs.map +0 -1
- package/dist/chunk-XJETEIRU.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { getCacheService, CACHE_CONFIGS, getLogger } from './chunk-
|
|
2
|
-
import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-
|
|
3
|
-
import { PluginService, MigrationService } from './chunk-
|
|
1
|
+
import { getCacheService, CACHE_CONFIGS, getLogger, SettingsService } from './chunk-6FR25MPC.js';
|
|
2
|
+
import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-G5KY3WJV.js';
|
|
3
|
+
import { PluginService, MigrationService } from './chunk-HSRPDEQQ.js';
|
|
4
4
|
import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderFAQList, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-3LZ6TLPC.js';
|
|
5
|
-
import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml } from './chunk-
|
|
5
|
+
import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml } from './chunk-VSLEA22M.js';
|
|
6
6
|
import { metricsTracker } from './chunk-FICTAGD4.js';
|
|
7
7
|
import { Hono } from 'hono';
|
|
8
8
|
import { cors } from 'hono/cors';
|
|
@@ -78,7 +78,7 @@ apiContentCrudRoutes.post("/", requireAuth(), async (c) => {
|
|
|
78
78
|
title,
|
|
79
79
|
JSON.stringify(data || {}),
|
|
80
80
|
status || "draft",
|
|
81
|
-
user?.userId || "
|
|
81
|
+
user?.userId || "system",
|
|
82
82
|
now,
|
|
83
83
|
now
|
|
84
84
|
).run();
|
|
@@ -671,6 +671,7 @@ apiMediaRoutes.post("/upload", async (c) => {
|
|
|
671
671
|
size: mediaRecord.size,
|
|
672
672
|
width: mediaRecord.width,
|
|
673
673
|
height: mediaRecord.height,
|
|
674
|
+
r2_key: mediaRecord.r2_key,
|
|
674
675
|
publicUrl: mediaRecord.public_url,
|
|
675
676
|
thumbnailUrl: mediaRecord.thumbnail_url,
|
|
676
677
|
uploadedAt: new Date(mediaRecord.uploaded_at * 1e3).toISOString()
|
|
@@ -797,6 +798,7 @@ apiMediaRoutes.post("/upload-multiple", async (c) => {
|
|
|
797
798
|
size: mediaRecord.size,
|
|
798
799
|
width: mediaRecord.width,
|
|
799
800
|
height: mediaRecord.height,
|
|
801
|
+
r2_key: mediaRecord.r2_key,
|
|
800
802
|
publicUrl: mediaRecord.public_url,
|
|
801
803
|
thumbnailUrl: mediaRecord.thumbnail_url,
|
|
802
804
|
uploadedAt: new Date(mediaRecord.uploaded_at * 1e3).toISOString()
|
|
@@ -916,10 +918,17 @@ apiMediaRoutes.post("/create-folder", async (c) => {
|
|
|
916
918
|
}
|
|
917
919
|
const checkStmt = c.env.DB.prepare("SELECT COUNT(*) as count FROM media WHERE folder = ? AND deleted_at IS NULL");
|
|
918
920
|
const existingFolder = await checkStmt.bind(folderName).first();
|
|
921
|
+
if (existingFolder && existingFolder.count > 0) {
|
|
922
|
+
return c.json({
|
|
923
|
+
success: false,
|
|
924
|
+
error: `Folder "${folderName}" already exists`
|
|
925
|
+
}, 400);
|
|
926
|
+
}
|
|
919
927
|
return c.json({
|
|
920
928
|
success: true,
|
|
921
|
-
message: `Folder "${folderName}"
|
|
922
|
-
folder: folderName
|
|
929
|
+
message: `Folder "${folderName}" is ready. Upload files to this folder to make it appear in the media library.`,
|
|
930
|
+
folder: folderName,
|
|
931
|
+
note: "Folders appear automatically when you upload files to them"
|
|
923
932
|
});
|
|
924
933
|
} catch (error) {
|
|
925
934
|
console.error("Create folder error:", error);
|
|
@@ -1433,8 +1442,12 @@ adminApiRoutes.get("/activity", async (c) => {
|
|
|
1433
1442
|
});
|
|
1434
1443
|
var createCollectionSchema = z.object({
|
|
1435
1444
|
name: z.string().min(1).max(255).regex(/^[a-z0-9_]+$/, "Must contain only lowercase letters, numbers, and underscores"),
|
|
1436
|
-
|
|
1445
|
+
displayName: z.string().min(1).max(255).optional(),
|
|
1446
|
+
display_name: z.string().min(1).max(255).optional(),
|
|
1437
1447
|
description: z.string().optional()
|
|
1448
|
+
}).refine((data) => data.displayName || data.display_name, {
|
|
1449
|
+
message: "Either displayName or display_name is required",
|
|
1450
|
+
path: ["displayName"]
|
|
1438
1451
|
});
|
|
1439
1452
|
var updateCollectionSchema = z.object({
|
|
1440
1453
|
display_name: z.string().min(1).max(255).optional(),
|
|
@@ -1521,15 +1534,16 @@ adminApiRoutes.get("/collections/:id", async (c) => {
|
|
|
1521
1534
|
updated_at: Number(row.updated_at)
|
|
1522
1535
|
}));
|
|
1523
1536
|
return c.json({
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1537
|
+
id: collection.id,
|
|
1538
|
+
name: collection.name,
|
|
1539
|
+
display_name: collection.display_name,
|
|
1540
|
+
description: collection.description,
|
|
1541
|
+
is_active: collection.is_active === 1,
|
|
1542
|
+
managed: collection.managed === 1,
|
|
1543
|
+
schema: collection.schema ? JSON.parse(collection.schema) : null,
|
|
1544
|
+
created_at: Number(collection.created_at),
|
|
1545
|
+
updated_at: Number(collection.updated_at),
|
|
1546
|
+
fields
|
|
1533
1547
|
});
|
|
1534
1548
|
} catch (error) {
|
|
1535
1549
|
console.error("Error fetching collection:", error);
|
|
@@ -1538,7 +1552,16 @@ adminApiRoutes.get("/collections/:id", async (c) => {
|
|
|
1538
1552
|
});
|
|
1539
1553
|
adminApiRoutes.post("/collections", async (c) => {
|
|
1540
1554
|
try {
|
|
1541
|
-
const
|
|
1555
|
+
const contentType = c.req.header("Content-Type");
|
|
1556
|
+
if (!contentType || !contentType.includes("application/json")) {
|
|
1557
|
+
return c.json({ error: "Content-Type must be application/json" }, 400);
|
|
1558
|
+
}
|
|
1559
|
+
let body;
|
|
1560
|
+
try {
|
|
1561
|
+
body = await c.req.json();
|
|
1562
|
+
} catch (e) {
|
|
1563
|
+
return c.json({ error: "Invalid JSON in request body" }, 400);
|
|
1564
|
+
}
|
|
1542
1565
|
const validation = createCollectionSchema.safeParse(body);
|
|
1543
1566
|
if (!validation.success) {
|
|
1544
1567
|
return c.json({ error: "Validation failed", details: validation.error.errors }, 400);
|
|
@@ -1546,6 +1569,7 @@ adminApiRoutes.post("/collections", async (c) => {
|
|
|
1546
1569
|
const validatedData = validation.data;
|
|
1547
1570
|
const db = c.env.DB;
|
|
1548
1571
|
const user = c.get("user");
|
|
1572
|
+
const displayName = validatedData.displayName || validatedData.display_name || "";
|
|
1549
1573
|
const existingStmt = db.prepare("SELECT id FROM collections WHERE name = ?");
|
|
1550
1574
|
const existing = await existingStmt.bind(validatedData.name).first();
|
|
1551
1575
|
if (existing) {
|
|
@@ -1582,7 +1606,7 @@ adminApiRoutes.post("/collections", async (c) => {
|
|
|
1582
1606
|
await insertStmt.bind(
|
|
1583
1607
|
collectionId,
|
|
1584
1608
|
validatedData.name,
|
|
1585
|
-
|
|
1609
|
+
displayName,
|
|
1586
1610
|
validatedData.description || null,
|
|
1587
1611
|
JSON.stringify(basicSchema),
|
|
1588
1612
|
1,
|
|
@@ -1597,13 +1621,11 @@ adminApiRoutes.post("/collections", async (c) => {
|
|
|
1597
1621
|
console.error("Error clearing cache:", e);
|
|
1598
1622
|
}
|
|
1599
1623
|
return c.json({
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
created_at: now
|
|
1606
|
-
}
|
|
1624
|
+
id: collectionId,
|
|
1625
|
+
name: validatedData.name,
|
|
1626
|
+
displayName,
|
|
1627
|
+
description: validatedData.description,
|
|
1628
|
+
created_at: now
|
|
1607
1629
|
}, 201);
|
|
1608
1630
|
} catch (error) {
|
|
1609
1631
|
console.error("Error creating collection:", error);
|
|
@@ -1667,6 +1689,11 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
|
|
|
1667
1689
|
try {
|
|
1668
1690
|
const id = c.req.param("id");
|
|
1669
1691
|
const db = c.env.DB;
|
|
1692
|
+
const collectionStmt = db.prepare("SELECT name FROM collections WHERE id = ?");
|
|
1693
|
+
const collection = await collectionStmt.bind(id).first();
|
|
1694
|
+
if (!collection) {
|
|
1695
|
+
return c.json({ error: "Collection not found" }, 404);
|
|
1696
|
+
}
|
|
1670
1697
|
const contentStmt = db.prepare("SELECT COUNT(*) as count FROM content WHERE collection_id = ?");
|
|
1671
1698
|
const contentResult = await contentStmt.bind(id).first();
|
|
1672
1699
|
if (contentResult && contentResult.count > 0) {
|
|
@@ -1674,17 +1701,13 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
|
|
|
1674
1701
|
error: `Cannot delete collection: it contains ${contentResult.count} content item(s). Delete all content first.`
|
|
1675
1702
|
}, 400);
|
|
1676
1703
|
}
|
|
1677
|
-
const collectionStmt = db.prepare("SELECT name FROM collections WHERE id = ?");
|
|
1678
|
-
const collection = await collectionStmt.bind(id).first();
|
|
1679
1704
|
const deleteFieldsStmt = db.prepare("DELETE FROM content_fields WHERE collection_id = ?");
|
|
1680
1705
|
await deleteFieldsStmt.bind(id).run();
|
|
1681
1706
|
const deleteStmt = db.prepare("DELETE FROM collections WHERE id = ?");
|
|
1682
1707
|
await deleteStmt.bind(id).run();
|
|
1683
1708
|
try {
|
|
1684
1709
|
await c.env.CACHE_KV.delete("cache:collections:all");
|
|
1685
|
-
|
|
1686
|
-
await c.env.CACHE_KV.delete(`cache:collection:${collection.name}`);
|
|
1687
|
-
}
|
|
1710
|
+
await c.env.CACHE_KV.delete(`cache:collection:${collection.name}`);
|
|
1688
1711
|
} catch (e) {
|
|
1689
1712
|
console.error("Error clearing cache:", e);
|
|
1690
1713
|
}
|
|
@@ -2344,7 +2367,7 @@ authRoutes.post("/register/form", async (c) => {
|
|
|
2344
2367
|
Account created successfully! Redirecting to admin dashboard...
|
|
2345
2368
|
<script>
|
|
2346
2369
|
setTimeout(() => {
|
|
2347
|
-
window.location.href = '/admin/
|
|
2370
|
+
window.location.href = '/admin/dashboard';
|
|
2348
2371
|
}, 2000);
|
|
2349
2372
|
</script>
|
|
2350
2373
|
</div>
|
|
@@ -2412,7 +2435,7 @@ authRoutes.post("/login/form", async (c) => {
|
|
|
2412
2435
|
</div>
|
|
2413
2436
|
<script>
|
|
2414
2437
|
setTimeout(() => {
|
|
2415
|
-
window.location.href = '/admin/
|
|
2438
|
+
window.location.href = '/admin/dashboard';
|
|
2416
2439
|
}, 2000);
|
|
2417
2440
|
</script>
|
|
2418
2441
|
</div>
|
|
@@ -2705,7 +2728,7 @@ authRoutes.post("/accept-invitation", async (c) => {
|
|
|
2705
2728
|
maxAge: 60 * 60 * 24
|
|
2706
2729
|
// 24 hours
|
|
2707
2730
|
});
|
|
2708
|
-
return c.redirect("/admin/
|
|
2731
|
+
return c.redirect("/admin/dashboard?welcome=true");
|
|
2709
2732
|
} catch (error) {
|
|
2710
2733
|
console.error("Accept invitation error:", error);
|
|
2711
2734
|
return c.json({ error: "Failed to accept invitation" }, 500);
|
|
@@ -5183,11 +5206,11 @@ adminContentRoutes.post("/", async (c) => {
|
|
|
5183
5206
|
const now = Date.now();
|
|
5184
5207
|
const insertStmt = db.prepare(`
|
|
5185
5208
|
INSERT INTO content (
|
|
5186
|
-
id, collection_id, slug, title, data, status,
|
|
5209
|
+
id, collection_id, slug, title, data, status,
|
|
5187
5210
|
scheduled_publish_at, scheduled_unpublish_at,
|
|
5188
|
-
meta_title, meta_description, author_id, created_at, updated_at
|
|
5211
|
+
meta_title, meta_description, author_id, created_by, created_at, updated_at
|
|
5189
5212
|
)
|
|
5190
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5213
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5191
5214
|
`);
|
|
5192
5215
|
await insertStmt.bind(
|
|
5193
5216
|
contentId,
|
|
@@ -5201,6 +5224,7 @@ adminContentRoutes.post("/", async (c) => {
|
|
|
5201
5224
|
data.meta_title || null,
|
|
5202
5225
|
data.meta_description || null,
|
|
5203
5226
|
user?.userId || "unknown",
|
|
5227
|
+
user?.userId || "unknown",
|
|
5204
5228
|
now,
|
|
5205
5229
|
now
|
|
5206
5230
|
).run();
|
|
@@ -5502,10 +5526,10 @@ adminContentRoutes.post("/duplicate", async (c) => {
|
|
|
5502
5526
|
originalData.title = `${originalData.title || "Untitled"} (Copy)`;
|
|
5503
5527
|
const insertStmt = db.prepare(`
|
|
5504
5528
|
INSERT INTO content (
|
|
5505
|
-
id, collection_id, slug, title, data, status,
|
|
5506
|
-
author_id, created_at, updated_at
|
|
5529
|
+
id, collection_id, slug, title, data, status,
|
|
5530
|
+
author_id, created_by, created_at, updated_at
|
|
5507
5531
|
)
|
|
5508
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5532
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5509
5533
|
`);
|
|
5510
5534
|
await insertStmt.bind(
|
|
5511
5535
|
newId,
|
|
@@ -5516,6 +5540,7 @@ adminContentRoutes.post("/duplicate", async (c) => {
|
|
|
5516
5540
|
"draft",
|
|
5517
5541
|
// Always start as draft
|
|
5518
5542
|
user?.userId || "unknown",
|
|
5543
|
+
user?.userId || "unknown",
|
|
5519
5544
|
now,
|
|
5520
5545
|
now
|
|
5521
5546
|
).run();
|
|
@@ -5866,6 +5891,11 @@ var admin_content_default = adminContentRoutes;
|
|
|
5866
5891
|
|
|
5867
5892
|
// src/templates/pages/admin-profile.template.ts
|
|
5868
5893
|
init_admin_layout_catalyst_template();
|
|
5894
|
+
function renderAvatarImage(avatarUrl, firstName, lastName) {
|
|
5895
|
+
return `<div id="avatar-image-container" class="w-24 h-24 rounded-full mx-auto mb-4 overflow-hidden bg-gradient-to-br from-cyan-400 to-purple-400 flex items-center justify-center ring-4 ring-zinc-950/5 dark:ring-white/10">
|
|
5896
|
+
${avatarUrl ? `<img src="${avatarUrl}" alt="Profile picture" class="w-full h-full object-cover">` : `<span class="text-2xl font-bold text-white">${firstName.charAt(0)}${lastName.charAt(0)}</span>`}
|
|
5897
|
+
</div>`;
|
|
5898
|
+
}
|
|
5869
5899
|
function renderProfilePage(data) {
|
|
5870
5900
|
const pageContent = `
|
|
5871
5901
|
<div class="space-y-8">
|
|
@@ -6064,9 +6094,7 @@ function renderProfilePage(data) {
|
|
|
6064
6094
|
<h3 class="text-base font-semibold text-zinc-950 dark:text-white mb-4">Profile Picture</h3>
|
|
6065
6095
|
|
|
6066
6096
|
<div class="text-center">
|
|
6067
|
-
|
|
6068
|
-
${data.profile.avatar_url ? `<img src="${data.profile.avatar_url}" alt="Profile picture" class="w-full h-full object-cover">` : `<span class="text-2xl font-bold text-white">${data.profile.first_name.charAt(0)}${data.profile.last_name.charAt(0)}</span>`}
|
|
6069
|
-
</div>
|
|
6097
|
+
${renderAvatarImage(data.profile.avatar_url, data.profile.first_name, data.profile.last_name)}
|
|
6070
6098
|
|
|
6071
6099
|
<form id="avatar-form" hx-post="/admin/profile/avatar" hx-target="#avatar-messages" hx-encoding="multipart/form-data">
|
|
6072
6100
|
<input
|
|
@@ -7902,6 +7930,10 @@ userRoutes.post("/profile/avatar", async (c) => {
|
|
|
7902
7930
|
WHERE id = ?
|
|
7903
7931
|
`);
|
|
7904
7932
|
await updateStmt.bind(avatarUrl, Date.now(), user.userId).run();
|
|
7933
|
+
const userStmt = db.prepare(`
|
|
7934
|
+
SELECT first_name, last_name FROM users WHERE id = ?
|
|
7935
|
+
`);
|
|
7936
|
+
const userData = await userStmt.bind(user.userId).first();
|
|
7905
7937
|
await logActivity(
|
|
7906
7938
|
db,
|
|
7907
7939
|
user.userId,
|
|
@@ -7912,11 +7944,18 @@ userRoutes.post("/profile/avatar", async (c) => {
|
|
|
7912
7944
|
c.req.header("x-forwarded-for") || c.req.header("cf-connecting-ip"),
|
|
7913
7945
|
c.req.header("user-agent")
|
|
7914
7946
|
);
|
|
7915
|
-
|
|
7947
|
+
const alertHtml = renderAlert2({
|
|
7916
7948
|
type: "success",
|
|
7917
7949
|
message: "Profile picture updated successfully!",
|
|
7918
7950
|
dismissible: true
|
|
7919
|
-
})
|
|
7951
|
+
});
|
|
7952
|
+
const avatarUrlWithCache = `${avatarUrl}?t=${Date.now()}`;
|
|
7953
|
+
const avatarImageHtml = renderAvatarImage(avatarUrlWithCache, userData.first_name, userData.last_name);
|
|
7954
|
+
const avatarImageWithOob = avatarImageHtml.replace(
|
|
7955
|
+
'id="avatar-image-container"',
|
|
7956
|
+
'id="avatar-image-container" hx-swap-oob="true"'
|
|
7957
|
+
);
|
|
7958
|
+
return c.html(alertHtml + avatarImageWithOob);
|
|
7920
7959
|
} catch (error) {
|
|
7921
7960
|
console.error("Avatar upload error:", error);
|
|
7922
7961
|
return c.html(renderAlert2({
|
|
@@ -9148,8 +9187,10 @@ function renderMediaLibraryPage(data) {
|
|
|
9148
9187
|
</button>
|
|
9149
9188
|
<button
|
|
9150
9189
|
class="w-full text-left px-3 py-2 text-sm text-zinc-700 dark:text-zinc-300 hover:text-zinc-950 dark:hover:text-white hover:bg-zinc-50 dark:hover:bg-zinc-800/50 rounded-lg transition-colors"
|
|
9151
|
-
hx-delete="/media/cleanup"
|
|
9190
|
+
hx-delete="/admin/media/cleanup"
|
|
9152
9191
|
hx-confirm="Delete unused files?"
|
|
9192
|
+
hx-target="body"
|
|
9193
|
+
hx-swap="beforeend"
|
|
9153
9194
|
>
|
|
9154
9195
|
Cleanup Unused
|
|
9155
9196
|
</button>
|
|
@@ -9329,11 +9370,12 @@ function renderMediaLibraryPage(data) {
|
|
|
9329
9370
|
</div>
|
|
9330
9371
|
|
|
9331
9372
|
<!-- Upload Form -->
|
|
9332
|
-
<form
|
|
9373
|
+
<form
|
|
9333
9374
|
id="upload-form"
|
|
9334
9375
|
hx-post="/admin/media/upload"
|
|
9335
9376
|
hx-encoding="multipart/form-data"
|
|
9336
9377
|
hx-target="#upload-results"
|
|
9378
|
+
hx-on::after-request="if(event.detail.successful) { setTimeout(() => { window.location.href = '/admin/media?t=' + Date.now(); }, 1500); }"
|
|
9337
9379
|
class="space-y-4"
|
|
9338
9380
|
>
|
|
9339
9381
|
<!-- Drag and Drop Zone -->
|
|
@@ -10216,21 +10258,23 @@ adminMediaRoutes.get("/", async (c) => {
|
|
|
10216
10258
|
const { results } = await stmt.bind(...params).all();
|
|
10217
10259
|
const foldersStmt = db.prepare(`
|
|
10218
10260
|
SELECT folder, COUNT(*) as count, SUM(size) as totalSize
|
|
10219
|
-
FROM media
|
|
10220
|
-
|
|
10261
|
+
FROM media
|
|
10262
|
+
WHERE deleted_at IS NULL
|
|
10263
|
+
GROUP BY folder
|
|
10221
10264
|
ORDER BY folder
|
|
10222
10265
|
`);
|
|
10223
10266
|
const { results: folders } = await foldersStmt.all();
|
|
10224
10267
|
const typesStmt = db.prepare(`
|
|
10225
|
-
SELECT
|
|
10226
|
-
CASE
|
|
10268
|
+
SELECT
|
|
10269
|
+
CASE
|
|
10227
10270
|
WHEN mime_type LIKE 'image/%' THEN 'images'
|
|
10228
10271
|
WHEN mime_type LIKE 'video/%' THEN 'videos'
|
|
10229
10272
|
WHEN mime_type IN ('application/pdf', 'text/plain') THEN 'documents'
|
|
10230
10273
|
ELSE 'other'
|
|
10231
10274
|
END as type,
|
|
10232
10275
|
COUNT(*) as count
|
|
10233
|
-
FROM media
|
|
10276
|
+
FROM media
|
|
10277
|
+
WHERE deleted_at IS NULL
|
|
10234
10278
|
GROUP BY type
|
|
10235
10279
|
`);
|
|
10236
10280
|
const { results: types } = await typesStmt.all();
|
|
@@ -10506,6 +10550,18 @@ adminMediaRoutes.post("/upload", async (c) => {
|
|
|
10506
10550
|
}
|
|
10507
10551
|
const uploadResults = [];
|
|
10508
10552
|
const errors = [];
|
|
10553
|
+
console.log("[MEDIA UPLOAD] c.env keys:", Object.keys(c.env));
|
|
10554
|
+
console.log("[MEDIA UPLOAD] MEDIA_BUCKET defined?", !!c.env.MEDIA_BUCKET);
|
|
10555
|
+
console.log("[MEDIA UPLOAD] MEDIA_BUCKET type:", typeof c.env.MEDIA_BUCKET);
|
|
10556
|
+
if (!c.env.MEDIA_BUCKET) {
|
|
10557
|
+
console.error("[MEDIA UPLOAD] MEDIA_BUCKET is not available! Available env keys:", Object.keys(c.env));
|
|
10558
|
+
return c.html(html`
|
|
10559
|
+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
10560
|
+
Media storage (R2) is not configured. Please check your wrangler.toml configuration.
|
|
10561
|
+
<br><small>Debug: Available bindings: ${Object.keys(c.env).join(", ")}</small>
|
|
10562
|
+
</div>
|
|
10563
|
+
`);
|
|
10564
|
+
}
|
|
10509
10565
|
for (const file of files) {
|
|
10510
10566
|
try {
|
|
10511
10567
|
const validation = fileValidationSchema2.safeParse({
|
|
@@ -10737,6 +10793,87 @@ adminMediaRoutes.put("/:id", async (c) => {
|
|
|
10737
10793
|
`);
|
|
10738
10794
|
}
|
|
10739
10795
|
});
|
|
10796
|
+
adminMediaRoutes.delete("/cleanup", requireRole("admin"), async (c) => {
|
|
10797
|
+
try {
|
|
10798
|
+
const db = c.env.DB;
|
|
10799
|
+
const allMediaStmt = db.prepare("SELECT id, r2_key, filename FROM media WHERE deleted_at IS NULL");
|
|
10800
|
+
const { results: allMedia } = await allMediaStmt.all();
|
|
10801
|
+
const contentStmt = db.prepare("SELECT data FROM content");
|
|
10802
|
+
const { results: contentRecords } = await contentStmt.all();
|
|
10803
|
+
const referencedUrls = /* @__PURE__ */ new Set();
|
|
10804
|
+
for (const record of contentRecords) {
|
|
10805
|
+
if (record.data) {
|
|
10806
|
+
const dataStr = typeof record.data === "string" ? record.data : JSON.stringify(record.data);
|
|
10807
|
+
const urlMatches = dataStr.matchAll(/\/files\/([^\s"',]+)/g);
|
|
10808
|
+
for (const match of urlMatches) {
|
|
10809
|
+
referencedUrls.add(match[1]);
|
|
10810
|
+
}
|
|
10811
|
+
}
|
|
10812
|
+
}
|
|
10813
|
+
const unusedFiles = allMedia.filter((file) => !referencedUrls.has(file.r2_key));
|
|
10814
|
+
if (unusedFiles.length === 0) {
|
|
10815
|
+
return c.html(html`
|
|
10816
|
+
<div class="bg-blue-100 border border-blue-400 text-blue-700 px-4 py-3 rounded">
|
|
10817
|
+
No unused media files found. All files are referenced in content.
|
|
10818
|
+
</div>
|
|
10819
|
+
<script>
|
|
10820
|
+
setTimeout(() => {
|
|
10821
|
+
window.location.href = '/admin/media?t=' + Date.now();
|
|
10822
|
+
}, 2000);
|
|
10823
|
+
</script>
|
|
10824
|
+
`);
|
|
10825
|
+
}
|
|
10826
|
+
let deletedCount = 0;
|
|
10827
|
+
const errors = [];
|
|
10828
|
+
for (const file of unusedFiles) {
|
|
10829
|
+
try {
|
|
10830
|
+
await c.env.MEDIA_BUCKET.delete(file.r2_key);
|
|
10831
|
+
const deleteStmt = db.prepare("UPDATE media SET deleted_at = ? WHERE id = ?");
|
|
10832
|
+
await deleteStmt.bind(Math.floor(Date.now() / 1e3), file.id).run();
|
|
10833
|
+
deletedCount++;
|
|
10834
|
+
} catch (error) {
|
|
10835
|
+
console.error(`Failed to delete ${file.filename}:`, error);
|
|
10836
|
+
errors.push({
|
|
10837
|
+
filename: file.filename,
|
|
10838
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
10839
|
+
});
|
|
10840
|
+
}
|
|
10841
|
+
}
|
|
10842
|
+
return c.html(html`
|
|
10843
|
+
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
|
|
10844
|
+
Successfully cleaned up ${deletedCount} unused media file${deletedCount !== 1 ? "s" : ""}.
|
|
10845
|
+
${errors.length > 0 ? html`
|
|
10846
|
+
<br><span class="text-sm">Failed to delete ${errors.length} file${errors.length !== 1 ? "s" : ""}.</span>
|
|
10847
|
+
` : ""}
|
|
10848
|
+
</div>
|
|
10849
|
+
|
|
10850
|
+
${errors.length > 0 ? html`
|
|
10851
|
+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
10852
|
+
<p class="font-medium">Cleanup errors:</p>
|
|
10853
|
+
<ul class="list-disc list-inside mt-2 text-sm">
|
|
10854
|
+
${errors.map((error) => html`
|
|
10855
|
+
<li>${error.filename}: ${error.error}</li>
|
|
10856
|
+
`)}
|
|
10857
|
+
</ul>
|
|
10858
|
+
</div>
|
|
10859
|
+
` : ""}
|
|
10860
|
+
|
|
10861
|
+
<script>
|
|
10862
|
+
// Refresh media library after cleanup
|
|
10863
|
+
setTimeout(() => {
|
|
10864
|
+
window.location.href = '/admin/media?t=' + Date.now();
|
|
10865
|
+
}, 2500);
|
|
10866
|
+
</script>
|
|
10867
|
+
`);
|
|
10868
|
+
} catch (error) {
|
|
10869
|
+
console.error("Cleanup error:", error);
|
|
10870
|
+
return c.html(html`
|
|
10871
|
+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
10872
|
+
Cleanup failed: ${error instanceof Error ? error.message : "Unknown error"}
|
|
10873
|
+
</div>
|
|
10874
|
+
`);
|
|
10875
|
+
}
|
|
10876
|
+
});
|
|
10740
10877
|
adminMediaRoutes.delete("/:id", async (c) => {
|
|
10741
10878
|
try {
|
|
10742
10879
|
const user = c.get("user");
|
|
@@ -16276,7 +16413,7 @@ router.get("/stats", async (c) => {
|
|
|
16276
16413
|
}
|
|
16277
16414
|
let contentCount = 0;
|
|
16278
16415
|
try {
|
|
16279
|
-
const contentStmt = db.prepare("SELECT COUNT(*) as count FROM content
|
|
16416
|
+
const contentStmt = db.prepare("SELECT COUNT(*) as count FROM content");
|
|
16280
16417
|
const contentResult = await contentStmt.first();
|
|
16281
16418
|
contentCount = contentResult?.count || 0;
|
|
16282
16419
|
} catch (error) {
|
|
@@ -16300,14 +16437,14 @@ router.get("/stats", async (c) => {
|
|
|
16300
16437
|
} catch (error) {
|
|
16301
16438
|
console.error("Error fetching users count:", error);
|
|
16302
16439
|
}
|
|
16303
|
-
const
|
|
16440
|
+
const html8 = renderStatsCards({
|
|
16304
16441
|
collections: collectionsCount,
|
|
16305
16442
|
contentItems: contentCount,
|
|
16306
16443
|
mediaFiles: mediaCount,
|
|
16307
16444
|
users: usersCount,
|
|
16308
16445
|
mediaSize
|
|
16309
16446
|
});
|
|
16310
|
-
return c.html(
|
|
16447
|
+
return c.html(html8);
|
|
16311
16448
|
} catch (error) {
|
|
16312
16449
|
console.error("Error fetching stats:", error);
|
|
16313
16450
|
return c.html('<div class="text-red-500">Failed to load statistics</div>');
|
|
@@ -16331,8 +16468,8 @@ router.get("/storage", async (c) => {
|
|
|
16331
16468
|
} catch (error) {
|
|
16332
16469
|
console.error("Error fetching media size:", error);
|
|
16333
16470
|
}
|
|
16334
|
-
const
|
|
16335
|
-
return c.html(
|
|
16471
|
+
const html8 = renderStorageUsage(databaseSize, mediaSize);
|
|
16472
|
+
return c.html(html8);
|
|
16336
16473
|
} catch (error) {
|
|
16337
16474
|
console.error("Error fetching storage usage:", error);
|
|
16338
16475
|
return c.html('<div class="text-red-500">Failed to load storage information</div>');
|
|
@@ -16381,12 +16518,12 @@ router.get("/recent-activity", async (c) => {
|
|
|
16381
16518
|
user: userName
|
|
16382
16519
|
};
|
|
16383
16520
|
});
|
|
16384
|
-
const
|
|
16385
|
-
return c.html(
|
|
16521
|
+
const html8 = renderRecentActivity(activities);
|
|
16522
|
+
return c.html(html8);
|
|
16386
16523
|
} catch (error) {
|
|
16387
16524
|
console.error("Error fetching recent activity:", error);
|
|
16388
|
-
const
|
|
16389
|
-
return c.html(
|
|
16525
|
+
const html8 = renderRecentActivity([]);
|
|
16526
|
+
return c.html(html8);
|
|
16390
16527
|
}
|
|
16391
16528
|
});
|
|
16392
16529
|
router.get("/api/metrics", async (c) => {
|
|
@@ -16399,7 +16536,7 @@ router.get("/api/metrics", async (c) => {
|
|
|
16399
16536
|
});
|
|
16400
16537
|
router.get("/system-status", async (c) => {
|
|
16401
16538
|
try {
|
|
16402
|
-
const
|
|
16539
|
+
const html8 = `
|
|
16403
16540
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
16404
16541
|
<div class="relative group">
|
|
16405
16542
|
<div class="absolute inset-0 bg-gradient-to-br from-blue-500/20 to-cyan-500/20 dark:from-blue-500/10 dark:to-cyan-500/10 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
@@ -16454,7 +16591,7 @@ router.get("/system-status", async (c) => {
|
|
|
16454
16591
|
</div>
|
|
16455
16592
|
</div>
|
|
16456
16593
|
`;
|
|
16457
|
-
return c.html(
|
|
16594
|
+
return c.html(html8);
|
|
16458
16595
|
} catch (error) {
|
|
16459
16596
|
console.error("Error fetching system status:", error);
|
|
16460
16597
|
return c.html('<div class="text-red-500">Failed to load system status</div>');
|
|
@@ -17872,11 +18009,13 @@ adminCollectionsRoutes.post("/", async (c) => {
|
|
|
17872
18009
|
now,
|
|
17873
18010
|
now
|
|
17874
18011
|
).run();
|
|
17875
|
-
|
|
17876
|
-
|
|
17877
|
-
|
|
17878
|
-
|
|
17879
|
-
|
|
18012
|
+
if (c.env.CACHE_KV) {
|
|
18013
|
+
try {
|
|
18014
|
+
await c.env.CACHE_KV.delete("cache:collections:all");
|
|
18015
|
+
await c.env.CACHE_KV.delete(`cache:collection:${name}`);
|
|
18016
|
+
} catch (e) {
|
|
18017
|
+
console.error("Error clearing cache:", e);
|
|
18018
|
+
}
|
|
17880
18019
|
}
|
|
17881
18020
|
if (isHtmx) {
|
|
17882
18021
|
return c.html(html`
|
|
@@ -18208,31 +18347,52 @@ function renderSettingsPage(data) {
|
|
|
18208
18347
|
// Initialize tab-specific features on page load
|
|
18209
18348
|
const currentTab = '${activeTab}';
|
|
18210
18349
|
|
|
18211
|
-
function saveAllSettings() {
|
|
18350
|
+
async function saveAllSettings() {
|
|
18212
18351
|
// Collect all form data
|
|
18213
18352
|
const formData = new FormData();
|
|
18214
|
-
|
|
18215
|
-
// Get all form inputs
|
|
18216
|
-
document.querySelectorAll('input, select, textarea').forEach(input => {
|
|
18353
|
+
|
|
18354
|
+
// Get all form inputs in the settings content area
|
|
18355
|
+
document.querySelectorAll('#settings-content input, #settings-content select, #settings-content textarea').forEach(input => {
|
|
18217
18356
|
if (input.type === 'checkbox') {
|
|
18218
|
-
formData.append(input.name, input.checked);
|
|
18357
|
+
formData.append(input.name, input.checked ? 'true' : 'false');
|
|
18219
18358
|
} else if (input.name) {
|
|
18220
18359
|
formData.append(input.name, input.value);
|
|
18221
18360
|
}
|
|
18222
18361
|
});
|
|
18223
|
-
|
|
18362
|
+
|
|
18224
18363
|
// Show loading state
|
|
18225
18364
|
const saveBtn = document.querySelector('button[onclick="saveAllSettings()"]');
|
|
18226
18365
|
const originalText = saveBtn.innerHTML;
|
|
18227
|
-
saveBtn.innerHTML = 'Saving...';
|
|
18366
|
+
saveBtn.innerHTML = '<svg class="animate-spin -ml-0.5 mr-1.5 h-5 w-5 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>Saving...';
|
|
18228
18367
|
saveBtn.disabled = true;
|
|
18229
|
-
|
|
18230
|
-
|
|
18231
|
-
|
|
18368
|
+
|
|
18369
|
+
try {
|
|
18370
|
+
// Determine which endpoint to call based on current tab
|
|
18371
|
+
let endpoint = '/admin/settings/general';
|
|
18372
|
+
if (currentTab === 'general') {
|
|
18373
|
+
endpoint = '/admin/settings/general';
|
|
18374
|
+
}
|
|
18375
|
+
// Add more endpoints for other tabs when implemented
|
|
18376
|
+
|
|
18377
|
+
const response = await fetch(endpoint, {
|
|
18378
|
+
method: 'POST',
|
|
18379
|
+
body: formData
|
|
18380
|
+
});
|
|
18381
|
+
|
|
18382
|
+
const result = await response.json();
|
|
18383
|
+
|
|
18384
|
+
if (result.success) {
|
|
18385
|
+
showNotification(result.message || 'Settings saved successfully!', 'success');
|
|
18386
|
+
} else {
|
|
18387
|
+
showNotification(result.error || 'Failed to save settings', 'error');
|
|
18388
|
+
}
|
|
18389
|
+
} catch (error) {
|
|
18390
|
+
console.error('Error saving settings:', error);
|
|
18391
|
+
showNotification('Failed to save settings. Please try again.', 'error');
|
|
18392
|
+
} finally {
|
|
18232
18393
|
saveBtn.innerHTML = originalText;
|
|
18233
18394
|
saveBtn.disabled = false;
|
|
18234
|
-
|
|
18235
|
-
}, 1000);
|
|
18395
|
+
}
|
|
18236
18396
|
}
|
|
18237
18397
|
|
|
18238
18398
|
function resetSettings() {
|
|
@@ -19637,15 +19797,20 @@ function getMockSettings(user) {
|
|
|
19637
19797
|
adminSettingsRoutes.get("/", (c) => {
|
|
19638
19798
|
return c.redirect("/admin/settings/general");
|
|
19639
19799
|
});
|
|
19640
|
-
adminSettingsRoutes.get("/general", (c) => {
|
|
19800
|
+
adminSettingsRoutes.get("/general", async (c) => {
|
|
19641
19801
|
const user = c.get("user");
|
|
19802
|
+
const db = c.env.DB;
|
|
19803
|
+
const settingsService = new SettingsService(db);
|
|
19804
|
+
const generalSettings = await settingsService.getGeneralSettings(user?.email);
|
|
19805
|
+
const mockSettings = getMockSettings(user);
|
|
19806
|
+
mockSettings.general = generalSettings;
|
|
19642
19807
|
const pageData = {
|
|
19643
19808
|
user: user ? {
|
|
19644
19809
|
name: user.email,
|
|
19645
19810
|
email: user.email,
|
|
19646
19811
|
role: user.role
|
|
19647
19812
|
} : void 0,
|
|
19648
|
-
settings:
|
|
19813
|
+
settings: mockSettings,
|
|
19649
19814
|
activeTab: "general",
|
|
19650
19815
|
version: c.get("appVersion")
|
|
19651
19816
|
};
|
|
@@ -19926,28 +20091,55 @@ adminSettingsRoutes.post("/api/database-tools/truncate", async (c) => {
|
|
|
19926
20091
|
}, 500);
|
|
19927
20092
|
}
|
|
19928
20093
|
});
|
|
19929
|
-
adminSettingsRoutes.post("/", async (c) => {
|
|
20094
|
+
adminSettingsRoutes.post("/general", async (c) => {
|
|
19930
20095
|
try {
|
|
20096
|
+
const user = c.get("user");
|
|
20097
|
+
if (!user || user.role !== "admin") {
|
|
20098
|
+
return c.json({
|
|
20099
|
+
success: false,
|
|
20100
|
+
error: "Unauthorized. Admin access required."
|
|
20101
|
+
}, 403);
|
|
20102
|
+
}
|
|
19931
20103
|
const formData = await c.req.formData();
|
|
19932
|
-
|
|
19933
|
-
|
|
19934
|
-
|
|
19935
|
-
|
|
19936
|
-
|
|
19937
|
-
|
|
19938
|
-
|
|
19939
|
-
|
|
19940
|
-
|
|
19941
|
-
|
|
20104
|
+
const db = c.env.DB;
|
|
20105
|
+
const settingsService = new SettingsService(db);
|
|
20106
|
+
const settings = {
|
|
20107
|
+
siteName: formData.get("siteName"),
|
|
20108
|
+
siteDescription: formData.get("siteDescription"),
|
|
20109
|
+
adminEmail: formData.get("adminEmail"),
|
|
20110
|
+
timezone: formData.get("timezone"),
|
|
20111
|
+
language: formData.get("language"),
|
|
20112
|
+
maintenanceMode: formData.get("maintenanceMode") === "true"
|
|
20113
|
+
};
|
|
20114
|
+
if (!settings.siteName || !settings.siteDescription) {
|
|
20115
|
+
return c.json({
|
|
20116
|
+
success: false,
|
|
20117
|
+
error: "Site name and description are required"
|
|
20118
|
+
}, 400);
|
|
20119
|
+
}
|
|
20120
|
+
const success = await settingsService.saveGeneralSettings(settings);
|
|
20121
|
+
if (success) {
|
|
20122
|
+
return c.json({
|
|
20123
|
+
success: true,
|
|
20124
|
+
message: "General settings saved successfully!"
|
|
20125
|
+
});
|
|
20126
|
+
} else {
|
|
20127
|
+
return c.json({
|
|
20128
|
+
success: false,
|
|
20129
|
+
error: "Failed to save settings"
|
|
20130
|
+
}, 500);
|
|
20131
|
+
}
|
|
19942
20132
|
} catch (error) {
|
|
19943
|
-
console.error("Error saving settings:", error);
|
|
19944
|
-
return c.
|
|
19945
|
-
|
|
19946
|
-
|
|
19947
|
-
|
|
19948
|
-
`);
|
|
20133
|
+
console.error("Error saving general settings:", error);
|
|
20134
|
+
return c.json({
|
|
20135
|
+
success: false,
|
|
20136
|
+
error: "Failed to save settings. Please try again."
|
|
20137
|
+
}, 500);
|
|
19949
20138
|
}
|
|
19950
20139
|
});
|
|
20140
|
+
adminSettingsRoutes.post("/", async (c) => {
|
|
20141
|
+
return c.redirect("/admin/settings/general");
|
|
20142
|
+
});
|
|
19951
20143
|
|
|
19952
20144
|
// src/routes/index.ts
|
|
19953
20145
|
var ROUTES_INFO = {
|
|
@@ -19978,5 +20170,5 @@ var ROUTES_INFO = {
|
|
|
19978
20170
|
};
|
|
19979
20171
|
|
|
19980
20172
|
export { ROUTES_INFO, adminCheckboxRoutes, adminCollectionsRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_code_examples_default, admin_content_default, admin_faq_default, admin_testimonials_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, router, userRoutes };
|
|
19981
|
-
//# sourceMappingURL=chunk-
|
|
19982
|
-
//# sourceMappingURL=chunk-
|
|
20173
|
+
//# sourceMappingURL=chunk-LGC3TNCY.js.map
|
|
20174
|
+
//# sourceMappingURL=chunk-LGC3TNCY.js.map
|