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.
- package/README.md +82 -0
- package/dist/module.d.mts +5 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +60 -0
- package/dist/runtime/app/components/admin.d.vue.ts +13 -0
- package/dist/runtime/app/components/admin.vue +9 -0
- package/dist/runtime/app/components/admin.vue.d.ts +13 -0
- package/dist/runtime/app/components/content/index.d.vue.ts +3 -0
- package/dist/runtime/app/components/content/index.vue +89 -0
- package/dist/runtime/app/components/content/index.vue.d.ts +3 -0
- package/dist/runtime/app/components/content/upload.d.vue.ts +3 -0
- package/dist/runtime/app/components/content/upload.vue +32 -0
- package/dist/runtime/app/components/content/upload.vue.d.ts +3 -0
- package/dist/runtime/app/composables/useAdminUpload.d.ts +9 -0
- package/dist/runtime/app/composables/useAdminUpload.js +61 -0
- package/dist/runtime/app/pages/admin/index.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin/index.vue +22 -0
- package/dist/runtime/app/pages/admin/index.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin/login.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin/login.vue +42 -0
- package/dist/runtime/app/pages/admin/login.vue.d.ts +3 -0
- package/dist/runtime/server/api/admin/content/upload.d.ts +5 -0
- package/dist/runtime/server/api/admin/content/upload.js +46 -0
- package/dist/runtime/server/api/admin/login.d.ts +4 -0
- package/dist/runtime/server/api/admin/login.js +18 -0
- package/dist/runtime/server/api/admin/logout.d.ts +2 -0
- package/dist/runtime/server/api/admin/logout.js +6 -0
- package/dist/runtime/server/api/content/[path].d.ts +2 -0
- package/dist/runtime/server/api/content/[path].js +18 -0
- package/dist/runtime/server/api/content/list.d.ts +2 -0
- package/dist/runtime/server/api/content/list.js +13 -0
- package/dist/runtime/server/middleware/auth.d.ts +2 -0
- package/dist/runtime/server/middleware/auth.js +18 -0
- package/dist/runtime/server/plugins/storage.d.ts +2 -0
- package/dist/runtime/server/plugins/storage.js +21 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/types.d.mts +7 -0
- 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
|
+
- [✨ Release Notes](/CHANGELOG.md)
|
|
18
|
+
<!-- - [🏀 Online playground](https://stackblitz.com/github/mktcode/mktcms?file=playground%2Fapp.vue) -->
|
|
19
|
+
<!-- - [📖 Documentation](https://example.com) -->
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
<!-- Highlight some of the features your module provide here -->
|
|
24
|
+
- ⛰ Foo
|
|
25
|
+
- 🚠 Bar
|
|
26
|
+
- 🌲 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
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -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,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,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,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,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,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,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
|
+
});
|
package/dist/types.d.mts
ADDED
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
|
+
}
|