@rimelight/cms 0.0.3 → 0.0.4
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 +30 -14
- package/src/admin/api/media.ts +28 -3
- package/src/admin/layouts/CMSDashboardLayout.astro +16 -3
- package/src/admin/pages/pages-preview.astro +1 -1
- package/src/astro/PageRenderer.astro +4 -4
- package/src/env.d.ts +6 -0
- package/src/index.ts +17 -4
- package/src/integration.ts +54 -0
- 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.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Rimelight Entertainment's Content Management System Package",
|
|
6
6
|
"homepage": "https://rimelight.com/docs",
|
|
@@ -16,12 +16,31 @@
|
|
|
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/*",
|
|
27
46
|
"./client/styles/*": "./src/client/styles/*",
|
|
@@ -34,38 +53,35 @@
|
|
|
34
53
|
"./auth/*": "./src/auth/*",
|
|
35
54
|
"./cache/*": "./src/cache/*",
|
|
36
55
|
"./search/*": "./src/search/*",
|
|
37
|
-
"./integration": "./src/integration.ts",
|
|
38
|
-
"./storage": "./src/storage/index.ts",
|
|
39
56
|
"./admin/*": "./src/admin/*"
|
|
40
57
|
},
|
|
41
58
|
"publishConfig": {
|
|
42
59
|
"access": "public"
|
|
43
60
|
},
|
|
44
61
|
"dependencies": {
|
|
45
|
-
"@rimelight/
|
|
46
|
-
"@rimelight/
|
|
62
|
+
"@rimelight/auth": "0.0.3",
|
|
63
|
+
"@rimelight/i18n": "0.0.7",
|
|
64
|
+
"@rimelight/ui": "0.0.46",
|
|
47
65
|
"drizzle-orm": "0.45.2",
|
|
48
66
|
"solid-js": "1.9.15",
|
|
49
67
|
"sortablejs": "1.15.7"
|
|
50
68
|
},
|
|
51
69
|
"devDependencies": {
|
|
52
70
|
"@astrojs/check": "0.9.10",
|
|
53
|
-
"@rimelight/config": "0.0.
|
|
71
|
+
"@rimelight/config": "0.0.4",
|
|
54
72
|
"@types/sortablejs": "1.15.9",
|
|
55
|
-
"astro": "7.
|
|
56
|
-
"better-auth": "1.7.2",
|
|
73
|
+
"astro": "7.3.1",
|
|
57
74
|
"typescript": "6.0.3",
|
|
58
|
-
"unocss": "66.
|
|
59
|
-
"vitest": "4.1.11"
|
|
75
|
+
"unocss": "66.10.0"
|
|
60
76
|
},
|
|
61
77
|
"peerDependencies": {
|
|
62
|
-
"astro": ">=7.0.0"
|
|
63
|
-
"better-auth": ">=1.0.0"
|
|
78
|
+
"astro": ">=7.0.0"
|
|
64
79
|
},
|
|
65
80
|
"engines": {
|
|
66
81
|
"node": ">=26.7.0"
|
|
67
82
|
},
|
|
68
83
|
"scripts": {
|
|
84
|
+
"build": "vp pack",
|
|
69
85
|
"check": "vp check --fix && astro check"
|
|
70
86
|
}
|
|
71
87
|
}
|
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">
|
|
@@ -18,7 +18,7 @@ if (!id) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const token = Astro.url.searchParams.get("token");
|
|
21
|
-
const secret = process.env.
|
|
21
|
+
const secret = process.env.CMS_PREVIEW_SECRET || "rimelight-preview-secret-key";
|
|
22
22
|
if (token) {
|
|
23
23
|
await verifyPreviewToken(id, token, secret);
|
|
24
24
|
}
|
|
@@ -6,10 +6,10 @@ import BlockRenderer from "./BlockRenderer.astro"
|
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
8
|
blocks: BaseBlock[]
|
|
9
|
-
session?: UserSessionContext
|
|
10
|
-
locale?: string
|
|
11
|
-
class?: string
|
|
12
|
-
components?: Record<string, any>
|
|
9
|
+
session?: UserSessionContext | undefined
|
|
10
|
+
locale?: string | undefined
|
|
11
|
+
class?: string | undefined
|
|
12
|
+
components?: Record<string, any> | undefined
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const {
|
package/src/env.d.ts
CHANGED
|
@@ -15,3 +15,9 @@ declare module "virtual:rimelight-cms/db" {
|
|
|
15
15
|
declare module "virtual:rimelight-cms/storage" {
|
|
16
16
|
export const storageConfig: any
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
declare module "virtual:rimelight-cms/auth" {
|
|
20
|
+
import type { AuthAdapter } from "@rimelight/auth"
|
|
21
|
+
export const authAdapter: AuthAdapter
|
|
22
|
+
export default authAdapter
|
|
23
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -23,11 +23,25 @@ export type {
|
|
|
23
23
|
export * from "./core/registry.ts"
|
|
24
24
|
export * from "./core/validator.ts"
|
|
25
25
|
|
|
26
|
-
// Auth &
|
|
26
|
+
// Universal Auth & Pluggable Adapters (RFC-0004 & RFC-0006)
|
|
27
27
|
export * from "./auth/permissions.ts"
|
|
28
28
|
export * from "./auth/filter-blocks.ts"
|
|
29
29
|
export * from "./auth/editor-guards.ts"
|
|
30
30
|
export * from "./auth/filter-templates.ts"
|
|
31
|
+
export {
|
|
32
|
+
cfAccessAuth,
|
|
33
|
+
auth0Auth,
|
|
34
|
+
mockAuth,
|
|
35
|
+
hasRole,
|
|
36
|
+
hasPermission,
|
|
37
|
+
evaluateAccess
|
|
38
|
+
} from "@rimelight/auth"
|
|
39
|
+
export type {
|
|
40
|
+
AuthAdapter as CmsAuthAdapter,
|
|
41
|
+
AuthAdapter,
|
|
42
|
+
UserSessionContext,
|
|
43
|
+
UserType
|
|
44
|
+
} from "@rimelight/auth"
|
|
31
45
|
|
|
32
46
|
// Edge Cache & Search
|
|
33
47
|
export * from "./cache/purge.ts"
|
|
@@ -36,9 +50,8 @@ export * from "./search/query.ts"
|
|
|
36
50
|
// Database Schemas & Migration
|
|
37
51
|
export * from "./schema/index.ts"
|
|
38
52
|
|
|
39
|
-
// Astro Components
|
|
40
|
-
export {
|
|
41
|
-
export { default as BlockRenderer } from "./astro/BlockRenderer.astro"
|
|
53
|
+
// Astro Integration & Components
|
|
54
|
+
export { rimelightCms, type RimelightCmsOptions } from "./integration.ts"
|
|
42
55
|
|
|
43
56
|
// Toast API
|
|
44
57
|
export * from "./client/toast.ts"
|