mktcms 0.1.6

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 (38) hide show
  1. package/README.md +82 -0
  2. package/dist/module.d.mts +5 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +60 -0
  5. package/dist/runtime/app/components/admin.d.vue.ts +13 -0
  6. package/dist/runtime/app/components/admin.vue +9 -0
  7. package/dist/runtime/app/components/admin.vue.d.ts +13 -0
  8. package/dist/runtime/app/components/content/index.d.vue.ts +3 -0
  9. package/dist/runtime/app/components/content/index.vue +89 -0
  10. package/dist/runtime/app/components/content/index.vue.d.ts +3 -0
  11. package/dist/runtime/app/components/content/upload.d.vue.ts +3 -0
  12. package/dist/runtime/app/components/content/upload.vue +32 -0
  13. package/dist/runtime/app/components/content/upload.vue.d.ts +3 -0
  14. package/dist/runtime/app/composables/useAdminUpload.d.ts +9 -0
  15. package/dist/runtime/app/composables/useAdminUpload.js +61 -0
  16. package/dist/runtime/app/pages/admin/index.d.vue.ts +3 -0
  17. package/dist/runtime/app/pages/admin/index.vue +22 -0
  18. package/dist/runtime/app/pages/admin/index.vue.d.ts +3 -0
  19. package/dist/runtime/app/pages/admin/login.d.vue.ts +3 -0
  20. package/dist/runtime/app/pages/admin/login.vue +42 -0
  21. package/dist/runtime/app/pages/admin/login.vue.d.ts +3 -0
  22. package/dist/runtime/server/api/admin/content/upload.d.ts +5 -0
  23. package/dist/runtime/server/api/admin/content/upload.js +46 -0
  24. package/dist/runtime/server/api/admin/login.d.ts +4 -0
  25. package/dist/runtime/server/api/admin/login.js +18 -0
  26. package/dist/runtime/server/api/admin/logout.d.ts +2 -0
  27. package/dist/runtime/server/api/admin/logout.js +6 -0
  28. package/dist/runtime/server/api/content/[path].d.ts +2 -0
  29. package/dist/runtime/server/api/content/[path].js +18 -0
  30. package/dist/runtime/server/api/content/list.d.ts +2 -0
  31. package/dist/runtime/server/api/content/list.js +13 -0
  32. package/dist/runtime/server/middleware/auth.d.ts +2 -0
  33. package/dist/runtime/server/middleware/auth.js +18 -0
  34. package/dist/runtime/server/plugins/storage.d.ts +2 -0
  35. package/dist/runtime/server/plugins/storage.js +21 -0
  36. package/dist/runtime/server/tsconfig.json +3 -0
  37. package/dist/types.d.mts +7 -0
  38. package/package.json +57 -0
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ <!--
2
+ Get your module up and running quickly.
3
+
4
+ Find and replace all on all files (CMD+SHIFT+F):
5
+ - Name: My Module
6
+ - Package name: mktcms
7
+ - Description: My new Nuxt module
8
+ -->
9
+
10
+ # Simple CMS module for Nuxt
11
+
12
+ [![npm version][npm-version-src]][npm-version-href]
13
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
14
+ [![License][license-src]][license-href]
15
+ [![Nuxt][nuxt-src]][nuxt-href]
16
+
17
+ - [✨ &nbsp;Release Notes](/CHANGELOG.md)
18
+ <!-- - [🏀 Online playground](https://stackblitz.com/github/mktcode/mktcms?file=playground%2Fapp.vue) -->
19
+ <!-- - [📖 &nbsp;Documentation](https://example.com) -->
20
+
21
+ ## Features
22
+
23
+ <!-- Highlight some of the features your module provide here -->
24
+ - ⛰ &nbsp;Foo
25
+ - 🚠 &nbsp;Bar
26
+ - 🌲 &nbsp;Baz
27
+
28
+ ## Quick Setup
29
+
30
+ Install the module to your Nuxt application with one command:
31
+
32
+ ```bash
33
+ npx nuxi module add mktcms
34
+ ```
35
+
36
+ That's it! You can now use My Module in your Nuxt app ✨
37
+
38
+
39
+ ## Contribution
40
+
41
+ <details>
42
+ <summary>Local development</summary>
43
+
44
+ ```bash
45
+ # Install dependencies
46
+ npm install
47
+
48
+ # Generate type stubs
49
+ npm run dev:prepare
50
+
51
+ # Develop with the playground
52
+ npm run dev
53
+
54
+ # Build the playground
55
+ npm run dev:build
56
+
57
+ # Run ESLint
58
+ npm run lint
59
+
60
+ # Run Vitest
61
+ npm run test
62
+ npm run test:watch
63
+
64
+ # Release new version
65
+ npm run release
66
+ ```
67
+
68
+ </details>
69
+
70
+
71
+ <!-- Badges -->
72
+ [npm-version-src]: https://img.shields.io/npm/v/mktcms/latest.svg?style=flat&colorA=020420&colorB=00DC82
73
+ [npm-version-href]: https://npmjs.com/package/mktcms
74
+
75
+ [npm-downloads-src]: https://img.shields.io/npm/dm/mktcms.svg?style=flat&colorA=020420&colorB=00DC82
76
+ [npm-downloads-href]: https://npm.chart.dev/mktcms
77
+
78
+ [license-src]: https://img.shields.io/npm/l/mktcms.svg?style=flat&colorA=020420&colorB=00DC82
79
+ [license-href]: https://npmjs.com/package/mktcms
80
+
81
+ [nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt
82
+ [nuxt-href]: https://nuxt.com
@@ -0,0 +1,5 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ declare const _default: _nuxt_schema.NuxtModule<_nuxt_schema.ModuleOptions, _nuxt_schema.ModuleOptions, false>;
4
+
5
+ export { _default as default };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "mktcms",
3
+ "configKey": "mktcms",
4
+ "version": "0.1.6",
5
+ "builder": {
6
+ "@nuxt/module-builder": "1.0.2",
7
+ "unbuild": "3.6.1"
8
+ }
9
+ }
@@ -0,0 +1,60 @@
1
+ import { defineNuxtModule, createResolver, addServerPlugin, addServerHandler, extendPages } from '@nuxt/kit';
2
+ import defu from 'defu';
3
+
4
+ const module$1 = defineNuxtModule({
5
+ meta: {
6
+ name: "mktcms",
7
+ configKey: "mktcms"
8
+ },
9
+ setup(_options, _nuxt) {
10
+ const resolver = createResolver(import.meta.url);
11
+ _nuxt.options.runtimeConfig.mktcms = defu((_nuxt.options.runtimeConfig.mktcms, {
12
+ adminAuthKey: "",
13
+ filesPathPrefix: "",
14
+ s3AccessKey: "",
15
+ s3SecretKey: "",
16
+ s3Endpoint: "",
17
+ s3Bucket: "",
18
+ s3Region: ""
19
+ }));
20
+ addServerPlugin(resolver.resolve("./runtime/server/plugins/storage"));
21
+ addServerHandler({
22
+ middleware: true,
23
+ handler: resolver.resolve("./runtime/server/middleware/auth")
24
+ });
25
+ addServerHandler({
26
+ route: "/api/admin/login",
27
+ handler: resolver.resolve("./runtime/server/api/admin/login")
28
+ });
29
+ addServerHandler({
30
+ route: "/api/admin/logout",
31
+ handler: resolver.resolve("./runtime/server/api/admin/logout")
32
+ });
33
+ addServerHandler({
34
+ route: "/api/admin/content/upload",
35
+ handler: resolver.resolve("./runtime/server/api/admin/content/upload")
36
+ });
37
+ addServerHandler({
38
+ route: "/api/content/list",
39
+ handler: resolver.resolve("./runtime/server/api/content/list")
40
+ });
41
+ addServerHandler({
42
+ route: "/api/content/[path]",
43
+ handler: resolver.resolve("./runtime/server/api/content/[path]")
44
+ });
45
+ extendPages((pages) => {
46
+ pages.push({
47
+ name: "Admin Dashboard",
48
+ path: "/admin/:path(.*)?",
49
+ file: resolver.resolve("./runtime/app/pages/admin/index.vue")
50
+ });
51
+ pages.push({
52
+ name: "Admin Login",
53
+ path: "/admin/login",
54
+ file: resolver.resolve("./runtime/app/pages/admin/login.vue")
55
+ });
56
+ });
57
+ }
58
+ });
59
+
60
+ export { module$1 as default };
@@ -0,0 +1,13 @@
1
+ declare var __VLS_1: {};
2
+ type __VLS_Slots = {} & {
3
+ default?: (props: typeof __VLS_1) => any;
4
+ };
5
+ declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
9
+ type __VLS_WithSlots<T, S> = T & {
10
+ new (): {
11
+ $slots: S;
12
+ };
13
+ };
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <div id="mktcms-admin">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <style>
8
+ body{font-family:Arial,sans-serif;margin:0}#mktcms-admin{background-color:#f9f9f9;margin:0 auto;max-width:800px;padding:20px}#mktcms-admin h1{color:#333}#mktcms-admin button{background-color:#3cb371;border:none;border-radius:5px;color:#fff;cursor:pointer;font-size:16px;padding:10px 20px;transition:background-color .3s}#mktcms-admin button:hover{background-color:#45a049}#mktcms-admin input[type=email],#mktcms-admin input[type=password],#mktcms-admin input[type=text],#mktcms-admin textarea{border:1px solid #ccc;border-radius:5px;font-size:16px;padding:10px}#mktcms-admin .breadcrumbs{color:#888;font-size:1.5rem;margin:20px 0}#mktcms-admin .breadcrumbs a{color:#888;text-decoration:none}#mktcms-admin .breadcrumbs a:hover{text-decoration:underline}#mktcms-admin .dirs,#mktcms-admin .files{display:flex;flex-direction:column;gap:8px}#mktcms-admin .dirs a,#mktcms-admin .files a{border-radius:4px;display:block;padding:8px 12px;text-decoration:none}#mktcms-admin .dirs a:hover,#mktcms-admin .files a:hover{text-decoration:underline}#mktcms-admin .files a{background-color:#fff;color:#555}#mktcms-admin .dirs a{background-color:#555;color:#fff;display:flex;justify-content:space-between}
9
+ </style>
@@ -0,0 +1,13 @@
1
+ declare var __VLS_1: {};
2
+ type __VLS_Slots = {} & {
3
+ default?: (props: typeof __VLS_1) => any;
4
+ };
5
+ declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
9
+ type __VLS_WithSlots<T, S> = T & {
10
+ new (): {
11
+ $slots: S;
12
+ };
13
+ };
@@ -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,89 @@
1
+ <script setup>
2
+ import { useFetch, useRoute } from "#app";
3
+ import { computed } from "vue";
4
+ const path = useRoute().params.path || "";
5
+ const pathParts = path.split("/");
6
+ const { data: keys } = await useFetch("/api/content/list", {
7
+ query: { path }
8
+ });
9
+ const keysWithoutCurrentPath = computed(() => {
10
+ return keys.value?.map(
11
+ (key) => key.replace(new RegExp("^" + pathParts.join(":") + ":"), "")
12
+ ) || [];
13
+ });
14
+ const files = computed(() => {
15
+ return keysWithoutCurrentPath.value.filter((key) => !key.includes(":"));
16
+ });
17
+ const dirs = computed(() => {
18
+ return keysWithoutCurrentPath.value.reduce((acc, key) => {
19
+ const parts = key.split(":");
20
+ if (parts.length > 1 && parts[0]) {
21
+ const dir = parts[0];
22
+ if (!acc.includes(dir)) {
23
+ acc.push(dir);
24
+ }
25
+ }
26
+ return acc;
27
+ }, []);
28
+ });
29
+ </script>
30
+
31
+ <template>
32
+ <div>
33
+ <div class="breadcrumbs">
34
+ <a href="/admin">Hauptverzeichnis</a>
35
+ <span
36
+ v-for="(part, index) in pathParts"
37
+ :key="index"
38
+ >
39
+ /
40
+ <a :href="`/admin/${pathParts.slice(0, index + 1).join('/')}`">
41
+ {{ part }}
42
+ </a>
43
+ </span>
44
+ </div>
45
+
46
+ <div
47
+ v-if="files.length"
48
+ class="files"
49
+ >
50
+ <a
51
+ v-for="file in files"
52
+ :key="file"
53
+ :href="`/admin/${path ? path + '/' : ''}${file}`"
54
+ >{{ file }}</a>
55
+ </div>
56
+
57
+ <div
58
+ v-if="dirs.length"
59
+ class="dirs"
60
+ style="margin-top: 8px;"
61
+ >
62
+ <a
63
+ v-for="dir in dirs"
64
+ :key="dir"
65
+ :href="`/admin/${path ? path + '/' : ''}${dir}`"
66
+ >
67
+ <span>
68
+ {{ dir.replace(/:/g, "/").replace(path, "") }}
69
+ </span>
70
+ <span>
71
+ <svg
72
+ xmlns="http://www.w3.org/2000/svg"
73
+ fill="none"
74
+ viewBox="0 0 24 24"
75
+ stroke-width="1.5"
76
+ stroke="currentColor"
77
+ style="width: 16px; height: 16px; vertical-align: middle;"
78
+ >
79
+ <path
80
+ stroke-linecap="round"
81
+ stroke-linejoin="round"
82
+ d="m8.25 4.5 7.5 7.5-7.5 7.5"
83
+ />
84
+ </svg>
85
+ </span>
86
+ </a>
87
+ </div>
88
+ </div>
89
+ </template>
@@ -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,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,32 @@
1
+ <script setup>
2
+ import useAdminUpload from "../../composables/useAdminUpload";
3
+ const { isUploading, fileInput, path, uploadFiles } = useAdminUpload();
4
+ </script>
5
+
6
+ <template>
7
+ <div style="display: flex; gap: 8px; margin: 16px 0;">
8
+ <input
9
+ v-model="path"
10
+ type="text"
11
+ placeholder="Unterordner (z.B. 'Produkte')"
12
+ >
13
+ <button :disabled="isUploading">
14
+ Neuer Inhalt
15
+ </button>
16
+ <button
17
+ :disabled="isUploading"
18
+ @click="fileInput?.click()"
19
+ >
20
+ Bild/Dokument hochladen
21
+ </button>
22
+ <input
23
+ ref="fileInput"
24
+ style="display: none"
25
+ type="file"
26
+ accept=".pdf,.jpg,.jpeg,.png,.gif,.svg,.webp,.md,.docx,.txt"
27
+ @change="async (e) => {
28
+ await uploadFiles(e);
29
+ }"
30
+ >
31
+ </div>
32
+ </template>
@@ -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,9 @@
1
+ export default function useAdminUpload(): {
2
+ uploadError: import("vue").Ref<string | null, string | null>;
3
+ isUploading: import("vue").Ref<boolean, boolean>;
4
+ path: import("vue").Ref<string | null, string | null>;
5
+ files: import("vue").Ref<string[], string[]>;
6
+ fileInput: import("vue").Ref<HTMLInputElement | null, HTMLInputElement | null>;
7
+ uploadFiles: (event: Event) => Promise<void>;
8
+ deleteFile: (path: string) => Promise<void>;
9
+ };
@@ -0,0 +1,61 @@
1
+ import { computed, ref } from "vue";
2
+ export default function useAdminUpload() {
3
+ const uploadError = ref(null);
4
+ const files = ref([]);
5
+ const fileInput = ref(null);
6
+ const isUploading = ref(false);
7
+ const path = ref(null);
8
+ const sanePath = computed(() => {
9
+ return path.value ? path.value.replace(/^\//, "").replace(/\/$/, "") : void 0;
10
+ });
11
+ async function uploadFiles(event) {
12
+ if (isUploading.value) return;
13
+ isUploading.value = true;
14
+ uploadError.value = null;
15
+ const input = event.target;
16
+ if (!input.files) {
17
+ return;
18
+ }
19
+ const file = input.files[0];
20
+ if (!file) {
21
+ return;
22
+ }
23
+ const formData = new FormData();
24
+ formData.append("file", file);
25
+ try {
26
+ const res = await $fetch("/api/admin/content/upload", {
27
+ method: "POST",
28
+ body: formData,
29
+ query: { path: sanePath.value }
30
+ });
31
+ if (res?.success && res.path) {
32
+ files.value.push(res.path);
33
+ }
34
+ } catch (error) {
35
+ uploadError.value = error.data?.statusMessage || "Fehler beim Hochladen der Datei";
36
+ input.value = "";
37
+ }
38
+ isUploading.value = false;
39
+ }
40
+ async function deleteFile(path2) {
41
+ uploadError.value = null;
42
+ try {
43
+ await $fetch("/api/content/remove", {
44
+ method: "DELETE",
45
+ body: { path: path2 }
46
+ });
47
+ files.value = files.value.filter((p) => p !== path2);
48
+ } catch (error) {
49
+ uploadError.value = error.data?.statusMessage || "Fehler beim L\xF6schen der Datei";
50
+ }
51
+ }
52
+ return {
53
+ uploadError,
54
+ isUploading,
55
+ path,
56
+ files,
57
+ fileInput,
58
+ uploadFiles,
59
+ deleteFile
60
+ };
61
+ }
@@ -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,22 @@
1
+ <script setup>
2
+ import Admin from "../../components/admin.vue";
3
+ import Content from "../../components/content/index.vue";
4
+ import ContentUpload from "../../components/content/upload.vue";
5
+ </script>
6
+
7
+ <template>
8
+ <Admin>
9
+ <div style="display: flex; justify-content: space-between; align-items: center;">
10
+ <h1>Website Verwaltung</h1>
11
+ <a
12
+ href="/api/admin/logout"
13
+ style="background-color: #ccc; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;"
14
+ >
15
+ Abmelden
16
+ </a>
17
+ </div>
18
+
19
+ <ContentUpload />
20
+ <Content />
21
+ </Admin>
22
+ </template>
@@ -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,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,42 @@
1
+ <script setup>
2
+ import { ref } from "vue";
3
+ import Admin from "../../components/admin.vue";
4
+ import { navigateTo } from "#app";
5
+ const adminAuthKey = ref("");
6
+ async function login() {
7
+ await $fetch("/api/admin/login", {
8
+ method: "POST",
9
+ body: {
10
+ adminAuthKey: adminAuthKey.value
11
+ }
12
+ });
13
+ await navigateTo("/admin");
14
+ }
15
+ </script>
16
+
17
+ <template>
18
+ <Admin>
19
+ <h1>Admin Login</h1>
20
+ <div>
21
+ <label
22
+ for="authKey"
23
+ class="text-gray-700"
24
+ >
25
+ Schlüssel:
26
+ </label>
27
+ <input
28
+ id="adminAuthKey"
29
+ v-model="adminAuthKey"
30
+ type="password"
31
+ class="border border-gray-300 rounded-md p-2 mt-2"
32
+ @keyup.enter="login"
33
+ >
34
+ <button
35
+ class="bg-brand text-white px-4 py-2 rounded-md hover:bg-brand/90 cursor-pointer"
36
+ @click="login"
37
+ >
38
+ Anmelden
39
+ </button>
40
+ </div>
41
+ </Admin>
42
+ </template>
@@ -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,5 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ success: boolean;
3
+ path: string;
4
+ }>>;
5
+ export default _default;
@@ -0,0 +1,46 @@
1
+ import z from "zod";
2
+ import { createError, defineEventHandler, getValidatedQuery, readMultipartFormData } from "h3";
3
+ import { useRuntimeConfig, useStorage } from "nitropack/runtime";
4
+ function sanitizeFilename(filename) {
5
+ return filename.replace(/[^\w.-]/g, "_");
6
+ }
7
+ const querySchema = z.object({
8
+ path: z.string().optional()
9
+ });
10
+ export default defineEventHandler(async (event) => {
11
+ const form = await readMultipartFormData(event);
12
+ const { mktcms: { filesPathPrefix } } = useRuntimeConfig();
13
+ const { path } = await getValidatedQuery(event, (query) => querySchema.parse(query));
14
+ const sanePath = path ? path.replace(/^\//, "").replace(/\/$/, "") : void 0;
15
+ if (!form) {
16
+ throw createError({
17
+ statusCode: 400,
18
+ statusMessage: "No form data received"
19
+ });
20
+ }
21
+ const file = form.find((item) => item.name === "file");
22
+ if (!file) {
23
+ throw createError({
24
+ statusCode: 400,
25
+ statusMessage: "Missing file"
26
+ });
27
+ }
28
+ if (!file.filename || !file.data) {
29
+ throw createError({
30
+ statusCode: 400,
31
+ statusMessage: "Invalid file upload"
32
+ });
33
+ }
34
+ const allowedExtensions = [".pdf", ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".md", ".docx", ".txt"];
35
+ const fileExtension = file.filename.toLowerCase().slice(file.filename.lastIndexOf("."));
36
+ if (!allowedExtensions.includes(fileExtension)) {
37
+ throw createError({
38
+ statusCode: 400,
39
+ statusMessage: "Invalid file type. Only PDF, JPG, JPEG, and PNG files are allowed."
40
+ });
41
+ }
42
+ const filePath = [filesPathPrefix, sanePath, sanitizeFilename(file.filename)].filter(Boolean).join("/");
43
+ await useStorage("content").setItemRaw(filePath, Buffer.from(file.data));
44
+ const returnFileName = filePath;
45
+ return { success: true, path: returnFileName };
46
+ });
@@ -0,0 +1,4 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ message: string;
3
+ }>>;
4
+ export default _default;
@@ -0,0 +1,18 @@
1
+ import { useRuntimeConfig } from "nitropack/runtime";
2
+ import { createError, defineEventHandler, readValidatedBody, setCookie } from "h3";
3
+ import z from "zod";
4
+ const bodySchema = z.object({
5
+ adminAuthKey: z.string()
6
+ });
7
+ export default defineEventHandler(async (event) => {
8
+ const { mktcms: { adminAuthKey } } = useRuntimeConfig();
9
+ const body = await readValidatedBody(event, (body2) => bodySchema.parse(body2));
10
+ if (body.adminAuthKey !== adminAuthKey.toString()) {
11
+ throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
12
+ }
13
+ setCookie(event, "mktcms_admin_auth_key", adminAuthKey.toString(), {
14
+ httpOnly: true,
15
+ maxAge: 7 * 24 * 60 * 60
16
+ });
17
+ return { message: "Login successful" };
18
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
2
+ export default _default;
@@ -0,0 +1,6 @@
1
+ import { defineEventHandler, deleteCookie } from "h3";
2
+ export default defineEventHandler(async (event) => {
3
+ deleteCookie(event, "mktcms_admin_auth_key");
4
+ event.node.res.writeHead(302, { Location: "/admin/login" });
5
+ event.node.res.end();
6
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string | number | true | object>>;
2
+ export default _default;
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import { createError, defineEventHandler, getValidatedRouterParams } from "h3";
3
+ import { useStorage } from "nitropack/runtime";
4
+ const paramsSchema = z.object({
5
+ path: z.string().min(1)
6
+ });
7
+ export default defineEventHandler(async (event) => {
8
+ const { path } = await getValidatedRouterParams(event, (params) => paramsSchema.parse(params));
9
+ const storage = useStorage("content");
10
+ const file = await storage.getItem(path);
11
+ if (!file) {
12
+ throw createError({
13
+ statusCode: 404,
14
+ statusMessage: "File not found"
15
+ });
16
+ }
17
+ return file;
18
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string[]>>;
2
+ export default _default;
@@ -0,0 +1,13 @@
1
+ import z from "zod";
2
+ import { useStorage, useRuntimeConfig } from "nitropack/runtime";
3
+ import { defineEventHandler, getValidatedQuery } from "h3";
4
+ const querySchema = z.object({
5
+ path: z.string().optional()
6
+ });
7
+ export default defineEventHandler(async (event) => {
8
+ const { path } = await getValidatedQuery(event, (query) => querySchema.parse(query));
9
+ const { mktcms: { filesPathPrefix } } = useRuntimeConfig();
10
+ const storage = useStorage("content");
11
+ const keys = await storage.getKeys(filesPathPrefix + (path ? ":" + path : ""));
12
+ return keys.map((key) => key.replace(filesPathPrefix + ":", ""));
13
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
2
+ export default _default;
@@ -0,0 +1,18 @@
1
+ import { useRuntimeConfig } from "nitropack/runtime";
2
+ import { createError, defineEventHandler, getCookie, getRequestURL, sendRedirect } from "h3";
3
+ export default defineEventHandler(async (event) => {
4
+ const pathname = getRequestURL(event).pathname;
5
+ const isAdminLoginRoute = pathname === "/admin/login" || pathname === "/api/admin/login";
6
+ const isAdminRoute = pathname === "/admin" || pathname.startsWith("/admin/");
7
+ const isAdminApiRoute = pathname === "/api/admin" || pathname.startsWith("/api/admin/");
8
+ if (isAdminLoginRoute || !isAdminRoute && !isAdminApiRoute) return;
9
+ const { mktcms: { adminAuthKey } } = useRuntimeConfig();
10
+ const authKeyCookie = getCookie(event, "mktcms_admin_auth_key");
11
+ if (!authKeyCookie || authKeyCookie !== adminAuthKey.toString() || adminAuthKey === "") {
12
+ if (isAdminApiRoute) {
13
+ throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
14
+ } else {
15
+ return sendRedirect(event, "/admin/login");
16
+ }
17
+ }
18
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("nitropack").NitroAppPlugin;
2
+ export default _default;
@@ -0,0 +1,21 @@
1
+ import createS3Driver from "unstorage/drivers/s3";
2
+ import createFsDriver from "unstorage/drivers/fs";
3
+ import { defineNitroPlugin, useStorage, useRuntimeConfig } from "nitropack/runtime";
4
+ export default defineNitroPlugin(() => {
5
+ const storage = useStorage();
6
+ const s3Driver = createS3Driver({
7
+ accessKeyId: useRuntimeConfig().mktcms.s3AccessKey,
8
+ secretAccessKey: useRuntimeConfig().mktcms.s3SecretKey,
9
+ endpoint: useRuntimeConfig().mktcms.s3Endpoint,
10
+ bucket: useRuntimeConfig().mktcms.s3Bucket,
11
+ region: useRuntimeConfig().mktcms.s3Region
12
+ });
13
+ const fsDriver = createFsDriver({
14
+ base: "./.storage"
15
+ });
16
+ if (process.env.NODE_ENV === "production") {
17
+ storage.mount("content", s3Driver);
18
+ } else {
19
+ storage.mount("content", fsDriver);
20
+ }
21
+ });
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../../.nuxt/tsconfig.server.json",
3
+ }
@@ -0,0 +1,7 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module.mjs'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { default } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "mktcms",
3
+ "version": "0.1.6",
4
+ "description": "Simple CMS module for Nuxt",
5
+ "repository": "mktcode/mktcms",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/types.d.mts",
11
+ "import": "./dist/module.mjs"
12
+ }
13
+ },
14
+ "main": "./dist/module.mjs",
15
+ "typesVersions": {
16
+ "*": {
17
+ ".": [
18
+ "./dist/types.d.mts"
19
+ ]
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "prepack": "nuxt-module-build build",
27
+ "dev": "npm run dev:prepare && nuxi dev playground",
28
+ "dev:build": "nuxi build playground",
29
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
30
+ "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
31
+ "lint": "eslint .",
32
+ "lint:fix": "eslint . --fix",
33
+ "test": "vitest run",
34
+ "test:watch": "vitest watch",
35
+ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
36
+ },
37
+ "dependencies": {
38
+ "@nuxt/kit": "^4.2.2",
39
+ "aws4fetch": "^1.0.20",
40
+ "defu": "^6.1.4",
41
+ "zod": "^4.3.5"
42
+ },
43
+ "devDependencies": {
44
+ "@nuxt/devtools": "^3.1.1",
45
+ "@nuxt/eslint-config": "^1.12.1",
46
+ "@nuxt/module-builder": "^1.0.2",
47
+ "@nuxt/schema": "^4.2.2",
48
+ "@nuxt/test-utils": "^3.22.0",
49
+ "@types/node": "latest",
50
+ "changelogen": "^0.6.2",
51
+ "eslint": "^9.39.2",
52
+ "nuxt": "^4.2.2",
53
+ "typescript": "~5.9.3",
54
+ "vitest": "^3.2.4",
55
+ "vue-tsc": "^3.2.2"
56
+ }
57
+ }