@xleddyl/nuxt-cms 0.1.60 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +137 -9
  3. package/dist/runtime/app/components/cms/BlocksField.vue +86 -10
  4. package/dist/runtime/app/components/cms/BrandMark.d.vue.ts +6 -0
  5. package/dist/runtime/app/components/cms/BrandMark.vue +19 -0
  6. package/dist/runtime/app/components/cms/BrandMark.vue.d.ts +6 -0
  7. package/dist/runtime/app/components/cms/Checkbox.d.vue.ts +11 -0
  8. package/dist/runtime/app/components/cms/Checkbox.vue +21 -0
  9. package/dist/runtime/app/components/cms/Checkbox.vue.d.ts +11 -0
  10. package/dist/runtime/app/components/cms/EmptyState.vue +4 -2
  11. package/dist/runtime/app/components/cms/EntryDrawer.vue +1 -0
  12. package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +1 -0
  13. package/dist/runtime/app/components/cms/EntryForm.vue +105 -30
  14. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +1 -0
  15. package/dist/runtime/app/components/cms/FieldInput.vue +20 -5
  16. package/dist/runtime/app/components/cms/FormField.d.vue.ts +5 -3
  17. package/dist/runtime/app/components/cms/FormField.vue +8 -2
  18. package/dist/runtime/app/components/cms/FormField.vue.d.ts +5 -3
  19. package/dist/runtime/app/components/cms/Icon.vue +1 -1
  20. package/dist/runtime/app/components/cms/MediaDetail.d.vue.ts +16 -0
  21. package/dist/runtime/app/components/cms/MediaDetail.vue +169 -0
  22. package/dist/runtime/app/components/cms/MediaDetail.vue.d.ts +16 -0
  23. package/dist/runtime/app/components/cms/MediaField.vue +9 -2
  24. package/dist/runtime/app/components/cms/MediaFolderPicker.d.vue.ts +2 -2
  25. package/dist/runtime/app/components/cms/MediaFolderPicker.vue +55 -54
  26. package/dist/runtime/app/components/cms/MediaFolderPicker.vue.d.ts +2 -2
  27. package/dist/runtime/app/components/cms/MediaGallery.d.vue.ts +8 -2
  28. package/dist/runtime/app/components/cms/MediaGallery.vue +927 -336
  29. package/dist/runtime/app/components/cms/MediaGallery.vue.d.ts +8 -2
  30. package/dist/runtime/app/components/cms/MediaUpload.d.vue.ts +1 -0
  31. package/dist/runtime/app/components/cms/MediaUpload.vue +76 -139
  32. package/dist/runtime/app/components/cms/MediaUpload.vue.d.ts +1 -0
  33. package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +5 -3
  34. package/dist/runtime/app/components/cms/PageHeader.vue +16 -6
  35. package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +5 -3
  36. package/dist/runtime/app/components/cms/PasswordInput.d.vue.ts +17 -0
  37. package/dist/runtime/app/components/cms/PasswordInput.vue +77 -0
  38. package/dist/runtime/app/components/cms/PasswordInput.vue.d.ts +17 -0
  39. package/dist/runtime/app/components/cms/SelectMenu.vue +3 -5
  40. package/dist/runtime/app/components/cms/SettingsAccount.d.vue.ts +3 -0
  41. package/dist/runtime/app/components/cms/SettingsAccount.vue +138 -0
  42. package/dist/runtime/app/components/cms/SettingsAccount.vue.d.ts +3 -0
  43. package/dist/runtime/app/components/cms/SettingsColumns.d.vue.ts +16 -0
  44. package/dist/runtime/app/components/cms/SettingsColumns.vue +102 -0
  45. package/dist/runtime/app/components/cms/SettingsColumns.vue.d.ts +16 -0
  46. package/dist/runtime/app/components/cms/SettingsEntry.d.vue.ts +6 -0
  47. package/dist/runtime/app/components/cms/SettingsEntry.vue +133 -0
  48. package/dist/runtime/app/components/cms/SettingsEntry.vue.d.ts +6 -0
  49. package/dist/runtime/app/components/cms/SettingsLayout.d.vue.ts +16 -0
  50. package/dist/runtime/app/components/cms/SettingsLayout.vue +177 -0
  51. package/dist/runtime/app/components/cms/SettingsLayout.vue.d.ts +16 -0
  52. package/dist/runtime/app/components/cms/SettingsModal.d.vue.ts +16 -0
  53. package/dist/runtime/app/components/cms/SettingsModal.vue +179 -0
  54. package/dist/runtime/app/components/cms/SettingsModal.vue.d.ts +16 -0
  55. package/dist/runtime/app/components/cms/SettingsUsers.d.vue.ts +3 -0
  56. package/dist/runtime/app/components/cms/SettingsUsers.vue +270 -0
  57. package/dist/runtime/app/components/cms/SettingsUsers.vue.d.ts +3 -0
  58. package/dist/runtime/app/components/cms/Spinner.vue +2 -2
  59. package/dist/runtime/app/components/cms/Table.d.vue.ts +7 -2
  60. package/dist/runtime/app/components/cms/Table.vue +38 -1
  61. package/dist/runtime/app/components/cms/Table.vue.d.ts +7 -2
  62. package/dist/runtime/app/components/cms/Toaster.vue +1 -1
  63. package/dist/runtime/app/components/cms/WelcomeModal.d.vue.ts +3 -0
  64. package/dist/runtime/app/components/cms/WelcomeModal.vue +69 -0
  65. package/dist/runtime/app/components/cms/WelcomeModal.vue.d.ts +3 -0
  66. package/dist/runtime/app/composables/cms-account.d.ts +11 -0
  67. package/dist/runtime/app/composables/cms-account.js +23 -0
  68. package/dist/runtime/app/composables/cms-media-library.d.ts +75 -0
  69. package/dist/runtime/app/composables/cms-media-library.js +151 -0
  70. package/dist/runtime/app/composables/cms-media-uploader.d.ts +22 -0
  71. package/dist/runtime/app/composables/cms-media-uploader.js +244 -0
  72. package/dist/runtime/app/composables/cms-settings.d.ts +13 -0
  73. package/dist/runtime/app/composables/cms-settings.js +59 -0
  74. package/dist/runtime/app/composables/cms-theme.d.ts +9 -0
  75. package/dist/runtime/app/composables/cms-theme.js +27 -0
  76. package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
  77. package/dist/runtime/app/layouts/cms-admin.vue +71 -17
  78. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
  79. package/dist/runtime/app/pages/admin-collection.vue +127 -101
  80. package/dist/runtime/app/pages/admin-entry.vue +1 -0
  81. package/dist/runtime/app/pages/admin-login.vue +18 -3
  82. package/dist/runtime/app/pages/admin-media.vue +19 -3
  83. package/dist/runtime/app/utils/ui.d.ts +2 -0
  84. package/dist/runtime/app/utils/ui.js +18 -0
  85. package/dist/runtime/assets/main.css +1 -1
  86. package/dist/runtime/server/api/account-password.put.d.ts +4 -0
  87. package/dist/runtime/server/api/account-password.put.js +38 -0
  88. package/dist/runtime/server/api/account-welcome.post.d.ts +4 -0
  89. package/dist/runtime/server/api/account-welcome.post.js +14 -0
  90. package/dist/runtime/server/api/account.put.d.ts +3 -0
  91. package/dist/runtime/server/api/account.put.js +25 -0
  92. package/dist/runtime/server/api/media-delete.post.d.ts +4 -0
  93. package/dist/runtime/server/api/media-delete.post.js +23 -0
  94. package/dist/runtime/server/api/media-folder.delete.js +6 -16
  95. package/dist/runtime/server/api/media-folder.patch.d.ts +5 -0
  96. package/dist/runtime/server/api/media-folder.patch.js +68 -0
  97. package/dist/runtime/server/api/media-folder.post.js +3 -15
  98. package/dist/runtime/server/api/media-move.post.d.ts +5 -0
  99. package/dist/runtime/server/api/media-move.post.js +28 -0
  100. package/dist/runtime/server/api/media-usage.post.d.ts +5 -0
  101. package/dist/runtime/server/api/media-usage.post.js +15 -0
  102. package/dist/runtime/server/api/settings.delete.d.ts +4 -0
  103. package/dist/runtime/server/api/settings.delete.js +12 -0
  104. package/dist/runtime/server/api/settings.get.d.ts +3 -0
  105. package/dist/runtime/server/api/settings.get.js +15 -0
  106. package/dist/runtime/server/api/settings.put.d.ts +3 -0
  107. package/dist/runtime/server/api/settings.put.js +14 -0
  108. package/dist/runtime/server/api/user.delete.d.ts +4 -0
  109. package/dist/runtime/server/api/user.delete.js +12 -0
  110. package/dist/runtime/server/api/user.put.d.ts +3 -0
  111. package/dist/runtime/server/api/user.put.js +27 -0
  112. package/dist/runtime/server/api/users.get.d.ts +3 -0
  113. package/dist/runtime/server/api/users.get.js +11 -0
  114. package/dist/runtime/server/api/users.post.d.ts +3 -0
  115. package/dist/runtime/server/api/users.post.js +32 -0
  116. package/dist/runtime/server/plugins/session-validate.d.ts +2 -0
  117. package/dist/runtime/server/plugins/session-validate.js +13 -0
  118. package/dist/runtime/server/routes/auth/login.post.js +18 -8
  119. package/dist/runtime/server/utils/media-folders.d.ts +86 -0
  120. package/dist/runtime/server/utils/media-folders.js +37 -0
  121. package/dist/runtime/server/utils/media-references.d.ts +7 -1
  122. package/dist/runtime/server/utils/media-references.js +40 -0
  123. package/dist/runtime/server/utils/media-usage.d.ts +2 -0
  124. package/dist/runtime/server/utils/media-usage.js +18 -0
  125. package/dist/runtime/server/utils/password.d.ts +3 -0
  126. package/dist/runtime/server/utils/password.js +31 -0
  127. package/dist/runtime/server/utils/require-admin.d.ts +6 -2
  128. package/dist/runtime/server/utils/require-admin.js +33 -28
  129. package/dist/runtime/server/utils/same-origin.d.ts +2 -0
  130. package/dist/runtime/server/utils/same-origin.js +24 -0
  131. package/dist/runtime/server/utils/settings.d.ts +16 -0
  132. package/dist/runtime/server/utils/settings.js +31 -0
  133. package/dist/runtime/server/utils/users.d.ts +15 -0
  134. package/dist/runtime/server/utils/users.js +34 -0
  135. package/dist/runtime/shared/index.d.ts +29 -1
  136. package/dist/runtime/shared/index.js +36 -2
  137. package/dist/runtime/shared/layout.d.ts +34 -0
  138. package/dist/runtime/shared/layout.js +130 -0
  139. package/dist/runtime/shared/users.d.ts +18 -0
  140. package/dist/runtime/shared/users.js +1 -0
  141. package/dist/runtime/shared/validation.js +5 -4
  142. package/package.json +3 -3
@@ -0,0 +1,15 @@
1
+ import { defineEventHandler } from "h3";
2
+ import { useDb } from "#cms-db";
3
+ import { cms_settings } from "#cms-tables";
4
+ import { requireAdmin } from "../utils/require-admin.js";
5
+ import { parseSettingsValue } from "../utils/settings.js";
6
+ export default defineEventHandler(async (event) => {
7
+ await requireAdmin(event);
8
+ const rows = await useDb().select().from(cms_settings);
9
+ return Object.fromEntries(
10
+ rows.flatMap((row) => {
11
+ const value = parseSettingsValue(row.value);
12
+ return value ? [[row.key, value]] : [];
13
+ })
14
+ );
15
+ });
@@ -0,0 +1,3 @@
1
+ import type { CmsEntrySettings } from '../../shared/index.js';
2
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<CmsEntrySettings>>;
3
+ export default _default;
@@ -0,0 +1,14 @@
1
+ import { defineEventHandler, readValidatedBody } from "h3";
2
+ import { useDb } from "#cms-db";
3
+ import { cms_settings } from "#cms-tables";
4
+ import { requireAdmin } from "../utils/require-admin.js";
5
+ import { entrySettingsSchema, requireSettingsKey } from "../utils/settings.js";
6
+ export default defineEventHandler(async (event) => {
7
+ await requireAdmin(event);
8
+ const key = requireSettingsKey(event);
9
+ const body = await readValidatedBody(event, entrySettingsSchema.parse);
10
+ const value = JSON.stringify(body);
11
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
12
+ await useDb().insert(cms_settings).values({ key, value, updatedAt }).onConflictDoUpdate({ target: cms_settings.key, set: { value, updatedAt } });
13
+ return body;
14
+ });
@@ -0,0 +1,4 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ ok: boolean;
3
+ }>>;
4
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { createError, defineEventHandler, getRouterParam } from "h3";
3
+ import { useDb } from "#cms-db";
4
+ import { cms_users } from "#cms-tables";
5
+ import { requireSuperAdmin } from "../utils/require-admin.js";
6
+ export default defineEventHandler(async (event) => {
7
+ await requireSuperAdmin(event);
8
+ const id = getRouterParam(event, "id") ?? "";
9
+ const [row] = await useDb().delete(cms_users).where(eq(cms_users.id, id)).returning();
10
+ if (!row) throw createError({ statusCode: 404, statusMessage: "User not found" });
11
+ return { ok: true };
12
+ });
@@ -0,0 +1,3 @@
1
+ import type { CmsAccount } from '../../shared/index.js';
2
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<CmsAccount>>;
3
+ export default _default;
@@ -0,0 +1,27 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { createError, defineEventHandler, getRouterParam, readValidatedBody } from "h3";
3
+ import { z } from "zod";
4
+ import { useDb } from "#cms-db";
5
+ import { cms_users } from "#cms-tables";
6
+ import { MIN_PASSWORD_LENGTH } from "../../shared/index.js";
7
+ import { hashPassword } from "../utils/password.js";
8
+ import { requireSuperAdmin } from "../utils/require-admin.js";
9
+ import { toAccount } from "../utils/users.js";
10
+ const bodySchema = z.object({
11
+ name: z.string().trim().max(120).nullish(),
12
+ password: z.string().min(MIN_PASSWORD_LENGTH).max(256).optional()
13
+ });
14
+ export default defineEventHandler(async (event) => {
15
+ await requireSuperAdmin(event);
16
+ const id = getRouterParam(event, "id") ?? "";
17
+ const body = await readValidatedBody(event, bodySchema.parse);
18
+ const set = { updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
19
+ if (body.name !== void 0) set.name = body.name || null;
20
+ if (body.password) {
21
+ set.passwordHash = await hashPassword(body.password);
22
+ set.mustChangePassword = true;
23
+ }
24
+ const [row] = await useDb().update(cms_users).set(set).where(eq(cms_users.id, id)).returning();
25
+ if (!row) throw createError({ statusCode: 404, statusMessage: "User not found" });
26
+ return toAccount(row);
27
+ });
@@ -0,0 +1,3 @@
1
+ import type { CmsAccount } from '../../shared/index.js';
2
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<CmsAccount[]>>;
3
+ export default _default;
@@ -0,0 +1,11 @@
1
+ import { asc } from "drizzle-orm";
2
+ import { defineEventHandler } from "h3";
3
+ import { useDb } from "#cms-db";
4
+ import { cms_users } from "#cms-tables";
5
+ import { requireSuperAdmin } from "../utils/require-admin.js";
6
+ import { toAccount } from "../utils/users.js";
7
+ export default defineEventHandler(async (event) => {
8
+ await requireSuperAdmin(event);
9
+ const rows = await useDb().select().from(cms_users).orderBy(asc(cms_users.createdAt));
10
+ return rows.map(toAccount);
11
+ });
@@ -0,0 +1,3 @@
1
+ import type { CmsAccount } from '../../shared/index.js';
2
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<CmsAccount>>;
3
+ export default _default;
@@ -0,0 +1,32 @@
1
+ import { createError, defineEventHandler, readValidatedBody } from "h3";
2
+ import { z } from "zod";
3
+ import { useDb } from "#cms-db";
4
+ import { cms_users } from "#cms-tables";
5
+ import { MIN_PASSWORD_LENGTH } from "../../shared/index.js";
6
+ import { customId } from "../utils/custom-id.js";
7
+ import { hashPassword } from "../utils/password.js";
8
+ import { isSuperAdminEmail, requireSuperAdmin } from "../utils/require-admin.js";
9
+ import { findUserByEmail, toAccount } from "../utils/users.js";
10
+ const bodySchema = z.object({
11
+ email: z.string().trim().toLowerCase().email().max(254),
12
+ name: z.string().trim().max(120).nullish(),
13
+ password: z.string().min(MIN_PASSWORD_LENGTH).max(256)
14
+ });
15
+ export default defineEventHandler(async (event) => {
16
+ await requireSuperAdmin(event);
17
+ const body = await readValidatedBody(event, bodySchema.parse);
18
+ if (isSuperAdminEmail(event, body.email) || await findUserByEmail(body.email)) {
19
+ throw createError({ statusCode: 409, statusMessage: "A user with this email already exists" });
20
+ }
21
+ const now = (/* @__PURE__ */ new Date()).toISOString();
22
+ const [row] = await useDb().insert(cms_users).values({
23
+ id: customId("usr"),
24
+ email: body.email,
25
+ name: body.name || null,
26
+ role: "admin",
27
+ passwordHash: await hashPassword(body.password),
28
+ createdAt: now,
29
+ updatedAt: now
30
+ }).returning();
31
+ return toAccount(row);
32
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: () => void;
2
+ export default _default;
@@ -0,0 +1,13 @@
1
+ import { createError } from "h3";
2
+ import { clearUserSession, sessionHooks } from "#imports";
3
+ import { findUserById, passwordStamp } from "../utils/users.js";
4
+ export default () => {
5
+ sessionHooks.hook("fetch", async (session, event) => {
6
+ const user = session.user;
7
+ if (!user?.id) return;
8
+ const row = await findUserById(user.id);
9
+ if (row && user.passwordStamp === passwordStamp(row.passwordHash)) return;
10
+ await clearUserSession(event);
11
+ throw createError({ statusCode: 401, statusMessage: "Session expired" });
12
+ });
13
+ };
@@ -1,8 +1,13 @@
1
1
  import { createHash, timingSafeEqual } from "node:crypto";
2
2
  import { createError, defineEventHandler, getRequestIP, readValidatedBody } from "h3";
3
3
  import { z } from "zod";
4
- import { setUserSession, useRuntimeConfig, useStorage } from "#imports";
5
- import { assertSameOrigin } from "../../utils/require-admin.js";
4
+ import { replaceUserSession, useRuntimeConfig, useStorage } from "#imports";
5
+ import { eq } from "drizzle-orm";
6
+ import { useDb } from "#cms-db";
7
+ import { cms_users } from "#cms-tables";
8
+ import { verifyNothing, verifyPassword } from "../../utils/password.js";
9
+ import { assertSameOrigin } from "../../utils/same-origin.js";
10
+ import { findUserByEmail, sessionUserFor } from "../../utils/users.js";
6
11
  const credentialsSchema = z.object({
7
12
  email: z.string().trim().min(1),
8
13
  password: z.string().min(1)
@@ -62,17 +67,22 @@ export default defineEventHandler(async (event) => {
62
67
  });
63
68
  }
64
69
  const body = await readValidatedBody(event, credentialsSchema.parse);
70
+ const email = body.email.toLowerCase();
65
71
  const { adminEmail, adminPassword } = useRuntimeConfig(event).cms;
66
- if (!adminEmail || !adminPassword) {
67
- throw createError({ statusCode: 403, statusMessage: "Admin credentials are not configured" });
72
+ const superAdmin = !!adminEmail && !!adminPassword && safeEqual(email, adminEmail.toLowerCase()) && safeEqual(body.password, adminPassword);
73
+ if (superAdmin) {
74
+ await rateStorage().removeItem(ip);
75
+ await replaceUserSession(event, { user: { email, role: "superadmin" } });
76
+ return { loggedIn: true };
68
77
  }
69
- const emailOk = safeEqual(body.email.toLowerCase(), adminEmail.toLowerCase());
70
- const passwordOk = safeEqual(body.password, adminPassword);
71
- if (!emailOk || !passwordOk) {
78
+ const row = await findUserByEmail(email);
79
+ const passwordOk = row ? await verifyPassword(row.passwordHash, body.password) : await verifyNothing(body.password);
80
+ if (!row || !passwordOk) {
72
81
  await Promise.all([recordFailure(ip, now), recordFailure(GLOBAL_KEY, now), prune(now)]);
73
82
  throw createError({ statusCode: 401, statusMessage: "Invalid credentials" });
74
83
  }
75
84
  await rateStorage().removeItem(ip);
76
- await setUserSession(event, { user: { email: adminEmail.toLowerCase() } });
85
+ await useDb().update(cms_users).set({ lastLoginAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(cms_users.id, row.id));
86
+ await replaceUserSession(event, { user: sessionUserFor(row) });
77
87
  return { loggedIn: true };
78
88
  });
@@ -0,0 +1,86 @@
1
+ import type { MediaStore } from './media.js';
2
+ export declare function writeMediaFolderMarker(store: MediaStore, folder: string): Promise<void>;
3
+ export declare function mediaRowsInFolder(folder: string): Omit<import("drizzle-orm/sqlite-core").SQLiteSelectBase<"cms_media", "sync", import("better-sqlite3").RunResult, {
4
+ key: import("drizzle-orm/sqlite-core").SQLiteColumn<{
5
+ name: "key";
6
+ tableName: "cms_media";
7
+ dataType: "string";
8
+ columnType: "SQLiteText";
9
+ data: string;
10
+ driverParam: string;
11
+ notNull: true;
12
+ hasDefault: false;
13
+ isPrimaryKey: false;
14
+ isAutoincrement: false;
15
+ hasRuntimeDefault: false;
16
+ enumValues: [string, ...string[]];
17
+ baseColumn: never;
18
+ identity: undefined;
19
+ generated: undefined;
20
+ }, {}, {
21
+ length: number | undefined;
22
+ }>;
23
+ folder: import("drizzle-orm/sqlite-core").SQLiteColumn<{
24
+ name: "folder";
25
+ tableName: "cms_media";
26
+ dataType: "string";
27
+ columnType: "SQLiteText";
28
+ data: string;
29
+ driverParam: string;
30
+ notNull: false;
31
+ hasDefault: false;
32
+ isPrimaryKey: false;
33
+ isAutoincrement: false;
34
+ hasRuntimeDefault: false;
35
+ enumValues: [string, ...string[]];
36
+ baseColumn: never;
37
+ identity: undefined;
38
+ generated: undefined;
39
+ }, {}, {
40
+ length: number | undefined;
41
+ }>;
42
+ }, "partial", Record<"cms_media", "not-null">, false, "where", {
43
+ key: string;
44
+ folder: string | null;
45
+ }[], {
46
+ key: import("drizzle-orm/sqlite-core").SQLiteColumn<{
47
+ name: "key";
48
+ tableName: "cms_media";
49
+ dataType: "string";
50
+ columnType: "SQLiteText";
51
+ data: string;
52
+ driverParam: string;
53
+ notNull: true;
54
+ hasDefault: false;
55
+ isPrimaryKey: false;
56
+ isAutoincrement: false;
57
+ hasRuntimeDefault: false;
58
+ enumValues: [string, ...string[]];
59
+ baseColumn: never;
60
+ identity: undefined;
61
+ generated: undefined;
62
+ }, {}, {
63
+ length: number | undefined;
64
+ }>;
65
+ folder: import("drizzle-orm/sqlite-core").SQLiteColumn<{
66
+ name: "folder";
67
+ tableName: "cms_media";
68
+ dataType: "string";
69
+ columnType: "SQLiteText";
70
+ data: string;
71
+ driverParam: string;
72
+ notNull: false;
73
+ hasDefault: false;
74
+ isPrimaryKey: false;
75
+ isAutoincrement: false;
76
+ hasRuntimeDefault: false;
77
+ enumValues: [string, ...string[]];
78
+ baseColumn: never;
79
+ identity: undefined;
80
+ generated: undefined;
81
+ }, {}, {
82
+ length: number | undefined;
83
+ }>;
84
+ }>, "where">;
85
+ export declare function deleteMediaRows(store: MediaStore, keys: string[]): Promise<void>;
86
+ export declare function keyChunks(keys: string[]): string[][];
@@ -0,0 +1,37 @@
1
+ import { inArray, like, or } from "drizzle-orm";
2
+ import { useDb } from "#cms-db";
3
+ import { cms_media } from "#cms-tables";
4
+ import { mediaFolderMarkerKey } from "../../shared/index.js";
5
+ const KEY_CHUNK = 90;
6
+ export async function writeMediaFolderMarker(store, folder) {
7
+ const key = mediaFolderMarkerKey(folder);
8
+ await store.write(key, new Uint8Array(), "application/x-empty");
9
+ const values = {
10
+ key,
11
+ mime: "application/x-empty",
12
+ size: 0,
13
+ width: null,
14
+ height: null,
15
+ alt: null,
16
+ folder
17
+ };
18
+ await useDb().insert(cms_media).values(values).onConflictDoUpdate({ target: cms_media.key, set: values });
19
+ }
20
+ export function mediaRowsInFolder(folder) {
21
+ return useDb().select({ key: cms_media.key, folder: cms_media.folder }).from(cms_media).where(or(like(cms_media.folder, `${folder}/%`), like(cms_media.folder, folder)));
22
+ }
23
+ export async function deleteMediaRows(store, keys) {
24
+ for (const key of keys) {
25
+ await store.remove(key);
26
+ }
27
+ for (const chunk of keyChunks(keys)) {
28
+ await useDb().delete(cms_media).where(inArray(cms_media.key, chunk));
29
+ }
30
+ }
31
+ export function keyChunks(keys) {
32
+ const chunks = [];
33
+ for (let index = 0; index < keys.length; index += KEY_CHUNK) {
34
+ chunks.push(keys.slice(index, index + KEY_CHUNK));
35
+ }
36
+ return chunks;
37
+ }
@@ -1,10 +1,16 @@
1
1
  import type { BaseSQLiteDatabase, SQLiteTable } from 'drizzle-orm/sqlite-core';
2
- import type { FieldConfig } from '../../shared/index.js';
2
+ import type { CmsEntry, FieldConfig, MediaUsage } from '../../shared/index.js';
3
3
  export interface MediaReference {
4
4
  field: string;
5
5
  key: string;
6
6
  }
7
7
  export type MediaLookup = (keys: string[]) => Promise<Set<string>>;
8
8
  export declare function mediaReferences(fields: Record<string, FieldConfig>, values: Record<string, unknown>): MediaReference[];
9
+ export interface MediaUsageSource {
10
+ name: string;
11
+ entry: CmsEntry;
12
+ rows: Record<string, unknown>[];
13
+ }
14
+ export declare function collectMediaUsage(sources: MediaUsageSource[], keys: string[]): Record<string, MediaUsage[]>;
9
15
  export declare function assertKnownMedia(references: MediaReference[], lookup: MediaLookup): Promise<void>;
10
16
  export declare function mediaKeysInTable(db: Pick<BaseSQLiteDatabase<'sync' | 'async', unknown>, 'select'>, table: SQLiteTable, keys: string[]): Promise<Set<string>>;
@@ -1,5 +1,6 @@
1
1
  import { inArray } from "drizzle-orm";
2
2
  import { createError } from "h3";
3
+ import { PAGE_PATH_FIELD, pageFields, pageRouteOf } from "../../shared/index.js";
3
4
  const LOOKUP_CHUNK = 90;
4
5
  function mediaKeysOf(value) {
5
6
  if (typeof value === "string") return value ? [value] : [];
@@ -29,6 +30,45 @@ export function mediaReferences(fields, values) {
29
30
  }
30
31
  return references;
31
32
  }
33
+ function titleOf(entry, row) {
34
+ if (entry.kind === "page") {
35
+ const path = row[PAGE_PATH_FIELD];
36
+ return pageRouteOf(entry, String(row.id))?.label ?? (typeof path === "string" ? path : null);
37
+ }
38
+ const value = entry.titleField ? row[entry.titleField] : void 0;
39
+ if (typeof value === "string") return value || null;
40
+ if (value && typeof value === "object") {
41
+ const first = Object.values(value).find(
42
+ (candidate) => typeof candidate === "string" && candidate !== ""
43
+ );
44
+ return first ?? null;
45
+ }
46
+ return null;
47
+ }
48
+ function fieldsOf(entry, row) {
49
+ const path = row[PAGE_PATH_FIELD];
50
+ return entry.kind === "page" && typeof path === "string" ? pageFields(entry, path) : entry.fields;
51
+ }
52
+ export function collectMediaUsage(sources, keys) {
53
+ const usage = Object.fromEntries(keys.map((key) => [key, []]));
54
+ for (const { name, entry, rows } of sources) {
55
+ for (const row of rows) {
56
+ for (const reference of mediaReferences(fieldsOf(entry, row), row)) {
57
+ const list = usage[reference.key];
58
+ if (!list) continue;
59
+ list.push({
60
+ collection: name,
61
+ label: entry.label,
62
+ kind: entry.kind,
63
+ id: entry.kind === "single" ? null : String(row.id),
64
+ title: titleOf(entry, row),
65
+ field: reference.field
66
+ });
67
+ }
68
+ }
69
+ }
70
+ return usage;
71
+ }
32
72
  export async function assertKnownMedia(references, lookup) {
33
73
  if (!references.length) return;
34
74
  const known = await lookup([...new Set(references.map((reference) => reference.key))]);
@@ -0,0 +1,2 @@
1
+ import type { MediaUsage } from '../../shared/index.js';
2
+ export declare function loadMediaUsage(keys: string[]): Promise<Record<string, MediaUsage[]>>;
@@ -0,0 +1,18 @@
1
+ import cmsConfig from "#cms-config";
2
+ import { useDb } from "#cms-db";
3
+ import { collectMediaUsage } from "./media-references.js";
4
+ import { readPages } from "./page-storage.js";
5
+ import { decodeRows, resolveTable } from "./registry.js";
6
+ async function usageSources() {
7
+ const sources = [];
8
+ for (const [name, entry] of Object.entries(cmsConfig)) {
9
+ const table = resolveTable(name);
10
+ if (!table) continue;
11
+ const rows = entry.kind === "page" ? await readPages(name, entry, table) : decodeRows(entry, await useDb().select().from(table));
12
+ sources.push({ name, entry, rows });
13
+ }
14
+ return sources;
15
+ }
16
+ export async function loadMediaUsage(keys) {
17
+ return collectMediaUsage(await usageSources(), keys);
18
+ }
@@ -0,0 +1,3 @@
1
+ export declare function hashPassword(password: string): Promise<string>;
2
+ export declare function verifyPassword(stored: string, password: string): Promise<boolean>;
3
+ export declare function verifyNothing(password: string): Promise<false>;
@@ -0,0 +1,31 @@
1
+ import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
2
+ const KEY_LENGTH = 64;
3
+ const COST = 16384;
4
+ function derive(password, salt, cost) {
5
+ return new Promise((resolve, reject) => {
6
+ scrypt(
7
+ password,
8
+ salt,
9
+ KEY_LENGTH,
10
+ { N: cost, r: 8, p: 1 },
11
+ (error, key) => error ? reject(error) : resolve(key)
12
+ );
13
+ });
14
+ }
15
+ export async function hashPassword(password) {
16
+ const salt = randomBytes(16);
17
+ const key = await derive(password, salt, COST);
18
+ return `scrypt$${COST}$${salt.toString("base64")}$${key.toString("base64")}`;
19
+ }
20
+ export async function verifyPassword(stored, password) {
21
+ const [scheme, cost, salt, key] = stored.split("$");
22
+ if (scheme !== "scrypt" || !cost || !salt || !key) return false;
23
+ const expected = Buffer.from(key, "base64");
24
+ const actual = await derive(password, Buffer.from(salt, "base64"), Number(cost));
25
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
26
+ }
27
+ const DUMMY_HASH = "scrypt$16384$AAAAAAAAAAAAAAAAAAAAAA==$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
28
+ export async function verifyNothing(password) {
29
+ await verifyPassword(DUMMY_HASH, password);
30
+ return false;
31
+ }
@@ -1,3 +1,7 @@
1
1
  import type { H3Event } from 'h3';
2
- export declare function assertSameOrigin(event: H3Event): void;
3
- export declare function requireAdmin(event: H3Event): Promise<void>;
2
+ import type { CmsSessionUser } from '../../shared/index.js';
3
+ import { assertSameOrigin } from './same-origin.js';
4
+ export { assertSameOrigin };
5
+ export declare function isSuperAdminEmail(event: H3Event, email: string): boolean;
6
+ export declare function requireAdmin(event: H3Event): Promise<CmsSessionUser>;
7
+ export declare function requireSuperAdmin(event: H3Event): Promise<CmsSessionUser>;
@@ -1,37 +1,42 @@
1
- import { createError, getRequestHeader, getRequestHost } from "h3";
1
+ import { createError } from "h3";
2
2
  import { getUserSession, useRuntimeConfig } from "#imports";
3
- const ORIGINLESS_SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
4
- function hostOfUrl(value) {
5
- if (!value) return void 0;
6
- try {
7
- const url = new URL(value);
8
- return url.protocol === "http:" || url.protocol === "https:" ? url.host : void 0;
9
- } catch {
10
- return void 0;
11
- }
12
- }
13
- function rejectCrossOrigin() {
14
- throw createError({ statusCode: 403, statusMessage: "Cross-origin request rejected" });
15
- }
16
- export function assertSameOrigin(event) {
17
- const expectedHost = getRequestHost(event);
18
- const origin = getRequestHeader(event, "origin");
19
- if (origin) {
20
- if (hostOfUrl(origin) !== expectedHost) rejectCrossOrigin();
21
- return;
22
- }
23
- if (ORIGINLESS_SAFE_METHODS.has(event.method)) return;
24
- if (hostOfUrl(getRequestHeader(event, "referer")) !== expectedHost) rejectCrossOrigin();
3
+ import { assertSameOrigin } from "./same-origin.js";
4
+ import { findUserById, passwordStamp } from "./users.js";
5
+ export { assertSameOrigin };
6
+ export function isSuperAdminEmail(event, email) {
7
+ const { adminEmail } = useRuntimeConfig(event).cms;
8
+ return !!adminEmail && email.toLowerCase() === adminEmail.toLowerCase();
25
9
  }
26
10
  export async function requireAdmin(event) {
27
11
  assertSameOrigin(event);
28
12
  const session = await getUserSession(event);
29
- const email = session.user?.email;
30
- if (!email) {
13
+ const user = session.user;
14
+ if (!user?.email) {
31
15
  throw createError({ statusCode: 401, statusMessage: "Authentication required" });
32
16
  }
33
- const { adminEmail } = useRuntimeConfig(event).cms;
34
- if (!adminEmail || email.toLowerCase() !== adminEmail.toLowerCase()) {
35
- throw createError({ statusCode: 403, statusMessage: "Not authorized" });
17
+ if (!user.id) {
18
+ if (!isSuperAdminEmail(event, user.email)) {
19
+ throw createError({ statusCode: 403, statusMessage: "Not authorized" });
20
+ }
21
+ return { email: user.email.toLowerCase(), name: null, role: "superadmin" };
22
+ }
23
+ const row = await findUserById(user.id);
24
+ if (!row || user.passwordStamp !== passwordStamp(row.passwordHash)) {
25
+ throw createError({ statusCode: 401, statusMessage: "Authentication required" });
26
+ }
27
+ return {
28
+ id: row.id,
29
+ email: row.email,
30
+ name: row.name ?? null,
31
+ role: "admin",
32
+ firstLogin: !!row.mustChangePassword,
33
+ passwordStamp: user.passwordStamp
34
+ };
35
+ }
36
+ export async function requireSuperAdmin(event) {
37
+ const user = await requireAdmin(event);
38
+ if (user.role !== "superadmin") {
39
+ throw createError({ statusCode: 403, statusMessage: "Only the super admin can manage users" });
36
40
  }
41
+ return user;
37
42
  }
@@ -0,0 +1,2 @@
1
+ import type { H3Event } from 'h3';
2
+ export declare function assertSameOrigin(event: H3Event): void;
@@ -0,0 +1,24 @@
1
+ import { createError, getRequestHeader, getRequestHost } from "h3";
2
+ const ORIGINLESS_SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
3
+ function hostOfUrl(value) {
4
+ if (!value) return void 0;
5
+ try {
6
+ const url = new URL(value);
7
+ return url.protocol === "http:" || url.protocol === "https:" ? url.host : void 0;
8
+ } catch {
9
+ return void 0;
10
+ }
11
+ }
12
+ function rejectCrossOrigin() {
13
+ throw createError({ statusCode: 403, statusMessage: "Cross-origin request rejected" });
14
+ }
15
+ export function assertSameOrigin(event) {
16
+ const expectedHost = getRequestHost(event);
17
+ const origin = getRequestHeader(event, "origin");
18
+ if (origin) {
19
+ if (hostOfUrl(origin) !== expectedHost) rejectCrossOrigin();
20
+ return;
21
+ }
22
+ if (ORIGINLESS_SAFE_METHODS.has(event.method)) return;
23
+ if (hostOfUrl(getRequestHeader(event, "referer")) !== expectedHost) rejectCrossOrigin();
24
+ }
@@ -0,0 +1,16 @@
1
+ import type { H3Event } from 'h3';
2
+ import { z } from 'zod';
3
+ import type { CmsEntrySettings } from '../../shared/index.js';
4
+ export declare const entrySettingsSchema: z.ZodObject<{
5
+ columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
6
+ layout: z.ZodOptional<z.ZodObject<{
7
+ main: z.ZodArray<z.ZodObject<{
8
+ title: z.ZodOptional<z.ZodString>;
9
+ description: z.ZodOptional<z.ZodString>;
10
+ collapsed: z.ZodOptional<z.ZodBoolean>;
11
+ rows: z.ZodArray<z.ZodArray<z.ZodString>>;
12
+ }, z.core.$strip>>;
13
+ }, z.core.$strip>>;
14
+ }, z.core.$strict>;
15
+ export declare function requireSettingsKey(event: H3Event): string;
16
+ export declare function parseSettingsValue(raw: string): CmsEntrySettings | null;
@@ -0,0 +1,31 @@
1
+ import { createError, getRouterParam } from "h3";
2
+ import { z } from "zod";
3
+ import cmsConfig from "#cms-config";
4
+ const fieldKey = z.string().min(1).max(128);
5
+ const sectionSchema = z.object({
6
+ title: z.string().max(200).optional(),
7
+ description: z.string().max(1e3).optional(),
8
+ collapsed: z.boolean().optional(),
9
+ rows: z.array(z.array(fieldKey).max(4)).max(500)
10
+ });
11
+ export const entrySettingsSchema = z.object({
12
+ columns: z.array(fieldKey).max(200).optional(),
13
+ layout: z.object({
14
+ main: z.array(sectionSchema).max(100)
15
+ }).optional()
16
+ }).strict();
17
+ export function requireSettingsKey(event) {
18
+ const key = getRouterParam(event, "key") ?? "";
19
+ if (!Object.hasOwn(cmsConfig, key)) {
20
+ throw createError({ statusCode: 404, statusMessage: `Unknown entry: ${key}` });
21
+ }
22
+ return key;
23
+ }
24
+ export function parseSettingsValue(raw) {
25
+ try {
26
+ const parsed = entrySettingsSchema.safeParse(JSON.parse(raw));
27
+ return parsed.success ? parsed.data : null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
@@ -0,0 +1,15 @@
1
+ import { cms_users } from '#cms-tables';
2
+ import type { CmsAccount } from '../../shared/index.js';
3
+ export type CmsUserRow = typeof cms_users.$inferSelect;
4
+ export declare function findUserByEmail(email: string): Promise<CmsUserRow | undefined>;
5
+ export declare function findUserById(id: string): Promise<CmsUserRow | undefined>;
6
+ export declare function passwordStamp(passwordHash: string): string;
7
+ export declare function sessionUserFor(row: CmsUserRow): {
8
+ id: string;
9
+ email: string;
10
+ name: string | null;
11
+ role: "admin";
12
+ firstLogin: boolean;
13
+ passwordStamp: string;
14
+ };
15
+ export declare function toAccount(row: CmsUserRow): CmsAccount;