@xleddyl/nuxt-cms 0.1.9

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 (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -0
  3. package/dist/module.d.mts +36 -0
  4. package/dist/module.json +9 -0
  5. package/dist/module.mjs +765 -0
  6. package/dist/runtime/app/components/cms/BlocksField.d.vue.ts +15 -0
  7. package/dist/runtime/app/components/cms/BlocksField.vue +110 -0
  8. package/dist/runtime/app/components/cms/BlocksField.vue.d.ts +15 -0
  9. package/dist/runtime/app/components/cms/EmptyState.d.vue.ts +18 -0
  10. package/dist/runtime/app/components/cms/EmptyState.vue +20 -0
  11. package/dist/runtime/app/components/cms/EmptyState.vue.d.ts +18 -0
  12. package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +23 -0
  13. package/dist/runtime/app/components/cms/EntryForm.vue +78 -0
  14. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +23 -0
  15. package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +16 -0
  16. package/dist/runtime/app/components/cms/FieldInput.vue +85 -0
  17. package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +16 -0
  18. package/dist/runtime/app/components/cms/JsonField.d.vue.ts +10 -0
  19. package/dist/runtime/app/components/cms/JsonField.vue +51 -0
  20. package/dist/runtime/app/components/cms/JsonField.vue.d.ts +10 -0
  21. package/dist/runtime/app/components/cms/MediaField.d.vue.ts +16 -0
  22. package/dist/runtime/app/components/cms/MediaField.vue +81 -0
  23. package/dist/runtime/app/components/cms/MediaField.vue.d.ts +16 -0
  24. package/dist/runtime/app/components/cms/MediaGallery.d.vue.ts +19 -0
  25. package/dist/runtime/app/components/cms/MediaGallery.vue +309 -0
  26. package/dist/runtime/app/components/cms/MediaGallery.vue.d.ts +19 -0
  27. package/dist/runtime/app/components/cms/MediaUpload.d.vue.ts +14 -0
  28. package/dist/runtime/app/components/cms/MediaUpload.vue +132 -0
  29. package/dist/runtime/app/components/cms/MediaUpload.vue.d.ts +14 -0
  30. package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +19 -0
  31. package/dist/runtime/app/components/cms/PageHeader.vue +23 -0
  32. package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +19 -0
  33. package/dist/runtime/app/components/cms/RelationField.d.vue.ts +16 -0
  34. package/dist/runtime/app/components/cms/RelationField.vue +87 -0
  35. package/dist/runtime/app/components/cms/RelationField.vue.d.ts +16 -0
  36. package/dist/runtime/app/components/cms/RichTextField.d.vue.ts +10 -0
  37. package/dist/runtime/app/components/cms/RichTextField.vue +123 -0
  38. package/dist/runtime/app/components/cms/RichTextField.vue.d.ts +10 -0
  39. package/dist/runtime/app/components/cms/SlugField.d.vue.ts +14 -0
  40. package/dist/runtime/app/components/cms/SlugField.vue +44 -0
  41. package/dist/runtime/app/components/cms/SlugField.vue.d.ts +14 -0
  42. package/dist/runtime/app/components/cms/TranslatableField.d.vue.ts +15 -0
  43. package/dist/runtime/app/components/cms/TranslatableField.vue +64 -0
  44. package/dist/runtime/app/components/cms/TranslatableField.vue.d.ts +15 -0
  45. package/dist/runtime/app/composables/cms-query.d.ts +4 -0
  46. package/dist/runtime/app/composables/cms-query.js +18 -0
  47. package/dist/runtime/app/composables/cms-runtime.d.ts +5 -0
  48. package/dist/runtime/app/composables/cms-runtime.js +4 -0
  49. package/dist/runtime/app/i18n/en.json +93 -0
  50. package/dist/runtime/app/i18n/it.json +93 -0
  51. package/dist/runtime/app/layouts/cms-admin.d.vue.ts +13 -0
  52. package/dist/runtime/app/layouts/cms-admin.vue +97 -0
  53. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +13 -0
  54. package/dist/runtime/app/middleware/cms-auth.d.ts +2 -0
  55. package/dist/runtime/app/middleware/cms-auth.js +7 -0
  56. package/dist/runtime/app/pages/admin-collection.d.vue.ts +3 -0
  57. package/dist/runtime/app/pages/admin-collection.vue +273 -0
  58. package/dist/runtime/app/pages/admin-collection.vue.d.ts +3 -0
  59. package/dist/runtime/app/pages/admin-entry.d.vue.ts +3 -0
  60. package/dist/runtime/app/pages/admin-entry.vue +156 -0
  61. package/dist/runtime/app/pages/admin-entry.vue.d.ts +3 -0
  62. package/dist/runtime/app/pages/admin-index.d.vue.ts +3 -0
  63. package/dist/runtime/app/pages/admin-index.vue +11 -0
  64. package/dist/runtime/app/pages/admin-index.vue.d.ts +3 -0
  65. package/dist/runtime/app/pages/admin-login.d.vue.ts +3 -0
  66. package/dist/runtime/app/pages/admin-login.vue +87 -0
  67. package/dist/runtime/app/pages/admin-login.vue.d.ts +3 -0
  68. package/dist/runtime/app/pages/admin-media.d.vue.ts +3 -0
  69. package/dist/runtime/app/pages/admin-media.vue +13 -0
  70. package/dist/runtime/app/pages/admin-media.vue.d.ts +3 -0
  71. package/dist/runtime/app/utils/api.d.ts +7 -0
  72. package/dist/runtime/app/utils/api.js +3 -0
  73. package/dist/runtime/app/utils/ui.d.ts +8 -0
  74. package/dist/runtime/app/utils/ui.js +9 -0
  75. package/dist/runtime/assets/main.css +1 -0
  76. package/dist/runtime/server/api/collection.get.d.ts +6 -0
  77. package/dist/runtime/server/api/collection.get.js +19 -0
  78. package/dist/runtime/server/api/collection.post.d.ts +2 -0
  79. package/dist/runtime/server/api/collection.post.js +30 -0
  80. package/dist/runtime/server/api/collection.put.d.ts +2 -0
  81. package/dist/runtime/server/api/collection.put.js +28 -0
  82. package/dist/runtime/server/api/item.delete.d.ts +4 -0
  83. package/dist/runtime/server/api/item.delete.js +19 -0
  84. package/dist/runtime/server/api/item.get.d.ts +2 -0
  85. package/dist/runtime/server/api/item.get.js +18 -0
  86. package/dist/runtime/server/api/item.put.d.ts +2 -0
  87. package/dist/runtime/server/api/item.put.js +40 -0
  88. package/dist/runtime/server/api/media-presign.post.d.ts +11 -0
  89. package/dist/runtime/server/api/media-presign.post.js +51 -0
  90. package/dist/runtime/server/api/media.delete.d.ts +4 -0
  91. package/dist/runtime/server/api/media.delete.js +25 -0
  92. package/dist/runtime/server/api/media.get.d.ts +5 -0
  93. package/dist/runtime/server/api/media.get.js +13 -0
  94. package/dist/runtime/server/api/media.post.d.ts +3 -0
  95. package/dist/runtime/server/api/media.post.js +32 -0
  96. package/dist/runtime/server/api/media.put.d.ts +3 -0
  97. package/dist/runtime/server/api/media.put.js +28 -0
  98. package/dist/runtime/server/plugins/migrate-postgres.d.ts +2 -0
  99. package/dist/runtime/server/plugins/migrate-postgres.js +9 -0
  100. package/dist/runtime/server/plugins/migrate-sqlite.d.ts +2 -0
  101. package/dist/runtime/server/plugins/migrate-sqlite.js +9 -0
  102. package/dist/runtime/server/plugins/session-check.d.ts +2 -0
  103. package/dist/runtime/server/plugins/session-check.js +9 -0
  104. package/dist/runtime/server/routes/auth/login.post.d.ts +4 -0
  105. package/dist/runtime/server/routes/auth/login.post.js +62 -0
  106. package/dist/runtime/server/routes/graphql.d.ts +2 -0
  107. package/dist/runtime/server/routes/graphql.js +34 -0
  108. package/dist/runtime/server/tsconfig.json +3 -0
  109. package/dist/runtime/server/utils/custom-id.d.ts +2 -0
  110. package/dist/runtime/server/utils/custom-id.js +10 -0
  111. package/dist/runtime/server/utils/db-errors.d.ts +1 -0
  112. package/dist/runtime/server/utils/db-errors.js +31 -0
  113. package/dist/runtime/server/utils/db-postgres.d.ts +3 -0
  114. package/dist/runtime/server/utils/db-postgres.js +16 -0
  115. package/dist/runtime/server/utils/db-sqlite.d.ts +4 -0
  116. package/dist/runtime/server/utils/db-sqlite.js +17 -0
  117. package/dist/runtime/server/utils/graphql-depth.d.ts +4 -0
  118. package/dist/runtime/server/utils/graphql-depth.js +72 -0
  119. package/dist/runtime/server/utils/graphql.d.ts +14 -0
  120. package/dist/runtime/server/utils/graphql.js +317 -0
  121. package/dist/runtime/server/utils/media.d.ts +24 -0
  122. package/dist/runtime/server/utils/media.js +38 -0
  123. package/dist/runtime/server/utils/registry.d.ts +20 -0
  124. package/dist/runtime/server/utils/registry.js +47 -0
  125. package/dist/runtime/server/utils/relations.d.ts +12 -0
  126. package/dist/runtime/server/utils/relations.js +80 -0
  127. package/dist/runtime/server/utils/require-admin.d.ts +2 -0
  128. package/dist/runtime/server/utils/require-admin.js +13 -0
  129. package/dist/runtime/shared/empty-config.d.ts +2 -0
  130. package/dist/runtime/shared/empty-config.js +1 -0
  131. package/dist/runtime/shared/graphql-sdl.d.ts +5 -0
  132. package/dist/runtime/shared/graphql-sdl.js +147 -0
  133. package/dist/runtime/shared/index.d.ts +62 -0
  134. package/dist/runtime/shared/index.js +55 -0
  135. package/dist/runtime/shared/validation.d.ts +16 -0
  136. package/dist/runtime/shared/validation.js +82 -0
  137. package/dist/types.d.mts +3 -0
  138. package/package.json +93 -0
@@ -0,0 +1,19 @@
1
+ import { desc } from "drizzle-orm";
2
+ import { defineEventHandler } from "h3";
3
+ import { useDb } from "#cms-db";
4
+ import { getRegistryEntry, idColumn, tableColumns } from "../utils/registry.js";
5
+ import { attachManyToMany } from "../utils/relations.js";
6
+ import { requireAdmin } from "../utils/require-admin.js";
7
+ export default defineEventHandler(async (event) => {
8
+ await requireAdmin(event);
9
+ const { name, entry, table } = getRegistryEntry(event);
10
+ const db = useDb();
11
+ if (entry.kind === "single") {
12
+ const rows2 = await db.select().from(table).limit(1);
13
+ await attachManyToMany(name, entry, rows2);
14
+ return rows2[0] ?? null;
15
+ }
16
+ const orderBy = tableColumns(table).createdAt ?? idColumn(table);
17
+ const rows = await db.select().from(table).orderBy(desc(orderBy));
18
+ return attachManyToMany(name, entry, rows);
19
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Record<string, unknown> | undefined>>;
2
+ export default _default;
@@ -0,0 +1,30 @@
1
+ import { createError, defineEventHandler, readValidatedBody } from "h3";
2
+ import { useDb } from "#cms-db";
3
+ import { customId } from "../utils/custom-id.js";
4
+ import { mapConstraintErrors } from "../utils/db-errors.js";
5
+ import { buildValidator, getRegistryEntry } from "../utils/registry.js";
6
+ import {
7
+ assertRelationTargets,
8
+ attachManyToMany,
9
+ saveManyToMany,
10
+ splitRelationValues
11
+ } from "../utils/relations.js";
12
+ import { requireAdmin } from "../utils/require-admin.js";
13
+ export default defineEventHandler(async (event) => {
14
+ await requireAdmin(event);
15
+ const { name, entry, table } = getRegistryEntry(event);
16
+ if (entry.kind !== "collection") {
17
+ throw createError({ statusCode: 405, statusMessage: "Single objects are updated with PUT" });
18
+ }
19
+ const body = await readValidatedBody(event, buildValidator(entry).parse);
20
+ const { values, lists } = splitRelationValues(entry, body);
21
+ await assertRelationTargets(entry, lists);
22
+ if (entry.drafts) values.status ??= "draft";
23
+ values.id = customId(entry.id);
24
+ return mapConstraintErrors(async () => {
25
+ const [row] = await useDb().insert(table).values(values).returning();
26
+ await saveManyToMany(name, row.id, lists);
27
+ const [attached] = await attachManyToMany(name, entry, [row]);
28
+ return attached;
29
+ });
30
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Record<string, unknown> | undefined>>;
2
+ export default _default;
@@ -0,0 +1,28 @@
1
+ import { createError, defineEventHandler, readValidatedBody } from "h3";
2
+ import { useDb } from "#cms-db";
3
+ import { mapConstraintErrors } from "../utils/db-errors.js";
4
+ import { buildValidator, getRegistryEntry, idColumn, withUpdatedAt } from "../utils/registry.js";
5
+ import {
6
+ assertRelationTargets,
7
+ attachManyToMany,
8
+ saveManyToMany,
9
+ splitRelationValues
10
+ } from "../utils/relations.js";
11
+ import { requireAdmin } from "../utils/require-admin.js";
12
+ export default defineEventHandler(async (event) => {
13
+ await requireAdmin(event);
14
+ const { name, entry, table } = getRegistryEntry(event);
15
+ if (entry.kind !== "single") {
16
+ throw createError({ statusCode: 405, statusMessage: "Collections are updated with PUT /:id" });
17
+ }
18
+ const body = await readValidatedBody(event, buildValidator(entry).parse);
19
+ const { values, lists } = splitRelationValues(entry, body);
20
+ await assertRelationTargets(entry, lists);
21
+ const set = withUpdatedAt(table, values);
22
+ return mapConstraintErrors(async () => {
23
+ const [row] = await useDb().insert(table).values({ id: entry.id, ...set }).onConflictDoUpdate({ target: idColumn(table), set }).returning();
24
+ await saveManyToMany(name, entry.id, lists);
25
+ const [attached] = await attachManyToMany(name, entry, [row]);
26
+ return attached;
27
+ });
28
+ });
@@ -0,0 +1,4 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ deleted: boolean;
3
+ }>>;
4
+ export default _default;
@@ -0,0 +1,19 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { createError, defineEventHandler } from "h3";
3
+ import { useDb } from "#cms-db";
4
+ import { mapConstraintErrors } from "../utils/db-errors.js";
5
+ import { getRegistryEntry, idColumn, parseId } from "../utils/registry.js";
6
+ import { requireAdmin } from "../utils/require-admin.js";
7
+ export default defineEventHandler(async (event) => {
8
+ await requireAdmin(event);
9
+ const { entry, table } = getRegistryEntry(event);
10
+ if (entry.kind !== "collection") {
11
+ throw createError({ statusCode: 405, statusMessage: "Single objects cannot be deleted" });
12
+ }
13
+ const id = parseId(event);
14
+ return mapConstraintErrors(async () => {
15
+ const [row] = await useDb().delete(table).where(eq(idColumn(table), id)).returning();
16
+ if (!row) throw createError({ statusCode: 404, statusMessage: "Row not found" });
17
+ return { deleted: true };
18
+ });
19
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Record<string, unknown> | undefined>>;
2
+ export default _default;
@@ -0,0 +1,18 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { createError, defineEventHandler } from "h3";
3
+ import { useDb } from "#cms-db";
4
+ import { getRegistryEntry, idColumn, parseId } from "../utils/registry.js";
5
+ import { attachManyToMany } from "../utils/relations.js";
6
+ import { requireAdmin } from "../utils/require-admin.js";
7
+ export default defineEventHandler(async (event) => {
8
+ await requireAdmin(event);
9
+ const { name, entry, table } = getRegistryEntry(event);
10
+ if (entry.kind !== "collection") {
11
+ throw createError({ statusCode: 404, statusMessage: "Single objects have no items" });
12
+ }
13
+ const id = parseId(event);
14
+ const rows = await useDb().select().from(table).where(eq(idColumn(table), id)).limit(1);
15
+ if (!rows[0]) throw createError({ statusCode: 404, statusMessage: "Row not found" });
16
+ const [attached] = await attachManyToMany(name, entry, [rows[0]]);
17
+ return attached;
18
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Record<string, unknown> | undefined>>;
2
+ export default _default;
@@ -0,0 +1,40 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { createError, defineEventHandler, readValidatedBody } from "h3";
3
+ import { useDb } from "#cms-db";
4
+ import {
5
+ buildValidator,
6
+ getRegistryEntry,
7
+ idColumn,
8
+ parseId,
9
+ withUpdatedAt
10
+ } from "../utils/registry.js";
11
+ import {
12
+ assertRelationTargets,
13
+ attachManyToMany,
14
+ saveManyToMany,
15
+ splitRelationValues
16
+ } from "../utils/relations.js";
17
+ import { requireAdmin } from "../utils/require-admin.js";
18
+ import { mapConstraintErrors } from "../utils/db-errors.js";
19
+ export default defineEventHandler(async (event) => {
20
+ await requireAdmin(event);
21
+ const { name, entry, table } = getRegistryEntry(event);
22
+ if (entry.kind !== "collection") {
23
+ throw createError({
24
+ statusCode: 405,
25
+ statusMessage: "Single objects are updated with PUT without id"
26
+ });
27
+ }
28
+ const id = parseId(event);
29
+ const body = await readValidatedBody(event, buildValidator(entry).parse);
30
+ const { values, lists } = splitRelationValues(entry, body);
31
+ await assertRelationTargets(entry, lists);
32
+ const set = withUpdatedAt(table, values);
33
+ return mapConstraintErrors(async () => {
34
+ const [row] = await useDb().update(table).set(set).where(eq(idColumn(table), id)).returning();
35
+ if (!row) throw createError({ statusCode: 404, statusMessage: "Row not found" });
36
+ await saveManyToMany(name, id, lists);
37
+ const [attached] = await attachManyToMany(name, entry, [row]);
38
+ return attached;
39
+ });
40
+ });
@@ -0,0 +1,11 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ key: string;
3
+ uploadUrl: string;
4
+ method: string;
5
+ headers: {
6
+ 'content-type': string;
7
+ };
8
+ publicUrl: string | null;
9
+ expiresIn: number;
10
+ }>>;
11
+ export default _default;
@@ -0,0 +1,51 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createError, defineEventHandler, readValidatedBody } from "h3";
3
+ import { z } from "zod";
4
+ import { slugify } from "../../shared/index.js";
5
+ import { useMediaStorage } from "../utils/media.js";
6
+ import { requireAdmin } from "../utils/require-admin.js";
7
+ const MAX_BASE_LENGTH = 80;
8
+ const MAX_EXT_LENGTH = 10;
9
+ const MAX_FILE_SIZE = 10 * 1024 * 1024;
10
+ const bodySchema = z.object({
11
+ filename: z.string().trim().min(1).max(255),
12
+ contentType: z.string().regex(/^[-\w.+]+\/[-\w.+]+$/, "Invalid content type"),
13
+ size: z.number().int().positive()
14
+ });
15
+ function slugifyFilename(filename) {
16
+ const dot = filename.lastIndexOf(".");
17
+ const base = slugify(dot > 0 ? filename.slice(0, dot) : filename).slice(0, MAX_BASE_LENGTH) || "file";
18
+ const ext = dot > 0 ? filename.slice(dot + 1).toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, MAX_EXT_LENGTH) : "";
19
+ return ext ? `${base}.${ext}` : base;
20
+ }
21
+ export default defineEventHandler(async (event) => {
22
+ await requireAdmin(event);
23
+ const { media, client, bucketUrl, publicUrl } = useMediaStorage(event);
24
+ const { filename, contentType, size } = await readValidatedBody(event, bodySchema.parse);
25
+ if (size > MAX_FILE_SIZE) {
26
+ throw createError({
27
+ statusCode: 413,
28
+ statusMessage: "File exceeds the maximum size of 10 MB"
29
+ });
30
+ }
31
+ const now = /* @__PURE__ */ new Date();
32
+ const prefix = `${now.getUTCFullYear()}/${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
33
+ const key = `${prefix}/${randomUUID()}-${slugifyFilename(filename)}`;
34
+ const url = new URL(`${bucketUrl}/${key}`);
35
+ url.searchParams.set("X-Amz-Expires", String(media.presignExpiry));
36
+ const signed = await client.sign(
37
+ new Request(url, {
38
+ method: "PUT",
39
+ headers: { "content-type": contentType, "content-length": String(size) }
40
+ }),
41
+ { aws: { signQuery: true, allHeaders: true } }
42
+ );
43
+ return {
44
+ key,
45
+ uploadUrl: signed.url,
46
+ method: "PUT",
47
+ headers: { "content-type": contentType },
48
+ publicUrl: publicUrl(key),
49
+ expiresIn: media.presignExpiry
50
+ };
51
+ });
@@ -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,25 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { createError, defineEventHandler, getValidatedQuery } from "h3";
3
+ import { z } from "zod";
4
+ import { useDb } from "#cms-db";
5
+ import { cms_media } from "#cms-tables";
6
+ import { objectKeySchema } from "../../shared/validation.js";
7
+ import { encodeKey, useMediaStorage } from "../utils/media.js";
8
+ import { requireAdmin } from "../utils/require-admin.js";
9
+ const querySchema = z.object({ key: objectKeySchema });
10
+ export default defineEventHandler(async (event) => {
11
+ await requireAdmin(event);
12
+ const { client, bucketUrl } = useMediaStorage(event);
13
+ const { key } = await getValidatedQuery(event, querySchema.parse);
14
+ const db = useDb();
15
+ const [row] = await db.select().from(cms_media).where(eq(cms_media.key, key)).limit(1);
16
+ if (!row) {
17
+ throw createError({ statusCode: 404, statusMessage: "Media not found" });
18
+ }
19
+ const res = await client.fetch(`${bucketUrl}/${encodeKey(key)}`, { method: "DELETE" });
20
+ if (!res.ok && res.status !== 404) {
21
+ throw createError({ statusCode: 502, statusMessage: `Bucket delete failed (${res.status})` });
22
+ }
23
+ await db.delete(cms_media).where(eq(cms_media.key, key));
24
+ return { ok: true };
25
+ });
@@ -0,0 +1,5 @@
1
+ import type { MediaItem } from '../../shared/index.js';
2
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
3
+ items: MediaItem[];
4
+ }>>;
5
+ export default _default;
@@ -0,0 +1,13 @@
1
+ import { desc } from "drizzle-orm";
2
+ import { defineEventHandler } from "h3";
3
+ import { useDb } from "#cms-db";
4
+ import { cms_media } from "#cms-tables";
5
+ import { toMediaItem, useMediaConfig } from "../utils/media.js";
6
+ import { requireAdmin } from "../utils/require-admin.js";
7
+ export default defineEventHandler(async (event) => {
8
+ await requireAdmin(event);
9
+ const { publicUrl } = useMediaConfig(event);
10
+ const rows = await useDb().select().from(cms_media).orderBy(desc(cms_media.createdAt), desc(cms_media.id));
11
+ const items = rows.map((row) => toMediaItem(row, publicUrl));
12
+ return { items };
13
+ });
@@ -0,0 +1,3 @@
1
+ import type { MediaItem } from '../../shared/index.js';
2
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<MediaItem>>;
3
+ export default _default;
@@ -0,0 +1,32 @@
1
+ import { defineEventHandler, readValidatedBody } from "h3";
2
+ import { z } from "zod";
3
+ import { useDb } from "#cms-db";
4
+ import { cms_media } from "#cms-tables";
5
+ import { objectKeySchema } from "../../shared/validation.js";
6
+ import { toMediaItem, useMediaConfig } from "../utils/media.js";
7
+ import { requireAdmin } from "../utils/require-admin.js";
8
+ const bodySchema = z.object({
9
+ key: objectKeySchema,
10
+ mime: z.string().max(255).nullish(),
11
+ size: z.number().int().nonnegative().nullish(),
12
+ width: z.number().int().positive().nullish(),
13
+ height: z.number().int().positive().nullish(),
14
+ alt: z.string().max(1024).nullish(),
15
+ folder: z.string().max(255).nullish()
16
+ });
17
+ export default defineEventHandler(async (event) => {
18
+ await requireAdmin(event);
19
+ const { publicUrl } = useMediaConfig(event);
20
+ const body = await readValidatedBody(event, bodySchema.parse);
21
+ const values = {
22
+ key: body.key,
23
+ mime: body.mime ?? null,
24
+ size: body.size ?? null,
25
+ width: body.width ?? null,
26
+ height: body.height ?? null,
27
+ alt: body.alt ?? null,
28
+ folder: body.folder ?? null
29
+ };
30
+ const [row] = await useDb().insert(cms_media).values(values).onConflictDoUpdate({ target: cms_media.key, set: values }).returning();
31
+ return toMediaItem(row, publicUrl);
32
+ });
@@ -0,0 +1,3 @@
1
+ import type { MediaItem } from '../../shared/index.js';
2
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<MediaItem>>;
3
+ export default _default;
@@ -0,0 +1,28 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { createError, defineEventHandler, readValidatedBody } from "h3";
3
+ import { z } from "zod";
4
+ import { useDb } from "#cms-db";
5
+ import { cms_media } from "#cms-tables";
6
+ import { toMediaItem, useMediaConfig } from "../utils/media.js";
7
+ import { requireAdmin } from "../utils/require-admin.js";
8
+ const bodySchema = z.object({
9
+ id: z.number().int().positive(),
10
+ alt: z.string().max(1024).nullish(),
11
+ folder: z.string().max(255).nullish()
12
+ });
13
+ export default defineEventHandler(async (event) => {
14
+ await requireAdmin(event);
15
+ const { publicUrl } = useMediaConfig(event);
16
+ const body = await readValidatedBody(event, bodySchema.parse);
17
+ const updates = {};
18
+ if (body.alt !== void 0) updates.alt = body.alt;
19
+ if (body.folder !== void 0) updates.folder = body.folder;
20
+ if (Object.keys(updates).length === 0) {
21
+ throw createError({ statusCode: 400, statusMessage: "No fields to update" });
22
+ }
23
+ const [row] = await useDb().update(cms_media).set(updates).where(eq(cms_media.id, body.id)).returning();
24
+ if (!row) {
25
+ throw createError({ statusCode: 404, statusMessage: "Media not found" });
26
+ }
27
+ return toMediaItem(row, publicUrl);
28
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,9 @@
1
+ import { existsSync } from "node:fs";
2
+ import { migrate } from "drizzle-orm/node-postgres/migrator";
3
+ import { useRuntimeConfig } from "#imports";
4
+ import { useDb } from "../utils/db-postgres.js";
5
+ export default async () => {
6
+ const { migrationsDir } = useRuntimeConfig().cms;
7
+ if (!existsSync(migrationsDir)) return;
8
+ await migrate(useDb(), { migrationsFolder: migrationsDir });
9
+ };
@@ -0,0 +1,2 @@
1
+ declare const _default: () => void;
2
+ export default _default;
@@ -0,0 +1,9 @@
1
+ import { existsSync } from "node:fs";
2
+ import { migrate } from "drizzle-orm/better-sqlite3/migrator";
3
+ import { useRuntimeConfig } from "#imports";
4
+ import { useDb } from "../utils/db-sqlite.js";
5
+ export default () => {
6
+ const { migrationsDir } = useRuntimeConfig().cms;
7
+ if (!existsSync(migrationsDir)) return;
8
+ migrate(useDb(), { migrationsFolder: migrationsDir });
9
+ };
@@ -0,0 +1,2 @@
1
+ declare const _default: () => void;
2
+ export default _default;
@@ -0,0 +1,9 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ export default () => {
3
+ const { session } = useRuntimeConfig();
4
+ if (!session?.password || session.password.length < 32) {
5
+ throw new Error(
6
+ "[nuxt-cms] Admin sessions require NUXT_SESSION_PASSWORD (min 32 characters) in production"
7
+ );
8
+ }
9
+ };
@@ -0,0 +1,4 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ loggedIn: boolean;
3
+ }>>;
4
+ export default _default;
@@ -0,0 +1,62 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import { createError, defineEventHandler, getRequestIP, readValidatedBody } from "h3";
3
+ import { z } from "zod";
4
+ import { setUserSession, useRuntimeConfig } from "#imports";
5
+ const credentialsSchema = z.object({
6
+ email: z.string().trim().min(1),
7
+ password: z.string().min(1)
8
+ });
9
+ const RATE_WINDOW_MS = 15 * 6e4;
10
+ const RATE_MAX_FAILURES = 10;
11
+ const RATE_GLOBAL_MAX_FAILURES = 100;
12
+ const RATE_PRUNE_THRESHOLD = 1e3;
13
+ const failures = /* @__PURE__ */ new Map();
14
+ let globalFailures = { count: 0, resetAt: 0 };
15
+ function pruneFailures(now) {
16
+ if (failures.size < RATE_PRUNE_THRESHOLD) return;
17
+ for (const [ip, entry] of failures) {
18
+ if (entry.resetAt <= now) failures.delete(ip);
19
+ }
20
+ }
21
+ function safeEqual(a, b) {
22
+ const hashA = createHash("sha256").update(a).digest();
23
+ const hashB = createHash("sha256").update(b).digest();
24
+ return timingSafeEqual(hashA, hashB);
25
+ }
26
+ export default defineEventHandler(async (event) => {
27
+ const ip = getRequestIP(event) ?? "unknown";
28
+ const now = Date.now();
29
+ pruneFailures(now);
30
+ if (globalFailures.resetAt > now && globalFailures.count >= RATE_GLOBAL_MAX_FAILURES) {
31
+ throw createError({
32
+ statusCode: 429,
33
+ statusMessage: "Too many failed attempts, try again later"
34
+ });
35
+ }
36
+ const attempts = failures.get(ip);
37
+ if (attempts && attempts.resetAt > now && attempts.count >= RATE_MAX_FAILURES) {
38
+ throw createError({
39
+ statusCode: 429,
40
+ statusMessage: "Too many failed attempts, try again later"
41
+ });
42
+ }
43
+ const body = await readValidatedBody(event, credentialsSchema.parse);
44
+ const { adminEmail, adminPassword } = useRuntimeConfig(event).cms;
45
+ if (!adminEmail || !adminPassword) {
46
+ throw createError({ statusCode: 403, statusMessage: "Admin credentials are not configured" });
47
+ }
48
+ const emailOk = safeEqual(body.email.toLowerCase(), adminEmail.toLowerCase());
49
+ const passwordOk = safeEqual(body.password, adminPassword);
50
+ if (!emailOk || !passwordOk) {
51
+ const current = attempts && attempts.resetAt > now ? attempts : { count: 0, resetAt: now + RATE_WINDOW_MS };
52
+ current.count++;
53
+ failures.set(ip, current);
54
+ if (globalFailures.resetAt <= now)
55
+ globalFailures = { count: 0, resetAt: now + RATE_WINDOW_MS };
56
+ globalFailures.count++;
57
+ throw createError({ statusCode: 401, statusMessage: "Invalid credentials" });
58
+ }
59
+ failures.delete(ip);
60
+ await setUserSession(event, { user: { email: adminEmail.toLowerCase() } });
61
+ return { loggedIn: true };
62
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
2
+ export default _default;
@@ -0,0 +1,34 @@
1
+ import { createYoga } from "graphql-yoga";
2
+ import { defineEventHandler } from "h3";
3
+ import { useRuntimeConfig } from "#imports";
4
+ import { buildCmsSchema, createGraphqlContext } from "../utils/graphql.js";
5
+ import { createBreadthRule, createDepthRule, createIntrospectionRule } from "../utils/graphql-depth.js";
6
+ function buildYoga(options) {
7
+ return createYoga({
8
+ schema: buildCmsSchema(),
9
+ graphqlEndpoint: "/api/cms/graphql",
10
+ landingPage: false,
11
+ graphiql: options.graphiql,
12
+ cors: false,
13
+ context: () => createGraphqlContext(),
14
+ plugins: [
15
+ {
16
+ onValidate({
17
+ addValidationRule
18
+ }) {
19
+ addValidationRule(createDepthRule(options.maxDepth));
20
+ addValidationRule(createBreadthRule());
21
+ if (!options.graphiql) addValidationRule(createIntrospectionRule());
22
+ }
23
+ }
24
+ ]
25
+ });
26
+ }
27
+ let yoga = null;
28
+ export default defineEventHandler((event) => {
29
+ if (!yoga) {
30
+ const { graphql } = useRuntimeConfig(event).cms;
31
+ yoga = buildYoga(graphql);
32
+ }
33
+ return yoga(event.node.req, event.node.res);
34
+ });
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../../.nuxt/tsconfig.server.json"
3
+ }
@@ -0,0 +1,2 @@
1
+ export declare const ID_LENGTH = 16;
2
+ export declare function customId(prefix: string, length?: number): string;
@@ -0,0 +1,10 @@
1
+ import { randomInt } from "node:crypto";
2
+ const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
3
+ export const ID_LENGTH = 16;
4
+ export function customId(prefix, length = ID_LENGTH) {
5
+ let result = prefix ? `${prefix}_` : "";
6
+ for (let i = 0; i < length; i++) {
7
+ result += ALPHABET[randomInt(ALPHABET.length)];
8
+ }
9
+ return result;
10
+ }
@@ -0,0 +1 @@
1
+ export declare function mapConstraintErrors<T>(run: () => Promise<T>): Promise<T>;
@@ -0,0 +1,31 @@
1
+ import { createError, isError } from "h3";
2
+ const FK_CODES = /* @__PURE__ */ new Set(["SQLITE_CONSTRAINT_FOREIGNKEY", "23503"]);
3
+ const UNIQUE_CODES = /* @__PURE__ */ new Set(["SQLITE_CONSTRAINT_UNIQUE", "SQLITE_CONSTRAINT_PRIMARYKEY", "23505"]);
4
+ const NOT_NULL_CODES = /* @__PURE__ */ new Set(["SQLITE_CONSTRAINT_NOTNULL", "23502"]);
5
+ export async function mapConstraintErrors(run) {
6
+ try {
7
+ return await run();
8
+ } catch (error) {
9
+ if (isError(error)) throw error;
10
+ const code = error?.code ?? "";
11
+ if (FK_CODES.has(code)) {
12
+ throw createError({
13
+ statusCode: 409,
14
+ statusMessage: "Operation conflicts with a relation: a referenced entry is missing or this entry is still referenced"
15
+ });
16
+ }
17
+ if (UNIQUE_CODES.has(code)) {
18
+ throw createError({
19
+ statusCode: 409,
20
+ statusMessage: "Duplicate value for a unique field"
21
+ });
22
+ }
23
+ if (NOT_NULL_CODES.has(code)) {
24
+ throw createError({
25
+ statusCode: 422,
26
+ statusMessage: "A required field is missing"
27
+ });
28
+ }
29
+ throw error;
30
+ }
31
+ }
@@ -0,0 +1,3 @@
1
+ export declare function useDb(): import("drizzle-orm/node-postgres").NodePgDatabase<Record<string, unknown>> & {
2
+ $client: import("pg").Pool;
3
+ };
@@ -0,0 +1,16 @@
1
+ import { drizzle } from "drizzle-orm/node-postgres";
2
+ import pg from "pg";
3
+ import { useRuntimeConfig } from "#imports";
4
+ let _db = null;
5
+ export function useDb() {
6
+ if (!_db) {
7
+ const { databaseUrl } = useRuntimeConfig().cms;
8
+ if (!databaseUrl) {
9
+ throw new Error(
10
+ "[nuxt-cms] Missing Postgres connection string: set NUXT_CMS_DATABASE_URL or cms.database.url"
11
+ );
12
+ }
13
+ _db = drizzle(new pg.Pool({ connectionString: databaseUrl }));
14
+ }
15
+ return _db;
16
+ }
@@ -0,0 +1,4 @@
1
+ import Database from 'better-sqlite3';
2
+ export declare function useDb(): import("drizzle-orm/better-sqlite3").BetterSQLite3Database<Record<string, unknown>> & {
3
+ $client: Database.Database;
4
+ };
@@ -0,0 +1,17 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import Database from "better-sqlite3";
4
+ import { drizzle } from "drizzle-orm/better-sqlite3";
5
+ import { useRuntimeConfig } from "#imports";
6
+ let _db = null;
7
+ export function useDb() {
8
+ if (!_db) {
9
+ const { dbPath } = useRuntimeConfig().cms;
10
+ mkdirSync(dirname(dbPath), { recursive: true });
11
+ const sqlite = new Database(dbPath);
12
+ sqlite.pragma("journal_mode = WAL");
13
+ sqlite.pragma("foreign_keys = ON");
14
+ _db = drizzle(sqlite);
15
+ }
16
+ return _db;
17
+ }
@@ -0,0 +1,4 @@
1
+ import type { ASTVisitor, ValidationContext } from 'graphql';
2
+ export declare function createDepthRule(maxDepth?: number): (context: ValidationContext) => ASTVisitor;
3
+ export declare function createBreadthRule(maxFields?: number): (context: ValidationContext) => ASTVisitor;
4
+ export declare function createIntrospectionRule(): (context: ValidationContext) => ASTVisitor;