@rimelight/cms 0.0.3 → 0.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/bylines-Csggkl-g.d.mts +405 -0
- package/dist/index.d.mts +724 -0
- package/dist/index.mjs +2382 -0
- package/dist/integration.d.mts +33 -0
- package/dist/integration.mjs +161 -0
- package/dist/schema/index.d.mts +2063 -0
- package/dist/schema/index.mjs +176 -0
- package/dist/schema/sqlite.d.mts +2132 -0
- package/dist/schema/sqlite.mjs +137 -0
- package/dist/storage/index.d.mts +59 -0
- package/dist/storage/index.mjs +133 -0
- package/package.json +35 -14
- package/src/admin/api/media-file.ts +1 -1
- package/src/admin/api/media.ts +28 -3
- package/src/admin/layouts/CMSDashboardLayout.astro +16 -3
- package/src/admin/pages/assets.astro +11 -11
- package/src/admin/pages/pages-edit.astro +3 -3
- package/src/admin/pages/pages-index.astro +2 -2
- package/src/admin/pages/pages-preview.astro +3 -3
- package/src/admin/pages/pages-review.astro +2 -2
- package/src/admin/pages/templates-edit.astro +1 -1
- package/src/admin/pages/templates-index.astro +1 -1
- package/src/astro/BlockRenderer.astro +1 -1
- package/src/astro/PageRenderer.astro +4 -4
- package/src/astro/blocks/CalloutBlock.astro +1 -1
- package/src/astro/blocks/OrderedListBlock.astro +2 -2
- package/src/astro/blocks/ParagraphBlock.astro +1 -1
- package/src/astro/blocks/TabsBlock.astro +5 -5
- package/src/astro/blocks/UnorderedListBlock.astro +2 -2
- package/src/auth/editor-guards.ts +1 -1
- package/src/cache/purge.ts +2 -2
- package/src/client/components/RimelightBlock.tsx +1 -1
- package/src/client/components/RimelightBlockPicker.tsx +1 -1
- package/src/client/components/RimelightPreview.tsx +27 -27
- package/src/client/components/RimelightPropertiesPanel.tsx +4 -4
- package/src/client/components/RimelightTemplateEditor.tsx +9 -5
- package/src/client/components/editors/CalloutEditor.tsx +10 -10
- package/src/client/components/editors/CardEditor.tsx +5 -5
- package/src/client/components/editors/CardsEditor.tsx +2 -2
- package/src/client/components/editors/CodeEditor.tsx +5 -5
- package/src/client/components/editors/DialogueEditor.tsx +6 -6
- package/src/client/components/editors/FileTreeEditor.tsx +1 -1
- package/src/client/components/editors/ImageEditor.tsx +9 -9
- package/src/client/components/editors/ParagraphEditor.tsx +1 -1
- package/src/client/components/editors/SceneEditor.tsx +9 -9
- package/src/client/components/editors/ScriptEditor.tsx +8 -8
- package/src/client/components/editors/SectionEditor.tsx +3 -3
- package/src/client/components/editors/StepItemEditor.tsx +6 -5
- package/src/client/components/editors/StepsEditor.tsx +1 -1
- package/src/client/components/editors/TabItemEditor.tsx +3 -3
- package/src/client/components/editors/TableEditor.tsx +4 -4
- package/src/client/components/editors/TabsEditor.tsx +1 -1
- package/src/client/state/editor-store.ts +23 -23
- package/src/client/utils/sortable.ts +13 -12
- package/src/client/utils/tree-sanitizer.ts +1 -1
- package/src/core/excerpt.ts +16 -16
- package/src/core/page-definitions.ts +2 -2
- package/src/docs/agent.ts +1 -1
- package/src/docs/routing.ts +2 -2
- package/src/env.d.ts +6 -0
- package/src/index.ts +17 -4
- package/src/integration.ts +54 -0
- package/src/loader/live.ts +1 -1
- package/src/markdown/serializer.ts +26 -26
- package/src/schema/sqlite.ts +344 -0
- package/src/storage/index.ts +4 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
3
|
+
//#region src/schema/sqlite.ts
|
|
4
|
+
const pageTemplates = sqliteTable("page_templates", {
|
|
5
|
+
id: text("id").$defaultFn(() => crypto.randomUUID()).notNull().primaryKey(),
|
|
6
|
+
slug: text("slug").notNull().unique(),
|
|
7
|
+
title: text("title", { mode: "json" }).$type().notNull(),
|
|
8
|
+
description: text("description", { mode: "json" }).$type(),
|
|
9
|
+
pageType: text("page_type").$type().notNull(),
|
|
10
|
+
version: integer("version").default(1).notNull(),
|
|
11
|
+
allowedRoles: text("allowed_roles", { mode: "json" }).$type().default(sql`'[]'`).notNull(),
|
|
12
|
+
rolePermissions: text("role_permissions", { mode: "json" }).$type().default(sql`'{"whoCanCreate":[],"whoCanEdit":[],"whoCanReview":[],"whoCanView":[]}'`).notNull(),
|
|
13
|
+
requiredPermission: text("required_permission"),
|
|
14
|
+
approvalRules: text("approval_rules", { mode: "json" }).$type().default(sql`'{"allowSelfApproval":true,"minApprovals":1}'`).notNull(),
|
|
15
|
+
defaultProperties: text("default_properties", { mode: "json" }).default(sql`'{}'`).notNull(),
|
|
16
|
+
initialBlocks: text("initial_blocks", { mode: "json" }).$type().default(sql`'[]'`).notNull(),
|
|
17
|
+
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull(),
|
|
18
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).$onUpdate(() => /* @__PURE__ */ new Date())
|
|
19
|
+
});
|
|
20
|
+
const pageVersions = sqliteTable("page_versions", {
|
|
21
|
+
id: text("id").$defaultFn(() => crypto.randomUUID()).notNull().primaryKey(),
|
|
22
|
+
pageId: text("page_id").notNull(),
|
|
23
|
+
versionNumber: integer("version_number").notNull(),
|
|
24
|
+
status: text("status").$type().default("pending").notNull(),
|
|
25
|
+
slug: text("slug").notNull(),
|
|
26
|
+
type: text("type").$type().notNull(),
|
|
27
|
+
title: text("title", { mode: "json" }).$type().notNull(),
|
|
28
|
+
description: text("description", { mode: "json" }).$type(),
|
|
29
|
+
tags: text("tags", { mode: "json" }).$type().default(sql`'[]'`).notNull(),
|
|
30
|
+
authorIds: text("author_ids", { mode: "json" }).$type().default(sql`'[]'`),
|
|
31
|
+
bylines: text("bylines", { mode: "json" }).$type().default(sql`'[]'`),
|
|
32
|
+
content: text("content", { mode: "json" }).$type().notNull(),
|
|
33
|
+
createdBy: text("created_by").notNull(),
|
|
34
|
+
approvedBy: text("approved_by", { mode: "json" }).$type().default(sql`'[]'`).notNull(),
|
|
35
|
+
approvedAt: integer("approved_at", { mode: "timestamp" }),
|
|
36
|
+
changeSummary: text("change_summary"),
|
|
37
|
+
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull()
|
|
38
|
+
}, (table) => [uniqueIndex("page_versions_page_id_version_idx").on(table.pageId, table.versionNumber)]);
|
|
39
|
+
const pages = sqliteTable("pages", {
|
|
40
|
+
id: text("id").$defaultFn(() => crypto.randomUUID()).notNull().primaryKey(),
|
|
41
|
+
slug: text("slug").notNull(),
|
|
42
|
+
type: text("type").$type().notNull(),
|
|
43
|
+
templateId: text("template_id").references(() => pageTemplates.id, { onDelete: "set null" }),
|
|
44
|
+
templateVersion: integer("template_version").default(1).notNull(),
|
|
45
|
+
title: text("title", { mode: "json" }).$type().notNull(),
|
|
46
|
+
description: text("description", { mode: "json" }).$type(),
|
|
47
|
+
tags: text("tags", { mode: "json" }).$type().default(sql`'[]'`).notNull(),
|
|
48
|
+
authorIds: text("author_ids", { mode: "json" }).$type().default(sql`'[]'`),
|
|
49
|
+
bylines: text("bylines", { mode: "json" }).$type().default(sql`'[]'`),
|
|
50
|
+
content: text("content", { mode: "json" }).$type().notNull(),
|
|
51
|
+
publishedVersionId: text("published_version_id").references(() => pageVersions.id, { onDelete: "set null" }),
|
|
52
|
+
postedAt: integer("posted_at", { mode: "timestamp" }),
|
|
53
|
+
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull(),
|
|
54
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).$onUpdate(() => /* @__PURE__ */ new Date()),
|
|
55
|
+
deletedAt: integer("deleted_at", { mode: "timestamp" })
|
|
56
|
+
}, (table) => [uniqueIndex("pages_slug_active_unique_idx").on(table.slug).where(sql`deleted_at IS NULL`)]);
|
|
57
|
+
const pageDrafts = sqliteTable("page_drafts", {
|
|
58
|
+
pageId: text("page_id").notNull().primaryKey().references(() => pages.id, { onDelete: "cascade" }),
|
|
59
|
+
content: text("content", { mode: "json" }).$type().notNull(),
|
|
60
|
+
updatedBy: text("updated_by").notNull(),
|
|
61
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull()
|
|
62
|
+
});
|
|
63
|
+
const pageDraftLocks = sqliteTable("page_draft_locks", {
|
|
64
|
+
pageId: text("page_id").notNull().primaryKey().references(() => pages.id, { onDelete: "cascade" }),
|
|
65
|
+
lockedByUserId: text("locked_by_user_id").notNull(),
|
|
66
|
+
lockedByUserName: text("locked_by_user_name").notNull(),
|
|
67
|
+
acquiredAt: integer("acquired_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull(),
|
|
68
|
+
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull()
|
|
69
|
+
});
|
|
70
|
+
const pageVersionApprovals = sqliteTable("page_version_approvals", {
|
|
71
|
+
id: text("id").$defaultFn(() => crypto.randomUUID()).notNull().primaryKey(),
|
|
72
|
+
versionId: text("version_id").notNull().references(() => pageVersions.id, { onDelete: "cascade" }),
|
|
73
|
+
userId: text("user_id").notNull(),
|
|
74
|
+
userRole: text("user_role").notNull(),
|
|
75
|
+
approvedAt: integer("approved_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull()
|
|
76
|
+
});
|
|
77
|
+
const pageVersionComments = sqliteTable("page_version_comments", {
|
|
78
|
+
id: text("id").$defaultFn(() => crypto.randomUUID()).notNull().primaryKey(),
|
|
79
|
+
versionId: text("version_id").notNull().references(() => pageVersions.id, { onDelete: "cascade" }),
|
|
80
|
+
userId: text("user_id").notNull(),
|
|
81
|
+
userRole: text("user_role").notNull(),
|
|
82
|
+
content: text("content").notNull(),
|
|
83
|
+
blockId: text("block_id"),
|
|
84
|
+
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull()
|
|
85
|
+
});
|
|
86
|
+
const contentSearchIndex = sqliteTable("content_search_index", {
|
|
87
|
+
id: text("id").$defaultFn(() => crypto.randomUUID()).notNull().primaryKey(),
|
|
88
|
+
pageId: text("page_id").notNull().references(() => pages.id, { onDelete: "cascade" }),
|
|
89
|
+
locale: text("locale").notNull(),
|
|
90
|
+
titleText: text("title_text").notNull(),
|
|
91
|
+
contentText: text("content_text").notNull()
|
|
92
|
+
}, (table) => [index("content_search_index_page_id_idx").on(table.pageId)]);
|
|
93
|
+
const siteSettings = sqliteTable("site_settings", {
|
|
94
|
+
id: text("id").primaryKey().default("default"),
|
|
95
|
+
name: text("name").notNull(),
|
|
96
|
+
description: text("description").notNull(),
|
|
97
|
+
url: text("url").notNull(),
|
|
98
|
+
ogImage: text("og_image").notNull(),
|
|
99
|
+
author: text("author").notNull(),
|
|
100
|
+
email: text("email").notNull().default(""),
|
|
101
|
+
branding: text("branding", { mode: "json" }).$type().notNull(),
|
|
102
|
+
seo: text("seo", { mode: "json" }).$type().notNull(),
|
|
103
|
+
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull(),
|
|
104
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).$onUpdate(() => /* @__PURE__ */ new Date()).notNull()
|
|
105
|
+
});
|
|
106
|
+
const taxonomyTerms = sqliteTable("taxonomy_terms", {
|
|
107
|
+
id: text("id").$defaultFn(() => crypto.randomUUID()).notNull().primaryKey(),
|
|
108
|
+
taxonomy: text("taxonomy").notNull(),
|
|
109
|
+
slug: text("slug").notNull(),
|
|
110
|
+
label: text("label", { mode: "json" }).$type().notNull(),
|
|
111
|
+
description: text("description", { mode: "json" }).$type(),
|
|
112
|
+
parentId: text("parent_id").references(() => taxonomyTerms.id, { onDelete: "cascade" }),
|
|
113
|
+
displayOrder: integer("display_order").default(0).notNull(),
|
|
114
|
+
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull(),
|
|
115
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).$onUpdate(() => /* @__PURE__ */ new Date())
|
|
116
|
+
}, (table) => [uniqueIndex("taxonomy_terms_tax_slug_idx").on(table.taxonomy, table.slug)]);
|
|
117
|
+
const pageTaxonomyTerms = sqliteTable("page_taxonomy_terms", {
|
|
118
|
+
pageId: text("page_id").notNull().references(() => pages.id, { onDelete: "cascade" }),
|
|
119
|
+
termId: text("term_id").notNull().references(() => taxonomyTerms.id, { onDelete: "cascade" }),
|
|
120
|
+
assignedAt: integer("assigned_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull()
|
|
121
|
+
}, (table) => [uniqueIndex("page_taxonomy_terms_page_term_idx").on(table.pageId, table.termId)]);
|
|
122
|
+
const bylines = sqliteTable("bylines", {
|
|
123
|
+
id: text("id").$defaultFn(() => crypto.randomUUID()).primaryKey(),
|
|
124
|
+
name: text("name").notNull(),
|
|
125
|
+
slug: text("slug").notNull(),
|
|
126
|
+
websiteUrl: text("website_url"),
|
|
127
|
+
bio: text("bio"),
|
|
128
|
+
avatar: text("avatar"),
|
|
129
|
+
userId: text("user_id"),
|
|
130
|
+
socials: text("socials", { mode: "json" }).$type().default(sql`'{}'`),
|
|
131
|
+
metadata: text("metadata", { mode: "json" }).$type().default(sql`'{}'`),
|
|
132
|
+
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull(),
|
|
133
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull(),
|
|
134
|
+
deletedAt: integer("deleted_at", { mode: "timestamp" })
|
|
135
|
+
}, (table) => [uniqueIndex("bylines_slug_idx").on(table.slug)]);
|
|
136
|
+
//#endregion
|
|
137
|
+
export { bylines, contentSearchIndex, pageDraftLocks, pageDrafts, pageTaxonomyTerms, pageTemplates, pageVersionApprovals, pageVersionComments, pageVersions, pages, siteSettings, taxonomyTerms };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//#region src/storage/index.d.ts
|
|
2
|
+
interface StorageFile {
|
|
3
|
+
key: string;
|
|
4
|
+
name: string;
|
|
5
|
+
size: number;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
uploadedAt: string;
|
|
8
|
+
url: string;
|
|
9
|
+
etag?: string;
|
|
10
|
+
customMetadata?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
interface StorageListResult {
|
|
13
|
+
files: StorageFile[];
|
|
14
|
+
truncated: boolean;
|
|
15
|
+
cursor?: string;
|
|
16
|
+
}
|
|
17
|
+
interface R2StorageOptions {
|
|
18
|
+
binding?: string | undefined;
|
|
19
|
+
publicUrl?: string | undefined;
|
|
20
|
+
prefix?: string | undefined;
|
|
21
|
+
}
|
|
22
|
+
interface StorageConfig {
|
|
23
|
+
provider: "r2" | "local" | "s3";
|
|
24
|
+
binding?: string | undefined;
|
|
25
|
+
publicUrl?: string | undefined;
|
|
26
|
+
prefix?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
interface StorageListOptions {
|
|
29
|
+
prefix?: string | undefined;
|
|
30
|
+
cursor?: string | undefined;
|
|
31
|
+
limit?: number | undefined;
|
|
32
|
+
publicUrl?: string | undefined;
|
|
33
|
+
apiBasePath?: string | undefined;
|
|
34
|
+
}
|
|
35
|
+
interface StorageUploadOptions {
|
|
36
|
+
key?: string | undefined;
|
|
37
|
+
prefix?: string | undefined;
|
|
38
|
+
publicUrl?: string | undefined;
|
|
39
|
+
apiBasePath?: string | undefined;
|
|
40
|
+
}
|
|
41
|
+
declare function r2(options?: R2StorageOptions): StorageConfig;
|
|
42
|
+
/**
|
|
43
|
+
* Resolves Cloudflare R2 bucket instance from Cloudflare env or Astro locals.
|
|
44
|
+
*/
|
|
45
|
+
declare function getR2Bucket(locals?: any, bindingName?: string): Promise<any>;
|
|
46
|
+
/**
|
|
47
|
+
* Lists files from R2 Bucket under configured prefix.
|
|
48
|
+
*/
|
|
49
|
+
declare function listR2Files(bucket: any, options?: StorageListOptions): Promise<StorageListResult>;
|
|
50
|
+
/**
|
|
51
|
+
* Uploads a file buffer/stream into R2 Bucket.
|
|
52
|
+
*/
|
|
53
|
+
declare function uploadR2File(bucket: any, file: File, options?: StorageUploadOptions): Promise<StorageFile>;
|
|
54
|
+
/**
|
|
55
|
+
* Deletes a file from R2 Bucket by key.
|
|
56
|
+
*/
|
|
57
|
+
declare function deleteR2File(bucket: any, key: string): Promise<boolean>;
|
|
58
|
+
//#endregion
|
|
59
|
+
export { R2StorageOptions, StorageConfig, StorageFile, StorageListOptions, StorageListResult, StorageUploadOptions, deleteR2File, getR2Bucket, listR2Files, r2, uploadR2File };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
//#region src/storage/index.ts
|
|
2
|
+
function r2(options = {}) {
|
|
3
|
+
return {
|
|
4
|
+
provider: "r2",
|
|
5
|
+
binding: options.binding ?? "BLOB",
|
|
6
|
+
publicUrl: options.publicUrl,
|
|
7
|
+
prefix: options.prefix ?? "media/"
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolves Cloudflare R2 bucket instance from Cloudflare env or Astro locals.
|
|
12
|
+
*/
|
|
13
|
+
async function getR2Bucket(locals, bindingName = "BLOB") {
|
|
14
|
+
try {
|
|
15
|
+
const cf = await import("cloudflare:workers");
|
|
16
|
+
if (cf?.env && cf.env[bindingName]) return cf.env[bindingName];
|
|
17
|
+
} catch {}
|
|
18
|
+
if (locals) {
|
|
19
|
+
if (locals.runtime?.env && locals.runtime.env[bindingName]) return locals.runtime.env[bindingName];
|
|
20
|
+
if (locals.env && locals.env[bindingName]) return locals.env[bindingName];
|
|
21
|
+
if (locals[bindingName]) return locals[bindingName];
|
|
22
|
+
}
|
|
23
|
+
if (typeof process !== "undefined" && process.env && process.env[bindingName]) return process.env[bindingName];
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Lists files from R2 Bucket under configured prefix.
|
|
28
|
+
*/
|
|
29
|
+
async function listR2Files(bucket, options = {}) {
|
|
30
|
+
if (!bucket || typeof bucket.list !== "function") return {
|
|
31
|
+
files: [],
|
|
32
|
+
truncated: false
|
|
33
|
+
};
|
|
34
|
+
const prefix = options.prefix ?? "media/";
|
|
35
|
+
const limit = options.limit ?? 50;
|
|
36
|
+
const result = await bucket.list({
|
|
37
|
+
prefix,
|
|
38
|
+
cursor: options.cursor,
|
|
39
|
+
limit
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
files: (result.objects || []).map((obj) => {
|
|
43
|
+
const rawKey = obj.key;
|
|
44
|
+
const name = rawKey.startsWith(prefix) ? rawKey.slice(prefix.length) : rawKey;
|
|
45
|
+
const cleanedName = name.includes("/") ? name.split("/").pop() : name;
|
|
46
|
+
const mimeType = obj.httpMetadata?.contentType || guessMimeType(rawKey);
|
|
47
|
+
let url = "";
|
|
48
|
+
if (options.publicUrl) url = `${options.publicUrl.endsWith("/") ? options.publicUrl.slice(0, -1) : options.publicUrl}/${rawKey}`;
|
|
49
|
+
else if (options.apiBasePath) url = `${options.apiBasePath.endsWith("/") ? options.apiBasePath.slice(0, -1) : options.apiBasePath}/${rawKey}`;
|
|
50
|
+
else url = `/cms/api/media/${rawKey}`;
|
|
51
|
+
return {
|
|
52
|
+
key: rawKey,
|
|
53
|
+
name: cleanedName,
|
|
54
|
+
size: obj.size,
|
|
55
|
+
mimeType,
|
|
56
|
+
uploadedAt: obj.uploaded ? new Date(obj.uploaded).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
57
|
+
url,
|
|
58
|
+
etag: obj.etag,
|
|
59
|
+
customMetadata: obj.customMetadata
|
|
60
|
+
};
|
|
61
|
+
}),
|
|
62
|
+
truncated: result.truncated,
|
|
63
|
+
cursor: result.cursor
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Uploads a file buffer/stream into R2 Bucket.
|
|
68
|
+
*/
|
|
69
|
+
async function uploadR2File(bucket, file, options = {}) {
|
|
70
|
+
if (!bucket || typeof bucket.put !== "function") throw new Error("R2 bucket binding is not available in runtime environment.");
|
|
71
|
+
const prefix = options.prefix ?? "media/";
|
|
72
|
+
const timestamp = Date.now();
|
|
73
|
+
const safeName = file.name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
74
|
+
const randomSuffix = Math.random().toString(36).substring(2, 8);
|
|
75
|
+
const key = options.key ?? `${prefix}${timestamp}-${randomSuffix}/${safeName}`;
|
|
76
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
77
|
+
const r2Object = await bucket.put(key, arrayBuffer, {
|
|
78
|
+
httpMetadata: {
|
|
79
|
+
contentType: file.type || guessMimeType(safeName),
|
|
80
|
+
contentDisposition: `inline; filename="${safeName}"`
|
|
81
|
+
},
|
|
82
|
+
customMetadata: {
|
|
83
|
+
originalName: file.name,
|
|
84
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
let url = "";
|
|
88
|
+
if (options.publicUrl) url = `${options.publicUrl.endsWith("/") ? options.publicUrl.slice(0, -1) : options.publicUrl}/${key}`;
|
|
89
|
+
else if (options.apiBasePath) url = `${options.apiBasePath.endsWith("/") ? options.apiBasePath.slice(0, -1) : options.apiBasePath}/${key}`;
|
|
90
|
+
else url = `/cms/api/media/${key}`;
|
|
91
|
+
return {
|
|
92
|
+
key,
|
|
93
|
+
name: safeName,
|
|
94
|
+
size: file.size,
|
|
95
|
+
mimeType: file.type || guessMimeType(safeName),
|
|
96
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
97
|
+
url,
|
|
98
|
+
etag: r2Object?.etag
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Deletes a file from R2 Bucket by key.
|
|
103
|
+
*/
|
|
104
|
+
async function deleteR2File(bucket, key) {
|
|
105
|
+
if (!bucket || typeof bucket.delete !== "function") throw new Error("R2 bucket binding is not available in runtime environment.");
|
|
106
|
+
await bucket.delete(key);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* MIME type guesser fallback.
|
|
111
|
+
*/
|
|
112
|
+
function guessMimeType(filename) {
|
|
113
|
+
switch (filename.split(".").pop()?.toLowerCase()) {
|
|
114
|
+
case "jpg":
|
|
115
|
+
case "jpeg": return "image/jpeg";
|
|
116
|
+
case "png": return "image/png";
|
|
117
|
+
case "webp": return "image/webp";
|
|
118
|
+
case "gif": return "image/gif";
|
|
119
|
+
case "svg": return "image/svg+xml";
|
|
120
|
+
case "avif": return "image/avif";
|
|
121
|
+
case "mp4": return "video/mp4";
|
|
122
|
+
case "webm": return "video/webm";
|
|
123
|
+
case "mp3": return "audio/mpeg";
|
|
124
|
+
case "wav": return "audio/wav";
|
|
125
|
+
case "pdf": return "application/pdf";
|
|
126
|
+
case "json": return "application/json";
|
|
127
|
+
case "txt": return "text/plain";
|
|
128
|
+
case "zip": return "application/zip";
|
|
129
|
+
default: return "application/octet-stream";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
export { deleteR2File, getR2Bucket, listR2Files, r2, uploadR2File };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimelight/cms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Rimelight Entertainment's Content Management System Package",
|
|
6
6
|
"homepage": "https://rimelight.com/docs",
|
|
@@ -16,14 +16,38 @@
|
|
|
16
16
|
"url": "git+https://github.com/Rimelight-Entertainment/rimelight.git"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
+
"dist",
|
|
19
20
|
"src"
|
|
20
21
|
],
|
|
21
22
|
"type": "module",
|
|
22
23
|
"exports": {
|
|
23
|
-
".":
|
|
24
|
-
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.mts",
|
|
26
|
+
"import": "./dist/index.mjs"
|
|
27
|
+
},
|
|
28
|
+
"./schema": {
|
|
29
|
+
"types": "./dist/schema/index.d.mts",
|
|
30
|
+
"import": "./dist/schema/index.mjs"
|
|
31
|
+
},
|
|
32
|
+
"./schema/sqlite": {
|
|
33
|
+
"types": "./dist/schema/sqlite.d.mts",
|
|
34
|
+
"import": "./dist/schema/sqlite.mjs"
|
|
35
|
+
},
|
|
36
|
+
"./integration": {
|
|
37
|
+
"types": "./dist/integration.d.mts",
|
|
38
|
+
"import": "./dist/integration.mjs"
|
|
39
|
+
},
|
|
40
|
+
"./storage": {
|
|
41
|
+
"types": "./dist/storage/index.d.mts",
|
|
42
|
+
"import": "./dist/storage/index.mjs"
|
|
43
|
+
},
|
|
25
44
|
"./core/*": "./src/core/*",
|
|
26
45
|
"./astro/*": "./src/astro/*",
|
|
46
|
+
"./cron/scheduled": {
|
|
47
|
+
"types": "./src/cron/scheduled.ts",
|
|
48
|
+
"import": "./src/cron/scheduled.ts"
|
|
49
|
+
},
|
|
50
|
+
"./cron/*": "./src/cron/*",
|
|
27
51
|
"./client/styles/*": "./src/client/styles/*",
|
|
28
52
|
"./client/components": "./src/client/components/index.ts",
|
|
29
53
|
"./client/components/*.tsx": "./src/client/components/*.tsx",
|
|
@@ -34,38 +58,35 @@
|
|
|
34
58
|
"./auth/*": "./src/auth/*",
|
|
35
59
|
"./cache/*": "./src/cache/*",
|
|
36
60
|
"./search/*": "./src/search/*",
|
|
37
|
-
"./integration": "./src/integration.ts",
|
|
38
|
-
"./storage": "./src/storage/index.ts",
|
|
39
61
|
"./admin/*": "./src/admin/*"
|
|
40
62
|
},
|
|
41
63
|
"publishConfig": {
|
|
42
64
|
"access": "public"
|
|
43
65
|
},
|
|
44
66
|
"dependencies": {
|
|
45
|
-
"@rimelight/
|
|
46
|
-
"@rimelight/
|
|
67
|
+
"@rimelight/auth": "0.0.3",
|
|
68
|
+
"@rimelight/i18n": "0.0.9",
|
|
69
|
+
"@rimelight/ui": "0.0.47",
|
|
47
70
|
"drizzle-orm": "0.45.2",
|
|
48
71
|
"solid-js": "1.9.15",
|
|
49
72
|
"sortablejs": "1.15.7"
|
|
50
73
|
},
|
|
51
74
|
"devDependencies": {
|
|
52
75
|
"@astrojs/check": "0.9.10",
|
|
53
|
-
"@rimelight/config": "0.0.
|
|
76
|
+
"@rimelight/config": "0.0.5",
|
|
54
77
|
"@types/sortablejs": "1.15.9",
|
|
55
|
-
"astro": "7.
|
|
56
|
-
"better-auth": "1.7.2",
|
|
78
|
+
"astro": "7.3.1",
|
|
57
79
|
"typescript": "6.0.3",
|
|
58
|
-
"unocss": "66.
|
|
59
|
-
"vitest": "4.1.11"
|
|
80
|
+
"unocss": "66.10.0"
|
|
60
81
|
},
|
|
61
82
|
"peerDependencies": {
|
|
62
|
-
"astro": ">=7.0.0"
|
|
63
|
-
"better-auth": ">=1.0.0"
|
|
83
|
+
"astro": ">=7.0.0"
|
|
64
84
|
},
|
|
65
85
|
"engines": {
|
|
66
86
|
"node": ">=26.7.0"
|
|
67
87
|
},
|
|
68
88
|
"scripts": {
|
|
89
|
+
"build": "vp pack",
|
|
69
90
|
"check": "vp check --fix && astro check"
|
|
70
91
|
}
|
|
71
92
|
}
|
|
@@ -14,7 +14,7 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
|
|
14
14
|
return new Response("Storage binding not found.", { status: 500 })
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const key = params
|
|
17
|
+
const key = params["key"] as string
|
|
18
18
|
if (!key) {
|
|
19
19
|
return new Response("Missing file key.", { status: 400 })
|
|
20
20
|
}
|
package/src/admin/api/media.ts
CHANGED
|
@@ -2,11 +2,20 @@ import type { APIRoute } from "astro"
|
|
|
2
2
|
import { getR2Bucket, listR2Files, uploadR2File, deleteR2File } from "../../storage/index"
|
|
3
3
|
// @ts-ignore
|
|
4
4
|
import { storageConfig } from "virtual:rimelight-cms/storage"
|
|
5
|
+
// @ts-ignore
|
|
6
|
+
import authAdapter from "virtual:rimelight-cms/auth"
|
|
5
7
|
|
|
6
8
|
export const prerender = false
|
|
7
9
|
|
|
8
10
|
export const GET: APIRoute = async ({ request, locals }) => {
|
|
9
11
|
try {
|
|
12
|
+
const session = await authAdapter.getSession(request)
|
|
13
|
+
if (!session) {
|
|
14
|
+
return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), {
|
|
15
|
+
status: 401,
|
|
16
|
+
headers: { "Content-Type": "application/json" }
|
|
17
|
+
})
|
|
18
|
+
}
|
|
10
19
|
const url = new URL(request.url)
|
|
11
20
|
const prefix = (url.searchParams.get("prefix") ??
|
|
12
21
|
(storageConfig?.prefix as string | undefined) ??
|
|
@@ -17,7 +26,7 @@ export const GET: APIRoute = async ({ request, locals }) => {
|
|
|
17
26
|
const typeFilter = url.searchParams.get("type")?.toLowerCase()
|
|
18
27
|
|
|
19
28
|
const bindingName = (storageConfig?.binding as string | undefined) ?? "BLOB"
|
|
20
|
-
const bucket = getR2Bucket(locals, bindingName)
|
|
29
|
+
const bucket = await getR2Bucket(locals, bindingName)
|
|
21
30
|
|
|
22
31
|
if (!bucket) {
|
|
23
32
|
return new Response(
|
|
@@ -87,8 +96,16 @@ export const GET: APIRoute = async ({ request, locals }) => {
|
|
|
87
96
|
|
|
88
97
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
89
98
|
try {
|
|
99
|
+
const session = await authAdapter.getSession(request)
|
|
100
|
+
if (!session) {
|
|
101
|
+
return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), {
|
|
102
|
+
status: 401,
|
|
103
|
+
headers: { "Content-Type": "application/json" }
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
90
107
|
const bindingName = storageConfig?.binding ?? "BLOB"
|
|
91
|
-
const bucket = getR2Bucket(locals, bindingName)
|
|
108
|
+
const bucket = await getR2Bucket(locals, bindingName)
|
|
92
109
|
|
|
93
110
|
if (!bucket) {
|
|
94
111
|
return new Response(
|
|
@@ -150,8 +167,16 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
150
167
|
|
|
151
168
|
export const DELETE: APIRoute = async ({ request, locals }) => {
|
|
152
169
|
try {
|
|
170
|
+
const session = await authAdapter.getSession(request)
|
|
171
|
+
if (!session) {
|
|
172
|
+
return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), {
|
|
173
|
+
status: 401,
|
|
174
|
+
headers: { "Content-Type": "application/json" }
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
153
178
|
const bindingName = storageConfig?.binding ?? "BLOB"
|
|
154
|
-
const bucket = getR2Bucket(locals, bindingName)
|
|
179
|
+
const bucket = await getR2Bucket(locals, bindingName)
|
|
155
180
|
|
|
156
181
|
if (!bucket) {
|
|
157
182
|
return new Response(
|
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
---
|
|
2
2
|
import ParentLayout from "virtual:rimelight-cms/parent-layout";
|
|
3
|
+
import authAdapter from "virtual:rimelight-cms/auth";
|
|
3
4
|
import RLADashboardGroup from "@rimelight/ui/components/dashboard-group/RLADashboardGroup.astro";
|
|
4
5
|
import RLADashboardSidebar from "@rimelight/ui/components/dashboard-sidebar/RLADashboardSidebar.astro";
|
|
5
6
|
import RLADashboardNavbar from "@rimelight/ui/components/dashboard-navbar/RLADashboardNavbar.astro";
|
|
6
7
|
import RLADashboardPanel from "@rimelight/ui/components/dashboard-panel/RLADashboardPanel.astro";
|
|
7
8
|
import RLADashboardToolbar from "@rimelight/ui/components/dashboard-toolbar/RLADashboardToolbar.astro";
|
|
8
9
|
import RLADashboardSidebarCollapse from "@rimelight/ui/components/dashboard-sidebar-collapse/RLADashboardSidebarCollapse.astro";
|
|
10
|
+
import RLADashboardSidebarToggle from "@rimelight/ui/components/dashboard-sidebar-toggle/RLADashboardSidebarToggle.astro";
|
|
9
11
|
import RLAIcon from "@rimelight/ui/components/icon/RLAIcon.astro";
|
|
10
12
|
import { getRelativeLocaleUrl } from "@rimelight/i18n";
|
|
11
13
|
import "../../client/styles/cms-editor.css";
|
|
12
14
|
|
|
15
|
+
const session = await authAdapter.getSession(Astro.request);
|
|
16
|
+
if (!session) {
|
|
17
|
+
if (authAdapter.handleUnauthorized) {
|
|
18
|
+
const res = await authAdapter.handleUnauthorized(Astro.request);
|
|
19
|
+
if (res instanceof Response) return res;
|
|
20
|
+
}
|
|
21
|
+
return Astro.redirect(getRelativeLocaleUrl(Astro.currentLocale ?? "en", "/auth/sign-in"));
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
const currentLocale = Astro.currentLocale ?? "en";
|
|
14
25
|
|
|
15
26
|
const dashboardItem = {
|
|
@@ -149,10 +160,12 @@ const {
|
|
|
149
160
|
</RLADashboardSidebar>
|
|
150
161
|
|
|
151
162
|
<RLADashboardPanel>
|
|
152
|
-
<RLADashboardNavbar slot="header" title={title} icon="i-lucide-file-text"
|
|
163
|
+
<RLADashboardNavbar slot="header" title={title} icon="i-lucide-file-text" class="h-16 min-h-16">
|
|
153
164
|
<Fragment slot="left">
|
|
154
|
-
<div class="h-16 min-h-16 flex items-center gap-3"
|
|
155
|
-
<
|
|
165
|
+
<div class="h-16 min-h-16 flex items-center gap-3">
|
|
166
|
+
<RLADashboardSidebarToggle class="flex lg:hidden size-9 rounded-lg items-center justify-center border border-default hover:bg-muted text-toned hover:text-highlighted transition-colors shrink-0" />
|
|
167
|
+
<RLADashboardSidebarCollapse side="left" class="hidden lg:flex size-9 rounded-lg" />
|
|
168
|
+
<h1 class="text-lg sm:text-xl font-semibold text-highlighted truncate" data-dashboard-navbar-title>{title}</h1>
|
|
156
169
|
</div>
|
|
157
170
|
</Fragment>
|
|
158
171
|
<Fragment slot="right">
|
|
@@ -199,7 +199,7 @@ function formatDate(dateStr?: string): string {
|
|
|
199
199
|
const root = document.getElementById("assets-page-root");
|
|
200
200
|
if (!root) return;
|
|
201
201
|
|
|
202
|
-
const apiEndpoint = root.dataset
|
|
202
|
+
const apiEndpoint = root.dataset["apiEndpoint"] || "/cms/api/media";
|
|
203
203
|
const uploadInput = document.getElementById("media-upload-input") as HTMLInputElement | null;
|
|
204
204
|
const dropzone = document.getElementById("media-dropzone");
|
|
205
205
|
const indicator = document.getElementById("upload-status-indicator");
|
|
@@ -280,10 +280,10 @@ function formatDate(dateStr?: string): string {
|
|
|
280
280
|
|
|
281
281
|
const card = document.createElement("div");
|
|
282
282
|
card.className = "border border-default rounded-lg bg-default overflow-hidden flex flex-col hover:border-primary/50 transition-all group asset-card animate-in fade-in";
|
|
283
|
-
card.dataset
|
|
284
|
-
card.dataset
|
|
285
|
-
card.dataset
|
|
286
|
-
card.dataset
|
|
283
|
+
card.dataset["key"] = asset.key;
|
|
284
|
+
card.dataset["name"] = asset.name;
|
|
285
|
+
card.dataset["type"] = asset.mimeType;
|
|
286
|
+
card.dataset["url"] = asset.url;
|
|
287
287
|
|
|
288
288
|
card.innerHTML = `
|
|
289
289
|
<div class="h-40 bg-muted/30 flex items-center justify-center relative overflow-hidden">
|
|
@@ -320,7 +320,7 @@ function formatDate(dateStr?: string): string {
|
|
|
320
320
|
parent.querySelectorAll(".copy-url-btn").forEach((btn) => {
|
|
321
321
|
btn.addEventListener("click", async (e) => {
|
|
322
322
|
e.stopPropagation();
|
|
323
|
-
const url = (btn as HTMLElement).dataset
|
|
323
|
+
const url = (btn as HTMLElement).dataset["url"];
|
|
324
324
|
if (url) {
|
|
325
325
|
const fullUrl = url.startsWith("http") ? url : window.location.origin + url;
|
|
326
326
|
await navigator.clipboard.writeText(fullUrl);
|
|
@@ -337,7 +337,7 @@ function formatDate(dateStr?: string): string {
|
|
|
337
337
|
parent.querySelectorAll(".delete-asset-btn").forEach((btn) => {
|
|
338
338
|
btn.addEventListener("click", async (e) => {
|
|
339
339
|
e.stopPropagation();
|
|
340
|
-
const key = (btn as HTMLElement).dataset
|
|
340
|
+
const key = (btn as HTMLElement).dataset["key"];
|
|
341
341
|
if (!key) return;
|
|
342
342
|
if (!confirm(`Are you sure you want to delete this file from R2 storage?\nKey: ${key}`)) return;
|
|
343
343
|
|
|
@@ -368,9 +368,9 @@ function formatDate(dateStr?: string): string {
|
|
|
368
368
|
let visibleCount = 0;
|
|
369
369
|
|
|
370
370
|
cards.forEach((card) => {
|
|
371
|
-
const name = (card.dataset
|
|
372
|
-
const key = (card.dataset
|
|
373
|
-
const type = (card.dataset
|
|
371
|
+
const name = (card.dataset["name"] || "").toLowerCase();
|
|
372
|
+
const key = (card.dataset["key"] || "").toLowerCase();
|
|
373
|
+
const type = (card.dataset["type"] || "").toLowerCase();
|
|
374
374
|
|
|
375
375
|
const matchesSearch = !searchQuery || name.includes(searchQuery) || key.includes(searchQuery);
|
|
376
376
|
let matchesFilter = true;
|
|
@@ -408,7 +408,7 @@ function formatDate(dateStr?: string): string {
|
|
|
408
408
|
btn.classList.remove("bg-muted", "text-muted-foreground");
|
|
409
409
|
btn.classList.add("bg-primary", "text-white");
|
|
410
410
|
|
|
411
|
-
currentFilter = (btn as HTMLElement).dataset
|
|
411
|
+
currentFilter = (btn as HTMLElement).dataset["filter"] || "all";
|
|
412
412
|
filterCards();
|
|
413
413
|
});
|
|
414
414
|
});
|
|
@@ -15,7 +15,7 @@ import "../../client/styles/cms-editor.css";
|
|
|
15
15
|
export const prerender = false;
|
|
16
16
|
|
|
17
17
|
const currentLocale = Astro.currentLocale ?? "en";
|
|
18
|
-
const id = Astro.params
|
|
18
|
+
const id = Astro.params["id"];
|
|
19
19
|
|
|
20
20
|
let page: any = null;
|
|
21
21
|
let draft: any = null;
|
|
@@ -57,12 +57,12 @@ const extractedBlocks = Array.isArray(rawContent?.blocks)
|
|
|
57
57
|
|
|
58
58
|
const titleVal =
|
|
59
59
|
typeof page.title === "object" && page.title !== null
|
|
60
|
-
? (page.title as Record<string, string>)[currentLocale] || (page.title as Record<string, string>)
|
|
60
|
+
? (page.title as Record<string, string>)[currentLocale] || (page.title as Record<string, string>)["en"] || ""
|
|
61
61
|
: String(page.title || "");
|
|
62
62
|
|
|
63
63
|
const descVal =
|
|
64
64
|
typeof page.description === "object" && page.description !== null
|
|
65
|
-
? (page.description as Record<string, string>)[currentLocale] || (page.description as Record<string, string>)
|
|
65
|
+
? (page.description as Record<string, string>)[currentLocale] || (page.description as Record<string, string>)["en"] || ""
|
|
66
66
|
: String(page.description || "");
|
|
67
67
|
|
|
68
68
|
const pageType = (page as any).type || "blog";
|
|
@@ -29,7 +29,7 @@ if (db) {
|
|
|
29
29
|
const formattedPages = pagesList.map((page) => {
|
|
30
30
|
const titleVal =
|
|
31
31
|
typeof page.title === "object" && page.title !== null
|
|
32
|
-
? (page.title as Record<string, string>)
|
|
32
|
+
? (page.title as Record<string, string>)["en"] || ""
|
|
33
33
|
: String(page.title || "");
|
|
34
34
|
return {
|
|
35
35
|
id: page.id,
|
|
@@ -124,7 +124,7 @@ const columns: TableColumn[] = [
|
|
|
124
124
|
<script>
|
|
125
125
|
document.querySelectorAll(".delete-page-btn").forEach((btn) => {
|
|
126
126
|
btn.addEventListener("click", async () => {
|
|
127
|
-
const pageId = (btn as HTMLElement).dataset
|
|
127
|
+
const pageId = (btn as HTMLElement).dataset["pageId"];
|
|
128
128
|
if (!pageId) return;
|
|
129
129
|
if (!confirm("Are you sure you want to delete this page?")) return;
|
|
130
130
|
|