@xleddyl/nuxt-cms 0.8.1 → 0.9.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.
- package/dist/module.json +1 -1
- package/dist/module.mjs +25 -16
- package/dist/runtime/app/layouts/cms-admin.vue +2 -4
- package/dist/runtime/app/pages/admin-login.vue +19 -51
- package/dist/runtime/app/pages/admin-logout.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-logout.vue +36 -0
- package/dist/runtime/app/pages/admin-logout.vue.d.ts +3 -0
- package/dist/runtime/shared/preview.js +1 -1
- package/package.json +2 -2
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { minifyIntrospection, outputIntrospectionFile } from 'gql.tada/internal'
|
|
|
9
9
|
import { createJiti } from 'jiti';
|
|
10
10
|
import { typeName, blockTypeName as blockTypeName$1, blocksFieldTypeName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
|
|
11
11
|
import { migrationsDirFor } from '../dist/runtime/shared/migrations-dir.js';
|
|
12
|
-
import { entryFieldsFor, isPrivateField, blockTypeName, pageColumnFields, isTranslatableField, isTranslatableMediaField, isStringListField, pageAllFields, normalizeContentEntries, PAGE_PATH_FIELD, pageRoutes, isCollectionKind, isMultiSelect, layoutErrors, listErrors, mobileMediaKey, fieldConditions, selectOptionValues,
|
|
12
|
+
import { entryFieldsFor, isPrivateField, blockTypeName, pageColumnFields, isTranslatableField, isTranslatableMediaField, isStringListField, pageAllFields, normalizeContentEntries, PAGE_PATH_FIELD, pageRoutes, isCollectionKind, isMultiSelect, layoutErrors, listErrors, mobileMediaKey, fieldConditions, selectOptionValues, pageFieldsTableName, pageMediaTableName, CONTENT_SYSTEM_FIELDS, isRequiredField, pageFields, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE, normalizeCmsConfig } from '../dist/runtime/shared/index.js';
|
|
13
13
|
import { previewPathErrors, DEFAULT_CMS_PREVIEW_PATH } from '../dist/runtime/shared/preview.js';
|
|
14
14
|
import { scanMediaDirectory, readMediaFileMeta } from '../dist/runtime/server/utils/media-sync.js';
|
|
15
15
|
import { createHash } from 'node:crypto';
|
|
@@ -233,6 +233,7 @@ const RESERVED_ENTRY_KEYS = [
|
|
|
233
233
|
"admin",
|
|
234
234
|
"auth",
|
|
235
235
|
"login",
|
|
236
|
+
"logout",
|
|
236
237
|
"media",
|
|
237
238
|
"graphql",
|
|
238
239
|
"settings",
|
|
@@ -482,8 +483,6 @@ function validateConfig(config, i18n) {
|
|
|
482
483
|
if (!source) errors.push(`${fat}: slug source '${field.from}' is not a declared field`);
|
|
483
484
|
else if (source.type !== "text")
|
|
484
485
|
errors.push(`${fat}: slug source '${field.from}' must be a text field`);
|
|
485
|
-
else if (isTranslatableField(source) && !(entry.kind === "content" && key === CONTENT_SLUG_FIELD))
|
|
486
|
-
errors.push(`${fat}: slug source '${field.from}' cannot be translatable`);
|
|
487
486
|
}
|
|
488
487
|
if (field.type === "tags") {
|
|
489
488
|
errors.push(...tagsOptionErrors(fat, field));
|
|
@@ -761,9 +760,8 @@ function columnExpr(key, field, dialect, table) {
|
|
|
761
760
|
expr = `text('${col}').unique()`;
|
|
762
761
|
break;
|
|
763
762
|
case "relation": {
|
|
764
|
-
const onDelete =
|
|
765
|
-
|
|
766
|
-
expr = `text('${col}').references(${target} => ${field.to}.id, { onDelete: '${onDelete}' })`;
|
|
763
|
+
const onDelete = relationOnDelete(field);
|
|
764
|
+
expr = field.to === table ? `text('${col}')` : `text('${col}').references(() => ${field.to}.id, { onDelete: '${onDelete}' })`;
|
|
767
765
|
if (field.cardinality === "one-to-one") expr += ".unique()";
|
|
768
766
|
break;
|
|
769
767
|
}
|
|
@@ -773,8 +771,8 @@ function columnExpr(key, field, dialect, table) {
|
|
|
773
771
|
if (isRequiredField(field)) expr += ".notNull()";
|
|
774
772
|
return ` ${key}: ${expr},`;
|
|
775
773
|
}
|
|
776
|
-
function
|
|
777
|
-
return
|
|
774
|
+
function relationOnDelete(field) {
|
|
775
|
+
return field.onDelete ?? (field.required ? "restrict" : "set null");
|
|
778
776
|
}
|
|
779
777
|
function isSelfRelation(table, field) {
|
|
780
778
|
return field.type === "relation" && !isManyToMany(field) && field.to === table;
|
|
@@ -792,6 +790,7 @@ function tableExpr(name, entry, dialect) {
|
|
|
792
790
|
const pg = dialect === "postgres";
|
|
793
791
|
const tableFn = pg ? "pgTable" : "sqliteTable";
|
|
794
792
|
const lines = [];
|
|
793
|
+
const selfReferences = [];
|
|
795
794
|
lines.push(` id: text('id').primaryKey(),`);
|
|
796
795
|
if (entry.kind === "page") lines.push(` path: text('path').notNull().unique(),`);
|
|
797
796
|
for (const [key, field] of Object.entries(
|
|
@@ -799,13 +798,22 @@ function tableExpr(name, entry, dialect) {
|
|
|
799
798
|
)) {
|
|
800
799
|
if (isManyToMany(field)) continue;
|
|
801
800
|
lines.push(columnExpr(key, field, dialect, name));
|
|
801
|
+
if (isSelfRelation(name, field))
|
|
802
|
+
selfReferences.push(
|
|
803
|
+
`foreignKey({ columns: [table.${key}], foreignColumns: [table.id] }).onDelete('${relationOnDelete(
|
|
804
|
+
field
|
|
805
|
+
)}')`
|
|
806
|
+
);
|
|
802
807
|
}
|
|
803
808
|
if (entry.drafts) lines.push(` status: text('status').notNull().default('draft'),`);
|
|
804
809
|
if (isCollectionKind(entry)) lines.push(` createdAt: ${nowExpr(pg, "created_at")},`);
|
|
805
810
|
lines.push(` updatedAt: ${nowExpr(pg, "updated_at")},`);
|
|
811
|
+
const extra = selfReferences.length ? `, table => [
|
|
812
|
+
${selfReferences.join(",\n ")},
|
|
813
|
+
]` : "";
|
|
806
814
|
return `export const ${name} = ${tableFn}('${name}', {
|
|
807
815
|
${lines.join("\n")}
|
|
808
|
-
})`;
|
|
816
|
+
}${extra})`;
|
|
809
817
|
}
|
|
810
818
|
function joinTableExpr(name, key, field, dialect) {
|
|
811
819
|
const pg = dialect === "postgres";
|
|
@@ -888,18 +896,14 @@ function renderSchemaFile(config, dialect, resolveImport = (s) => s) {
|
|
|
888
896
|
core.add("jsonb");
|
|
889
897
|
if (pg) core.add("timestamp");
|
|
890
898
|
if (fields.some(isManyToMany) || rowsEntries.length) core.add("primaryKey");
|
|
891
|
-
|
|
899
|
+
if (derived.some(
|
|
892
900
|
([name, entry]) => Object.values(entry.kind === "page" ? pageColumnFields(entry) : entry.fields).some(
|
|
893
901
|
(field) => isSelfRelation(name, field)
|
|
894
902
|
)
|
|
895
|
-
)
|
|
903
|
+
))
|
|
904
|
+
core.add("foreignKey");
|
|
896
905
|
const imports = [
|
|
897
906
|
...pg ? [] : [`import { sql } from '${resolveImport("drizzle-orm")}'`],
|
|
898
|
-
...hasSelfRelation ? [
|
|
899
|
-
`import type { ${anyColumnType(dialect)} } from '${resolveImport(
|
|
900
|
-
`drizzle-orm/${pg ? "pg-core" : "sqlite-core"}`
|
|
901
|
-
)}'`
|
|
902
|
-
] : [],
|
|
903
907
|
`import { ${[...core].sort().join(", ")} } from '${resolveImport(
|
|
904
908
|
`drizzle-orm/${pg ? "pg-core" : "sqlite-core"}`
|
|
905
909
|
)}'`
|
|
@@ -1985,6 +1989,11 @@ const module$1 = defineNuxtModule({
|
|
|
1985
1989
|
path: "/cms/login",
|
|
1986
1990
|
file: resolver.resolve("./runtime/app/pages/admin-login.vue")
|
|
1987
1991
|
},
|
|
1992
|
+
{
|
|
1993
|
+
name: "cms-admin-logout",
|
|
1994
|
+
path: "/cms/logout",
|
|
1995
|
+
file: resolver.resolve("./runtime/app/pages/admin-logout.vue")
|
|
1996
|
+
},
|
|
1988
1997
|
{
|
|
1989
1998
|
name: "cms-admin-media",
|
|
1990
1999
|
path: "/cms/media",
|
|
@@ -86,7 +86,7 @@ import {
|
|
|
86
86
|
Root as KRoot,
|
|
87
87
|
provideTheme
|
|
88
88
|
} from "@xleddyl/443-components-vue";
|
|
89
|
-
import { computed, navigateTo, ref, useRoute,
|
|
89
|
+
import { computed, navigateTo, ref, useRoute, watch } from "#imports";
|
|
90
90
|
import cmsConfig from "#cms-config";
|
|
91
91
|
import { cmsBrand } from "#cms-brand";
|
|
92
92
|
import { CMS_SEARCH_MIN_LENGTH, normalizeSearchQuery } from "../../shared/search";
|
|
@@ -95,7 +95,6 @@ import { useCmsAccount } from "../composables/cms-account";
|
|
|
95
95
|
import { loadCmsSettings } from "../composables/cms-settings";
|
|
96
96
|
import { cmsIcon } from "../utils/ui";
|
|
97
97
|
const route = useRoute();
|
|
98
|
-
const { clear } = useUserSession();
|
|
99
98
|
const { email, displayName } = useCmsAccount();
|
|
100
99
|
const theme = useCmsTheme();
|
|
101
100
|
provideTheme(theme);
|
|
@@ -219,8 +218,7 @@ function openSettings() {
|
|
|
219
218
|
}
|
|
220
219
|
async function logout() {
|
|
221
220
|
settingsOpen.value = false;
|
|
222
|
-
await
|
|
223
|
-
await navigateTo("/cms/login");
|
|
221
|
+
await navigateTo("/cms/logout");
|
|
224
222
|
}
|
|
225
223
|
</script>
|
|
226
224
|
|
|
@@ -1,52 +1,29 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<KRoot class="cms-scope cms-canvas" :theme="theme" preset="default">
|
|
3
|
-
<
|
|
3
|
+
<KAuthLogin
|
|
4
|
+
:submit="login"
|
|
5
|
+
:submit-error="loginError"
|
|
4
6
|
title="Welcome back"
|
|
5
7
|
description="Sign in to manage your content."
|
|
6
8
|
footer="Powered by nuxt-cms"
|
|
9
|
+
aside-title="Your content, next to your code."
|
|
10
|
+
aside-description="Edit entries, pages and media for this site in one place."
|
|
7
11
|
>
|
|
8
12
|
<template #brand>
|
|
9
|
-
<CmsBrandMark size="lg" hide-text
|
|
13
|
+
<CmsBrandMark size="lg" hide-text />
|
|
10
14
|
</template>
|
|
11
|
-
|
|
12
|
-
<KForm :form="form">
|
|
13
|
-
<KFormField label="Email" name="email" required>
|
|
14
|
-
<KInput
|
|
15
|
-
v-model="form.values.email"
|
|
16
|
-
type="email"
|
|
17
|
-
:icon="cmsIcon('envelope')"
|
|
18
|
-
autocomplete="username"
|
|
19
|
-
placeholder="you@example.com"
|
|
20
|
-
/>
|
|
21
|
-
</KFormField>
|
|
22
|
-
<KFormField label="Password" name="password" required>
|
|
23
|
-
<KInput
|
|
24
|
-
v-model="form.values.password"
|
|
25
|
-
type="password"
|
|
26
|
-
icon="key"
|
|
27
|
-
autocomplete="current-password"
|
|
28
|
-
placeholder="••••••••"
|
|
29
|
-
/>
|
|
30
|
-
</KFormField>
|
|
31
|
-
<KButton type="submit" label="Sign in" trailing-icon="arrow-right" size="lg" block />
|
|
32
|
-
</KForm>
|
|
33
|
-
</KAuthLayout>
|
|
15
|
+
</KAuthLogin>
|
|
34
16
|
</KRoot>
|
|
35
17
|
</template>
|
|
36
18
|
|
|
37
19
|
<script setup>
|
|
38
20
|
import {
|
|
39
|
-
|
|
40
|
-
Button as KButton,
|
|
41
|
-
Form as KForm,
|
|
42
|
-
FormField as KFormField,
|
|
43
|
-
Input as KInput,
|
|
21
|
+
AuthLogin as KAuthLogin,
|
|
44
22
|
Root as KRoot,
|
|
45
|
-
provideTheme
|
|
46
|
-
useForm
|
|
23
|
+
provideTheme
|
|
47
24
|
} from "@xleddyl/443-components-vue";
|
|
48
25
|
import { definePageMeta, navigateTo, useUserSession } from "#imports";
|
|
49
|
-
import {
|
|
26
|
+
import { errorMessage } from "../utils/ui";
|
|
50
27
|
import { useCmsTheme } from "../composables/cms-theme";
|
|
51
28
|
definePageMeta({ layout: false });
|
|
52
29
|
const theme = useCmsTheme();
|
|
@@ -55,25 +32,16 @@ const { loggedIn } = useUserSession();
|
|
|
55
32
|
if (loggedIn.value) {
|
|
56
33
|
await navigateTo("/cms", { replace: true });
|
|
57
34
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
password: { initial: () => "" }
|
|
62
|
-
},
|
|
63
|
-
onSubmit: login
|
|
64
|
-
});
|
|
65
|
-
async function login(values) {
|
|
66
|
-
try {
|
|
67
|
-
await $fetch("/api/cms/auth/login", { method: "POST", body: values });
|
|
68
|
-
window.location.replace("/cms");
|
|
69
|
-
} catch (e) {
|
|
70
|
-
const status = e.statusCode;
|
|
71
|
-
form.setError(
|
|
72
|
-
"",
|
|
73
|
-
status === 401 ? "Invalid email or password." : errorMessage(e) ?? "Login failed. Please try again."
|
|
74
|
-
);
|
|
75
|
-
}
|
|
35
|
+
async function login(credentials) {
|
|
36
|
+
await $fetch("/api/cms/auth/login", { method: "POST", body: credentials });
|
|
37
|
+
window.location.replace("/cms");
|
|
76
38
|
}
|
|
39
|
+
const loginError = (error) => {
|
|
40
|
+
const status = error.statusCode;
|
|
41
|
+
return {
|
|
42
|
+
message: status === 401 ? "Invalid email or password." : errorMessage(error) ?? "Login failed. Please try again."
|
|
43
|
+
};
|
|
44
|
+
};
|
|
77
45
|
</script>
|
|
78
46
|
|
|
79
47
|
<style src="../../assets/admin.css">
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<KRoot class="cms-scope cms-canvas" :theme="theme" preset="default">
|
|
3
|
+
<KAuthLogout
|
|
4
|
+
:logout="clear"
|
|
5
|
+
:account="account"
|
|
6
|
+
login-to="/cms/login"
|
|
7
|
+
footer="Powered by nuxt-cms"
|
|
8
|
+
aside-title="Your content, next to your code."
|
|
9
|
+
aside-description="Edit entries, pages and media for this site in one place."
|
|
10
|
+
>
|
|
11
|
+
<template #brand>
|
|
12
|
+
<CmsBrandMark size="lg" hide-text />
|
|
13
|
+
</template>
|
|
14
|
+
</KAuthLogout>
|
|
15
|
+
</KRoot>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup>
|
|
19
|
+
import { AuthLogout as KAuthLogout, Root as KRoot, provideTheme } from "@xleddyl/443-components-vue";
|
|
20
|
+
import { computed, definePageMeta, useUserSession } from "#imports";
|
|
21
|
+
import { useCmsAccount } from "../composables/cms-account";
|
|
22
|
+
import { useCmsTheme } from "../composables/cms-theme";
|
|
23
|
+
definePageMeta({ layout: false, middleware: "cms-auth" });
|
|
24
|
+
const theme = useCmsTheme();
|
|
25
|
+
provideTheme(theme);
|
|
26
|
+
const { clear } = useUserSession();
|
|
27
|
+
const { user, email } = useCmsAccount();
|
|
28
|
+
const account = computed(() => ({
|
|
29
|
+
email: email.value,
|
|
30
|
+
name: user.value?.name?.trim() || void 0
|
|
31
|
+
}));
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<style src="../../assets/admin.css">
|
|
35
|
+
|
|
36
|
+
</style>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "./index.js";
|
|
15
15
|
export const CMS_PREVIEW_CHANNEL = "nuxt-cms:preview";
|
|
16
16
|
export const DEFAULT_CMS_PREVIEW_PATH = "/cms/preview";
|
|
17
|
-
const RESERVED_ADMIN_SEGMENTS = /* @__PURE__ */ new Set(["login", "media"]);
|
|
17
|
+
const RESERVED_ADMIN_SEGMENTS = /* @__PURE__ */ new Set(["login", "logout", "media"]);
|
|
18
18
|
export function previewPathErrors(path, entryNames) {
|
|
19
19
|
const match = /^\/cms\/([a-z0-9][a-z0-9-]*)$/.exec(path);
|
|
20
20
|
if (!match)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xleddyl/nuxt-cms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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)",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"@tiptap/pm": "^3.32.0",
|
|
70
70
|
"@tiptap/starter-kit": "^3.32.0",
|
|
71
71
|
"@tiptap/vue-3": "^3.32.0",
|
|
72
|
-
"@xleddyl/443-components-vue": "^0.
|
|
72
|
+
"@xleddyl/443-components-vue": "^0.16.49",
|
|
73
73
|
"aws4fetch": "^1.0.20",
|
|
74
74
|
"drizzle-kit": "^0.31.0",
|
|
75
75
|
"drizzle-orm": "^0.44.0",
|