@xleddyl/nuxt-cms 0.2.0 → 0.2.1
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/module.json +1 -1
- package/dist/module.mjs +16 -5
- package/dist/runtime/app/components/cms/Drawer.vue +5 -9
- package/dist/runtime/app/components/cms/EntryDrawer.vue +11 -3
- package/dist/runtime/app/components/cms/EntryForm.vue +0 -1
- package/dist/runtime/app/components/cms/Input.d.vue.ts +0 -1
- package/dist/runtime/app/components/cms/Input.vue +7 -4
- package/dist/runtime/app/components/cms/Input.vue.d.ts +0 -1
- package/dist/runtime/app/components/cms/LocaleSwitch.vue +1 -1
- package/dist/runtime/app/components/cms/MediaFolderPicker.d.vue.ts +0 -1
- package/dist/runtime/app/components/cms/MediaFolderPicker.vue +1 -3
- package/dist/runtime/app/components/cms/MediaFolderPicker.vue.d.ts +0 -1
- package/dist/runtime/app/components/cms/Modal.vue +5 -9
- package/dist/runtime/app/components/cms/PasswordInput.vue +9 -5
- package/dist/runtime/app/components/cms/RelationField.vue +7 -2
- package/dist/runtime/app/components/cms/SelectMenu.d.vue.ts +0 -1
- package/dist/runtime/app/components/cms/SelectMenu.vue +1 -6
- package/dist/runtime/app/components/cms/SelectMenu.vue.d.ts +0 -1
- package/dist/runtime/app/components/cms/SettingsEntry.vue +1 -8
- package/dist/runtime/app/components/cms/SettingsModal.vue +5 -10
- package/dist/runtime/app/components/cms/SettingsUsers.vue +8 -4
- package/dist/runtime/app/components/cms/Table.d.vue.ts +0 -7
- package/dist/runtime/app/components/cms/Table.vue +7 -49
- package/dist/runtime/app/components/cms/Table.vue.d.ts +0 -7
- package/dist/runtime/app/components/cms/Textarea.d.vue.ts +0 -1
- package/dist/runtime/app/components/cms/Textarea.vue +1 -2
- package/dist/runtime/app/components/cms/Textarea.vue.d.ts +0 -1
- package/dist/runtime/app/composables/cms-entry-disabled.js +1 -1
- package/dist/runtime/app/composables/cms-media-keys.d.ts +1 -0
- package/dist/runtime/app/composables/cms-media-keys.js +5 -0
- package/dist/runtime/app/composables/cms-media-library.js +3 -0
- package/dist/runtime/app/composables/cms-media-uploader.d.ts +0 -1
- package/dist/runtime/app/composables/cms-media-uploader.js +1 -1
- package/dist/runtime/app/composables/cms-overlay.d.ts +4 -0
- package/dist/runtime/app/composables/cms-overlay.js +23 -0
- package/dist/runtime/app/composables/cms-settings.d.ts +0 -1
- package/dist/runtime/app/composables/cms-settings.js +1 -1
- package/dist/runtime/app/pages/admin-entry.vue +1 -0
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/generate.js +1 -1
- package/dist/runtime/server/api/collection.get.js +1 -1
- package/dist/runtime/server/api/media.delete.js +2 -1
- package/dist/runtime/server/api/media.post.js +2 -1
- package/dist/runtime/server/api/media.put.js +2 -1
- package/dist/runtime/server/api/users.post.js +13 -9
- package/dist/runtime/server/routes/auth/login.post.js +2 -2
- package/dist/runtime/server/utils/graphql-depth.js +12 -5
- package/dist/runtime/server/utils/graphql.js +2 -2
- package/dist/runtime/server/utils/media-folders.js +10 -2
- package/dist/runtime/server/utils/media.d.ts +1 -0
- package/dist/runtime/server/utils/media.js +4 -1
- package/dist/runtime/server/utils/migrate.js +1 -1
- package/dist/runtime/server/utils/require-admin.d.ts +0 -2
- package/dist/runtime/server/utils/require-admin.js +0 -1
- package/dist/runtime/shared/layout.d.ts +0 -1
- package/dist/runtime/shared/layout.js +0 -3
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import { useDb } from "#cms-db";
|
|
|
4
4
|
import { cms_users } from "#cms-tables";
|
|
5
5
|
import { MIN_PASSWORD_LENGTH } from "../../shared/index.js";
|
|
6
6
|
import { customId } from "../utils/custom-id.js";
|
|
7
|
+
import { mapConstraintErrors } from "../utils/db-errors.js";
|
|
7
8
|
import { hashPassword } from "../utils/password.js";
|
|
8
9
|
import { isSuperAdminEmail, requireSuperAdmin } from "../utils/require-admin.js";
|
|
9
10
|
import { findUserByEmail, toAccount } from "../utils/users.js";
|
|
@@ -19,14 +20,17 @@ export default defineEventHandler(async (event) => {
|
|
|
19
20
|
throw createError({ statusCode: 409, statusMessage: "A user with this email already exists" });
|
|
20
21
|
}
|
|
21
22
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
const passwordHash = await hashPassword(body.password);
|
|
24
|
+
const [row] = await mapConstraintErrors(
|
|
25
|
+
() => useDb().insert(cms_users).values({
|
|
26
|
+
id: customId("usr"),
|
|
27
|
+
email: body.email,
|
|
28
|
+
name: body.name || null,
|
|
29
|
+
role: "admin",
|
|
30
|
+
passwordHash,
|
|
31
|
+
createdAt: now,
|
|
32
|
+
updatedAt: now
|
|
33
|
+
}).returning()
|
|
34
|
+
);
|
|
31
35
|
return toAccount(row);
|
|
32
36
|
});
|
|
@@ -9,8 +9,8 @@ import { verifyNothing, verifyPassword } from "../../utils/password.js";
|
|
|
9
9
|
import { assertSameOrigin } from "../../utils/same-origin.js";
|
|
10
10
|
import { findUserByEmail, sessionUserFor } from "../../utils/users.js";
|
|
11
11
|
const credentialsSchema = z.object({
|
|
12
|
-
email: z.string().trim().min(1),
|
|
13
|
-
password: z.string().min(1)
|
|
12
|
+
email: z.string().trim().min(1).max(254),
|
|
13
|
+
password: z.string().min(1).max(256)
|
|
14
14
|
});
|
|
15
15
|
const RATE_WINDOW_MS = 15 * 6e4;
|
|
16
16
|
const RATE_MAX_FAILURES = 10;
|
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
import { GraphQLError, Kind } from "graphql";
|
|
2
2
|
const DEFAULT_MAX_DEPTH = 8;
|
|
3
3
|
const DEFAULT_MAX_FIELDS = 500;
|
|
4
|
-
function measure(selectionSet, fragments, visited) {
|
|
4
|
+
function measure(selectionSet, fragments, visited, fragmentDepths) {
|
|
5
5
|
let max = 0;
|
|
6
6
|
for (const selection of selectionSet.selections) {
|
|
7
7
|
if (selection.kind === Kind.FIELD) {
|
|
8
8
|
if (selection.name.value.startsWith("__")) continue;
|
|
9
|
-
const depth = selection.selectionSet ? 1 + measure(selection.selectionSet, fragments, visited) : 1;
|
|
9
|
+
const depth = selection.selectionSet ? 1 + measure(selection.selectionSet, fragments, visited, fragmentDepths) : 1;
|
|
10
10
|
max = Math.max(max, depth);
|
|
11
11
|
} else if (selection.kind === Kind.FRAGMENT_SPREAD) {
|
|
12
12
|
const name = selection.name.value;
|
|
13
13
|
if (visited.has(name)) continue;
|
|
14
|
+
const known = fragmentDepths.get(name);
|
|
15
|
+
if (known !== void 0) {
|
|
16
|
+
max = Math.max(max, known);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
14
19
|
const fragment = fragments.get(name);
|
|
15
20
|
if (fragment) {
|
|
16
21
|
visited.add(name);
|
|
17
|
-
|
|
22
|
+
const depth = measure(fragment.selectionSet, fragments, visited, fragmentDepths);
|
|
18
23
|
visited.delete(name);
|
|
24
|
+
fragmentDepths.set(name, depth);
|
|
25
|
+
max = Math.max(max, depth);
|
|
19
26
|
}
|
|
20
27
|
} else if (selection.kind === Kind.INLINE_FRAGMENT) {
|
|
21
|
-
max = Math.max(max, measure(selection.selectionSet, fragments, visited));
|
|
28
|
+
max = Math.max(max, measure(selection.selectionSet, fragments, visited, fragmentDepths));
|
|
22
29
|
}
|
|
23
30
|
}
|
|
24
31
|
return max;
|
|
@@ -32,7 +39,7 @@ export function createDepthRule(maxDepth) {
|
|
|
32
39
|
if (definition.kind === Kind.FRAGMENT_DEFINITION)
|
|
33
40
|
fragments.set(definition.name.value, definition);
|
|
34
41
|
}
|
|
35
|
-
const depth = measure(node.selectionSet, fragments, /* @__PURE__ */ new Set());
|
|
42
|
+
const depth = measure(node.selectionSet, fragments, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map());
|
|
36
43
|
if (depth > limit) {
|
|
37
44
|
context.reportError(
|
|
38
45
|
new GraphQLError(`Query is too deep: depth ${depth} exceeds the maximum of ${limit}`)
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { GraphQLError, GraphQLScalarType, valueFromASTUntyped } from "graphql";
|
|
17
17
|
import { createSchema } from "graphql-yoga";
|
|
18
18
|
import cmsConfig from "#cms-config";
|
|
19
|
-
import { useDb } from "#cms-db";
|
|
19
|
+
import { cmsDialect, useDb } from "#cms-db";
|
|
20
20
|
import * as cmsTables from "#cms-tables";
|
|
21
21
|
import { useRuntimeConfig } from "#imports";
|
|
22
22
|
import {
|
|
@@ -129,7 +129,7 @@ const OPERATORS = {
|
|
|
129
129
|
lte: (column, value) => lte(column, value),
|
|
130
130
|
like: (column, value) => {
|
|
131
131
|
const pattern = String(value).replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_").replaceAll("*", "%");
|
|
132
|
-
return sql`${column} like ${pattern} escape '\\'`;
|
|
132
|
+
return cmsDialect === "postgres" ? sql`cast(${column} as text) ilike ${pattern} escape '\\'` : sql`${column} like ${pattern} escape '\\'`;
|
|
133
133
|
},
|
|
134
134
|
in: (column, value) => inArray(column, value)
|
|
135
135
|
};
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { eq, inArray, or, sql } from "drizzle-orm";
|
|
2
2
|
import { useDb } from "#cms-db";
|
|
3
3
|
import { cms_media } from "#cms-tables";
|
|
4
4
|
import { mediaFolderMarkerKey } from "../../shared/index.js";
|
|
5
5
|
const KEY_CHUNK = 90;
|
|
6
|
+
function escapeLike(value) {
|
|
7
|
+
return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
8
|
+
}
|
|
6
9
|
export async function writeMediaFolderMarker(store, folder) {
|
|
7
10
|
const key = mediaFolderMarkerKey(folder);
|
|
8
11
|
await store.write(key, new Uint8Array(), "application/x-empty");
|
|
@@ -18,7 +21,12 @@ export async function writeMediaFolderMarker(store, folder) {
|
|
|
18
21
|
await useDb().insert(cms_media).values(values).onConflictDoUpdate({ target: cms_media.key, set: values });
|
|
19
22
|
}
|
|
20
23
|
export function mediaRowsInFolder(folder) {
|
|
21
|
-
return useDb().select({ key: cms_media.key, folder: cms_media.folder }).from(cms_media).where(
|
|
24
|
+
return useDb().select({ key: cms_media.key, folder: cms_media.folder }).from(cms_media).where(
|
|
25
|
+
or(
|
|
26
|
+
sql`${cms_media.folder} like ${`${escapeLike(folder)}/%`} escape '\\'`,
|
|
27
|
+
eq(cms_media.folder, folder)
|
|
28
|
+
)
|
|
29
|
+
);
|
|
22
30
|
}
|
|
23
31
|
export async function deleteMediaRows(store, keys) {
|
|
24
32
|
for (const key of keys) {
|
|
@@ -20,6 +20,7 @@ export interface MediaStore {
|
|
|
20
20
|
write: (key: string, body: Uint8Array<ArrayBuffer>, contentType: string) => Promise<void>;
|
|
21
21
|
remove: (key: string) => Promise<void>;
|
|
22
22
|
}
|
|
23
|
+
export declare function baseContentType(contentType: string): string;
|
|
23
24
|
export declare function isAllowedUploadType(contentType: string): boolean;
|
|
24
25
|
export declare function assertUploadContentType(contentType: string): void;
|
|
25
26
|
export declare function assertUploadSize(size: number, maxFileSize: number): void;
|
|
@@ -20,8 +20,11 @@ const UPLOAD_TYPES = /* @__PURE__ */ new Set([
|
|
|
20
20
|
"application/vnd.ms-powerpoint",
|
|
21
21
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
22
22
|
]);
|
|
23
|
+
export function baseContentType(contentType) {
|
|
24
|
+
return contentType.split(";")[0].trim().toLowerCase();
|
|
25
|
+
}
|
|
23
26
|
export function isAllowedUploadType(contentType) {
|
|
24
|
-
const type = contentType
|
|
27
|
+
const type = baseContentType(contentType);
|
|
25
28
|
return !UPLOAD_TYPE_BLOCKLIST.has(type) && (UPLOAD_TYPES.has(type) || UPLOAD_TYPE_PREFIXES.some((prefix) => type.startsWith(prefix)));
|
|
26
29
|
}
|
|
27
30
|
export function assertUploadContentType(contentType) {
|
|
@@ -18,7 +18,7 @@ async function pendingMigrations() {
|
|
|
18
18
|
const migrations = await readMigrations(migrationsDir);
|
|
19
19
|
if (!migrations.length && Object.keys(cmsConfig).length) {
|
|
20
20
|
console.error(
|
|
21
|
-
`[nuxt-cms] No migrations were found for this build (expected in ${migrationsDir}). CMS tables may be missing
|
|
21
|
+
`[nuxt-cms] No migrations were found for this build (expected in ${migrationsDir}). CMS tables may be missing. Run the dev server once to generate them, commit server/db/migrations, and rebuild.`
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
24
|
return migrations;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { H3Event } from 'h3';
|
|
2
2
|
import type { CmsSessionUser } from '../../shared/index.js';
|
|
3
|
-
import { assertSameOrigin } from './same-origin.js';
|
|
4
|
-
export { assertSameOrigin };
|
|
5
3
|
export declare function isSuperAdminEmail(event: H3Event, email: string): boolean;
|
|
6
4
|
export declare function requireAdmin(event: H3Event): Promise<CmsSessionUser>;
|
|
7
5
|
export declare function requireSuperAdmin(event: H3Event): Promise<CmsSessionUser>;
|
|
@@ -2,7 +2,6 @@ import { createError } from "h3";
|
|
|
2
2
|
import { getUserSession, useRuntimeConfig } from "#imports";
|
|
3
3
|
import { assertSameOrigin } from "./same-origin.js";
|
|
4
4
|
import { findUserById, passwordStamp } from "./users.js";
|
|
5
|
-
export { assertSameOrigin };
|
|
6
5
|
export function isSuperAdminEmail(event, email) {
|
|
7
6
|
const { adminEmail } = useRuntimeConfig(event).cms;
|
|
8
7
|
return !!adminEmail && email.toLowerCase() === adminEmail.toLowerCase();
|
|
@@ -26,7 +26,6 @@ export interface CmsEntrySettings {
|
|
|
26
26
|
}
|
|
27
27
|
export type CmsSettingsMap = Record<string, CmsEntrySettings>;
|
|
28
28
|
export declare function layoutFromConfig(input: CmsLayoutBlock[] | undefined): CmsFormLayout;
|
|
29
|
-
export declare function layoutKeys(input: CmsLayoutBlock[] | undefined): string[];
|
|
30
29
|
export declare function resolveFormLayout(fields: Record<string, FieldConfig>, layout: CmsFormLayout | undefined): CmsFormLayout;
|
|
31
30
|
export declare function defaultListColumns(fields: Record<string, FieldConfig>, list: CmsListInput | undefined, extra?: string[]): string[];
|
|
32
31
|
export declare function resolveListColumns(fields: Record<string, FieldConfig>, saved: string[] | undefined, list: CmsListInput | undefined, extra?: string[]): string[];
|
|
@@ -30,9 +30,6 @@ function sectionsFromBlocks(blocks) {
|
|
|
30
30
|
export function layoutFromConfig(input) {
|
|
31
31
|
return { main: sectionsFromBlocks(input) };
|
|
32
32
|
}
|
|
33
|
-
export function layoutKeys(input) {
|
|
34
|
-
return layoutFromConfig(input).main.flatMap((section) => section.rows.flat());
|
|
35
|
-
}
|
|
36
33
|
function cloneSection(section) {
|
|
37
34
|
return { ...section, rows: section.rows.map((row) => [...row]) };
|
|
38
35
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xleddyl/nuxt-cms",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Lightweight CMS that ships with your Nuxt app: runs on the Nitro server, content types defined in code, /cms admin panel, GraphQL API, SQLite or Postgres. No external CMS needed!",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Edoardo Alberti (https://github.com/xleddyl)",
|