@xleddyl/nuxt-cms 0.1.21 → 0.1.23
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/README.md +2 -2
- package/dist/module.d.mts +2 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +11 -2
- package/dist/runtime/app/components/cms/MediaGallery.vue +9 -2
- package/dist/runtime/app/composables/cms-runtime.d.ts +2 -1
- package/dist/runtime/server/api/media.post.js +8 -2
- package/dist/runtime/server/api/media.put.js +3 -2
- package/dist/runtime/server/utils/media.d.ts +4 -1
- package/dist/runtime/server/utils/media.js +16 -3
- package/dist/runtime/shared/index.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ nuxt-cms is a Nuxt module that leverages the Nitro server to ship a lightweight
|
|
|
4
4
|
|
|
5
5
|
- **Zero extra infrastructure**: the CMS runs inside your app's Nitro server; you deploy one thing.
|
|
6
6
|
- **Content types in code**: a `cms.config.ts` with `defineCmsConfig()` declares collections, single documents, relations, blocks and translatable fields; database schema, migrations and TypeScript types are generated from it.
|
|
7
|
-
- **Admin panel at `/cms`**: entry editing with validation, drafts, media library (S3-compatible storage), single-admin auth from env credentials.
|
|
7
|
+
- **Admin panel at `/cms`**: entry editing with validation, drafts, media library (S3-compatible storage, or a read-only local mode), single-admin auth from env credentials.
|
|
8
8
|
- **Public GraphQL API**: read-only, typed end-to-end via gql.tada, with filtering, sorting and pagination.
|
|
9
9
|
- **SQLite, Postgres or libSQL/Turso**: a local file database by default, one config line to switch (including remote SQLite over the network).
|
|
10
10
|
|
|
@@ -78,7 +78,7 @@ Full documentation lives in [`docs/`](docs/README.md):
|
|
|
78
78
|
- [Schema](docs/schema.md) — `defineCmsConfig`, entries, field types, relations, blocks, i18n.
|
|
79
79
|
- [Querying content](docs/querying.md) — GraphQL API, `useCms` / `$cmsQuery`, filters, sorting, pagination.
|
|
80
80
|
- [Admin panel & security](docs/admin.md) — pages, authentication, sessions, admin REST API.
|
|
81
|
-
- [Media](docs/media.md) — S3-compatible storage, upload flow, allowed file types.
|
|
81
|
+
- [Media](docs/media.md) — S3-compatible storage or read-only local mode, upload flow, allowed file types.
|
|
82
82
|
|
|
83
83
|
## LLM guide
|
|
84
84
|
|
package/dist/module.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
import { MediaStorageMode } from '../dist/runtime/shared/index.js';
|
|
2
3
|
|
|
3
4
|
type Dialect = 'sqlite' | 'postgres';
|
|
4
5
|
type Driver = Dialect | 'libsql';
|
|
@@ -17,6 +18,7 @@ interface ModuleOptions {
|
|
|
17
18
|
authToken?: string;
|
|
18
19
|
};
|
|
19
20
|
media: {
|
|
21
|
+
storage: MediaStorageMode;
|
|
20
22
|
endpoint: string;
|
|
21
23
|
region: string;
|
|
22
24
|
bucket: string;
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { isAbsolute, resolve, relative, join, dirname } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { defineNuxtModule, createResolver, useLogger, addImports, resolvePath, addTemplate, addServerPlugin, addTypeTemplate, addVitePlugin,
|
|
6
|
+
import { defineNuxtModule, createResolver, useLogger, addImports, resolvePath, addTemplate, addServerPlugin, addTypeTemplate, addVitePlugin, addComponentsDir, addRouteMiddleware, extendPages, addServerHandler } from '@nuxt/kit';
|
|
7
7
|
import tailwindcss from '@tailwindcss/vite';
|
|
8
8
|
import svgLoader from 'vite-svg-loader';
|
|
9
9
|
import { introspectionFromSchema, buildSchema } from 'graphql';
|
|
@@ -437,6 +437,7 @@ const module$1 = defineNuxtModule({
|
|
|
437
437
|
authToken: ""
|
|
438
438
|
},
|
|
439
439
|
media: {
|
|
440
|
+
storage: "s3",
|
|
440
441
|
endpoint: "",
|
|
441
442
|
region: "auto",
|
|
442
443
|
bucket: "",
|
|
@@ -464,6 +465,7 @@ const module$1 = defineNuxtModule({
|
|
|
464
465
|
]);
|
|
465
466
|
nuxt.options.runtimeConfig.public.cms = {
|
|
466
467
|
mediaBaseUrl: options.media.publicBaseUrl,
|
|
468
|
+
mediaStorage: options.media.storage,
|
|
467
469
|
i18n: options.i18n
|
|
468
470
|
};
|
|
469
471
|
logger.info(
|
|
@@ -643,6 +645,7 @@ const module$1 = defineNuxtModule({
|
|
|
643
645
|
...existingConfig.graphql ?? {}
|
|
644
646
|
},
|
|
645
647
|
media: {
|
|
648
|
+
storage: options.media.storage,
|
|
646
649
|
endpoint: options.media.endpoint,
|
|
647
650
|
region: options.media.region,
|
|
648
651
|
bucket: options.media.bucket,
|
|
@@ -654,6 +657,7 @@ const module$1 = defineNuxtModule({
|
|
|
654
657
|
};
|
|
655
658
|
nuxt.options.runtimeConfig.public.cms = {
|
|
656
659
|
mediaBaseUrl: options.media.publicBaseUrl,
|
|
660
|
+
mediaStorage: options.media.storage,
|
|
657
661
|
i18n: options.i18n
|
|
658
662
|
};
|
|
659
663
|
addTypeTemplate(
|
|
@@ -682,7 +686,12 @@ const module$1 = defineNuxtModule({
|
|
|
682
686
|
addVitePlugin(tailwindcss());
|
|
683
687
|
addVitePlugin(svgLoader({ defaultImport: "url", svgoConfig: { plugins: ["prefixIds"] } }));
|
|
684
688
|
nuxt.options.css.push(resolver.resolve("./runtime/assets/main.css"));
|
|
685
|
-
|
|
689
|
+
nuxt.hook("app:templates", (app) => {
|
|
690
|
+
app.layouts["cms-admin"] = {
|
|
691
|
+
name: "cms-admin",
|
|
692
|
+
file: resolver.resolve("./runtime/app/layouts/cms-admin.vue")
|
|
693
|
+
};
|
|
694
|
+
});
|
|
686
695
|
addComponentsDir({ path: resolver.resolve("./runtime/app/components") });
|
|
687
696
|
addRouteMiddleware({
|
|
688
697
|
name: "cms-auth",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
<div v-else class="flex flex-col gap-5">
|
|
10
10
|
<CmsMediaUpload
|
|
11
|
+
v-if="!readOnly"
|
|
11
12
|
:multiple="!selectable"
|
|
12
13
|
:dense="selectable"
|
|
13
14
|
:media-type="mediaType"
|
|
@@ -89,6 +90,7 @@
|
|
|
89
90
|
class="absolute top-2 right-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
|
|
90
91
|
>
|
|
91
92
|
<CmsButton
|
|
93
|
+
v-if="!readOnly"
|
|
92
94
|
icon="pencil-square"
|
|
93
95
|
size="xs"
|
|
94
96
|
color="neutral"
|
|
@@ -107,6 +109,7 @@
|
|
|
107
109
|
@click="copy(item)"
|
|
108
110
|
/>
|
|
109
111
|
<CmsButton
|
|
112
|
+
v-if="!readOnly"
|
|
110
113
|
icon="trash"
|
|
111
114
|
size="xs"
|
|
112
115
|
color="error"
|
|
@@ -144,11 +147,12 @@
|
|
|
144
147
|
<CmsEmptyState
|
|
145
148
|
v-else
|
|
146
149
|
icon="photo"
|
|
147
|
-
title="No media yet"
|
|
148
|
-
body="Files you upload will show up here."
|
|
150
|
+
:title="readOnly ? 'No media' : 'No media yet'"
|
|
151
|
+
:body="readOnly ? 'No media registered.' : 'Files you upload will show up here.'"
|
|
149
152
|
/>
|
|
150
153
|
|
|
151
154
|
<CmsModal
|
|
155
|
+
v-if="!readOnly"
|
|
152
156
|
:open="!!editing"
|
|
153
157
|
:title="editing ? mediaFilename(editing.key) : ''"
|
|
154
158
|
:ui="CMS_MODAL_UI"
|
|
@@ -184,6 +188,7 @@
|
|
|
184
188
|
import { computed, onMounted, ref } from "#imports";
|
|
185
189
|
import { MEDIA_TYPES, mediaFilename, mediaIconFor } from "#nuxt-cms";
|
|
186
190
|
import { useCmsConfirm } from "../../composables/cms-confirm";
|
|
191
|
+
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
187
192
|
import { useCmsToast } from "../../composables/cms-toast";
|
|
188
193
|
import { CMS_MODAL_UI, errorMessage } from "../../utils/ui";
|
|
189
194
|
const filterLabels = {
|
|
@@ -199,6 +204,8 @@ const props = defineProps({
|
|
|
199
204
|
});
|
|
200
205
|
const emit = defineEmits(["select"]);
|
|
201
206
|
const toast = useCmsToast();
|
|
207
|
+
const runtime = useCmsRuntime();
|
|
208
|
+
const readOnly = computed(() => runtime.mediaStorage === "local");
|
|
202
209
|
const endpoint = "/api/cms/admin/media";
|
|
203
210
|
const items = ref([]);
|
|
204
211
|
const loading = ref(true);
|
|
@@ -3,7 +3,12 @@ import { z } from "zod";
|
|
|
3
3
|
import { useDb } from "#cms-db";
|
|
4
4
|
import { cms_media } from "#cms-tables";
|
|
5
5
|
import { objectKeySchema } from "../../shared/validation.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
assertMediaWritable,
|
|
8
|
+
assertUploadContentType,
|
|
9
|
+
toMediaItem,
|
|
10
|
+
useMediaConfig
|
|
11
|
+
} from "../utils/media.js";
|
|
7
12
|
import { requireAdmin } from "../utils/require-admin.js";
|
|
8
13
|
const bodySchema = z.object({
|
|
9
14
|
key: objectKeySchema,
|
|
@@ -16,7 +21,8 @@ const bodySchema = z.object({
|
|
|
16
21
|
});
|
|
17
22
|
export default defineEventHandler(async (event) => {
|
|
18
23
|
await requireAdmin(event);
|
|
19
|
-
const { publicUrl } = useMediaConfig(event);
|
|
24
|
+
const { media, publicUrl } = useMediaConfig(event);
|
|
25
|
+
assertMediaWritable(media);
|
|
20
26
|
const body = await readValidatedBody(event, bodySchema.parse);
|
|
21
27
|
if (body.mime) assertUploadContentType(body.mime);
|
|
22
28
|
const values = {
|
|
@@ -3,7 +3,7 @@ import { createError, defineEventHandler, readValidatedBody } from "h3";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { useDb } from "#cms-db";
|
|
5
5
|
import { cms_media } from "#cms-tables";
|
|
6
|
-
import { toMediaItem, useMediaConfig } from "../utils/media.js";
|
|
6
|
+
import { assertMediaWritable, toMediaItem, useMediaConfig } from "../utils/media.js";
|
|
7
7
|
import { requireAdmin } from "../utils/require-admin.js";
|
|
8
8
|
const bodySchema = z.object({
|
|
9
9
|
id: z.number().int().positive(),
|
|
@@ -12,7 +12,8 @@ const bodySchema = z.object({
|
|
|
12
12
|
});
|
|
13
13
|
export default defineEventHandler(async (event) => {
|
|
14
14
|
await requireAdmin(event);
|
|
15
|
-
const { publicUrl } = useMediaConfig(event);
|
|
15
|
+
const { media, publicUrl } = useMediaConfig(event);
|
|
16
|
+
assertMediaWritable(media);
|
|
16
17
|
const body = await readValidatedBody(event, bodySchema.parse);
|
|
17
18
|
const updates = {};
|
|
18
19
|
if (body.alt !== void 0) updates.alt = body.alt;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { H3Event } from 'h3';
|
|
2
2
|
import { AwsClient } from 'aws4fetch';
|
|
3
|
-
import type { MediaItem } from '../../shared/index.js';
|
|
3
|
+
import type { MediaItem, MediaStorageMode } from '../../shared/index.js';
|
|
4
4
|
interface MediaConfig {
|
|
5
|
+
storage: MediaStorageMode;
|
|
5
6
|
endpoint: string;
|
|
6
7
|
region: string;
|
|
7
8
|
bucket: string;
|
|
@@ -11,6 +12,8 @@ interface MediaConfig {
|
|
|
11
12
|
}
|
|
12
13
|
export declare function assertUploadContentType(contentType: string): void;
|
|
13
14
|
export declare function encodeKey(key: string): string;
|
|
15
|
+
export declare function assertMediaConfigured(media: MediaConfig): void;
|
|
16
|
+
export declare function assertMediaWritable(media: MediaConfig): void;
|
|
14
17
|
export declare function useMediaConfig(event: H3Event): {
|
|
15
18
|
media: MediaConfig;
|
|
16
19
|
publicUrl: (key: string) => string | null;
|
|
@@ -32,21 +32,34 @@ export function assertUploadContentType(contentType) {
|
|
|
32
32
|
export function encodeKey(key) {
|
|
33
33
|
return key.split("/").map(encodeURIComponent).join("/");
|
|
34
34
|
}
|
|
35
|
-
export function
|
|
36
|
-
|
|
37
|
-
const media = config.cms.media;
|
|
35
|
+
export function assertMediaConfigured(media) {
|
|
36
|
+
if (media.storage !== "s3") return;
|
|
38
37
|
if (!media.endpoint || !media.bucket || !media.accessKeyId || !media.secretAccessKey) {
|
|
39
38
|
throw createError({
|
|
40
39
|
statusCode: 501,
|
|
41
40
|
statusMessage: "Media storage is not configured (cms.media in nuxt.config)"
|
|
42
41
|
});
|
|
43
42
|
}
|
|
43
|
+
}
|
|
44
|
+
export function assertMediaWritable(media) {
|
|
45
|
+
if (media.storage === "local") {
|
|
46
|
+
throw createError({
|
|
47
|
+
statusCode: 501,
|
|
48
|
+
statusMessage: "Media storage is local; the media library is read-only"
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function useMediaConfig(event) {
|
|
53
|
+
const config = useRuntimeConfig(event);
|
|
54
|
+
const media = config.cms.media;
|
|
55
|
+
assertMediaConfigured(media);
|
|
44
56
|
const { mediaBaseUrl } = config.public.cms;
|
|
45
57
|
const publicUrl = (key) => mediaPublicUrl(mediaBaseUrl, key);
|
|
46
58
|
return { media, publicUrl };
|
|
47
59
|
}
|
|
48
60
|
export function useMediaStorage(event) {
|
|
49
61
|
const { media, publicUrl } = useMediaConfig(event);
|
|
62
|
+
assertMediaWritable(media);
|
|
50
63
|
const client = new AwsClient({
|
|
51
64
|
accessKeyId: media.accessKeyId,
|
|
52
65
|
secretAccessKey: media.secretAccessKey,
|
|
@@ -6,6 +6,7 @@ export interface CmsI18n {
|
|
|
6
6
|
locales: string[];
|
|
7
7
|
defaultLocale: string;
|
|
8
8
|
}
|
|
9
|
+
export type MediaStorageMode = 's3' | 'local';
|
|
9
10
|
export declare const MEDIA_TYPES: readonly ["image", "video", "file"];
|
|
10
11
|
export type MediaType = (typeof MEDIA_TYPES)[number];
|
|
11
12
|
export declare function mediaTypeForKey(key: string): MediaType;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xleddyl/nuxt-cms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
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)",
|