mktcms 0.2.13 → 0.2.14

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 (48) hide show
  1. package/README.md +5 -3
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +8 -5
  4. package/dist/runtime/app/components/content/index.vue +5 -17
  5. package/dist/runtime/app/components/content/upload.vue +7 -3
  6. package/dist/runtime/app/composables/useFileType.js +6 -6
  7. package/dist/runtime/app/pages/admin/login.vue +34 -7
  8. package/dist/runtime/server/api/admin/blob.js +4 -3
  9. package/dist/runtime/server/api/admin/csv.js +3 -2
  10. package/dist/runtime/server/api/admin/csv.post.js +6 -4
  11. package/dist/runtime/server/api/admin/delete.js +6 -4
  12. package/dist/runtime/server/api/admin/download.js +11 -8
  13. package/dist/runtime/server/api/admin/git-branch.js +3 -2
  14. package/dist/runtime/server/api/admin/git-history.js +3 -2
  15. package/dist/runtime/server/api/admin/git-update-status.js +3 -2
  16. package/dist/runtime/server/api/admin/git-update.post.js +2 -2
  17. package/dist/runtime/server/api/admin/image.post.js +14 -10
  18. package/dist/runtime/server/api/admin/list.js +11 -6
  19. package/dist/runtime/server/api/admin/login.js +6 -4
  20. package/dist/runtime/server/api/admin/logout.js +2 -1
  21. package/dist/runtime/server/api/admin/md.js +3 -2
  22. package/dist/runtime/server/api/admin/md.post.js +6 -4
  23. package/dist/runtime/server/api/admin/pdf.post.js +13 -9
  24. package/dist/runtime/server/api/admin/txt.js +3 -2
  25. package/dist/runtime/server/api/admin/txt.post.js +6 -4
  26. package/dist/runtime/server/api/admin/upload.js +10 -6
  27. package/dist/runtime/server/api/content/[path].js +14 -12
  28. package/dist/runtime/server/api/content/list.js +9 -7
  29. package/dist/runtime/server/middleware/auth.js +2 -1
  30. package/dist/runtime/server/utils/authCookie.d.ts +9 -0
  31. package/dist/runtime/server/utils/authCookie.js +12 -0
  32. package/dist/runtime/server/utils/contentKey.d.ts +2 -0
  33. package/dist/runtime/server/utils/contentKey.js +67 -0
  34. package/dist/runtime/server/utils/gitErrorSanitization.d.ts +1 -0
  35. package/dist/runtime/server/utils/gitErrorSanitization.js +17 -0
  36. package/dist/runtime/server/utils/gitVersioning.d.ts +1 -2
  37. package/dist/runtime/server/utils/gitVersioning.js +2 -13
  38. package/dist/runtime/server/utils/loginRateLimit.d.ts +4 -0
  39. package/dist/runtime/server/utils/loginRateLimit.js +72 -0
  40. package/dist/runtime/server/utils/uploadGuard.d.ts +2 -0
  41. package/dist/runtime/server/utils/uploadGuard.js +20 -0
  42. package/dist/runtime/shared/contentFiles.d.ts +15 -0
  43. package/dist/runtime/shared/contentFiles.js +38 -0
  44. package/package.json +1 -1
  45. package/dist/runtime/app/composables/useImport.d.ts +0 -6
  46. package/dist/runtime/app/composables/useImport.js +0 -37
  47. package/dist/runtime/server/api/admin/import.d.ts +0 -5
  48. package/dist/runtime/server/api/admin/import.js +0 -86
@@ -2,13 +2,18 @@ import z from "zod";
2
2
  import { createError, defineEventHandler, getValidatedQuery, readMultipartFormData } from "h3";
3
3
  import { useStorage } from "nitropack/runtime";
4
4
  import syncGitContent from "../../utils/syncGitContent.js";
5
+ import { normalizeContentKey } from "../../utils/contentKey.js";
6
+ import { isPdfPath } from "../../../shared/contentFiles.js";
7
+ import { assertUploadSize, getMaxUploadBytes } from "../../utils/uploadGuard.js";
8
+ import { toGitErrorMessage } from "../../utils/gitVersioning.js";
5
9
  const querySchema = z.object({
6
10
  path: z.string().min(1)
7
11
  });
8
12
  export default defineEventHandler(async (event) => {
9
13
  const form = await readMultipartFormData(event);
14
+ const maxUploadBytes = getMaxUploadBytes();
10
15
  const { path } = await getValidatedQuery(event, (query) => querySchema.parse(query));
11
- const decodedPath = decodeURIComponent(path);
16
+ const contentKey = normalizeContentKey(path);
12
17
  if (!form) {
13
18
  throw createError({
14
19
  statusCode: 400,
@@ -28,25 +33,24 @@ export default defineEventHandler(async (event) => {
28
33
  statusMessage: "Invalid file upload"
29
34
  });
30
35
  }
31
- const fileExtension = file.filename.toLowerCase().slice(file.filename.lastIndexOf("."));
32
- const targetExtension = decodedPath.toLowerCase().slice(decodedPath.lastIndexOf("."));
33
- if (targetExtension !== ".pdf") {
36
+ if (!isPdfPath(contentKey)) {
34
37
  throw createError({
35
38
  statusCode: 400,
36
39
  statusMessage: "Invalid target file type. Only PDF files are allowed."
37
40
  });
38
41
  }
39
- if (fileExtension !== ".pdf") {
42
+ if (!isPdfPath(file.filename)) {
40
43
  throw createError({
41
44
  statusCode: 400,
42
45
  statusMessage: "Invalid file type. Only PDF files are allowed."
43
46
  });
44
47
  }
45
- await useStorage("content").setItemRaw(decodedPath, Buffer.from(file.data));
48
+ assertUploadSize(file.data, maxUploadBytes);
49
+ await useStorage("content").setItemRaw(contentKey, Buffer.from(file.data));
46
50
  try {
47
- await syncGitContent("PDF ersetzt", [decodedPath]);
51
+ await syncGitContent("PDF ersetzt", [contentKey]);
48
52
  } catch (error) {
49
- console.error("Git-Fehler:", error);
53
+ console.error("Git-Fehler:", toGitErrorMessage(error, "Git sync failed"));
50
54
  }
51
- return { success: true, path: decodedPath };
55
+ return { success: true, path: contentKey };
52
56
  });
@@ -1,14 +1,15 @@
1
1
  import { z } from "zod";
2
2
  import { createError, defineEventHandler, getValidatedQuery } from "h3";
3
3
  import { useStorage } from "nitropack/runtime";
4
+ import { normalizeContentKey } from "../../utils/contentKey.js";
4
5
  const querySchema = z.object({
5
6
  path: z.string().min(1)
6
7
  });
7
8
  export default defineEventHandler(async (event) => {
8
9
  const { path } = await getValidatedQuery(event, (query) => querySchema.parse(query));
9
- const decodedPath = decodeURIComponent(path);
10
+ const contentKey = normalizeContentKey(path);
10
11
  const storage = useStorage("content");
11
- const file = await storage.getItem(decodedPath);
12
+ const file = await storage.getItem(contentKey);
12
13
  if (!file) {
13
14
  throw createError({
14
15
  statusCode: 404,
@@ -2,6 +2,8 @@ import { z } from "zod";
2
2
  import { defineEventHandler, getValidatedQuery, readValidatedBody } from "h3";
3
3
  import { useStorage } from "nitropack/runtime";
4
4
  import syncGitContent from "../../utils/syncGitContent.js";
5
+ import { normalizeContentKey } from "../../utils/contentKey.js";
6
+ import { toGitErrorMessage } from "../../utils/gitVersioning.js";
5
7
  const querySchema = z.object({
6
8
  path: z.string().min(1)
7
9
  });
@@ -12,13 +14,13 @@ const bodySchema = z.object({
12
14
  export default defineEventHandler(async (event) => {
13
15
  const { path } = await getValidatedQuery(event, (query) => querySchema.parse(query));
14
16
  const { text, commitMessage } = await readValidatedBody(event, (body) => bodySchema.parse(body));
15
- const decodedPath = decodeURIComponent(path);
17
+ const contentKey = normalizeContentKey(path);
16
18
  const storage = useStorage("content");
17
- await storage.setItem(decodedPath, text);
19
+ await storage.setItem(contentKey, text);
18
20
  try {
19
- await syncGitContent(commitMessage, [decodedPath]);
21
+ await syncGitContent(commitMessage, [contentKey]);
20
22
  } catch (error) {
21
- console.error("Git-Fehler:", error);
23
+ console.error("Git-Fehler:", toGitErrorMessage(error, "Git sync failed"));
22
24
  }
23
25
  return { success: true };
24
26
  });
@@ -2,6 +2,10 @@ import z from "zod";
2
2
  import { createError, defineEventHandler, getValidatedQuery, readMultipartFormData } from "h3";
3
3
  import { useStorage } from "nitropack/runtime";
4
4
  import syncGitContent from "../../utils/syncGitContent.js";
5
+ import { normalizeContentKey, normalizeContentPrefix } from "../../utils/contentKey.js";
6
+ import { CONTENT_UPLOAD_EXTENSIONS, hasAllowedExtension } from "../../../shared/contentFiles.js";
7
+ import { assertUploadSize, getMaxUploadBytes } from "../../utils/uploadGuard.js";
8
+ import { toGitErrorMessage } from "../../utils/gitVersioning.js";
5
9
  function sanitizeFilename(filename) {
6
10
  return filename.replace(/[/:\\]/g, "_");
7
11
  }
@@ -10,8 +14,9 @@ const querySchema = z.object({
10
14
  });
11
15
  export default defineEventHandler(async (event) => {
12
16
  const form = await readMultipartFormData(event);
17
+ const maxUploadBytes = getMaxUploadBytes();
13
18
  const { path } = await getValidatedQuery(event, (query) => querySchema.parse(query));
14
- const sanePath = path ? path.replace(/^\//, "").replace(/\/$/, "") : void 0;
19
+ const contentPrefix = normalizeContentPrefix(path);
15
20
  if (!form) {
16
21
  throw createError({
17
22
  statusCode: 400,
@@ -31,20 +36,19 @@ export default defineEventHandler(async (event) => {
31
36
  statusMessage: "Invalid file upload"
32
37
  });
33
38
  }
34
- const allowedExtensions = [".pdf", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".md", ".docx", ".txt", ".csv", ".json"];
35
- const fileExtension = file.filename.toLowerCase().slice(file.filename.lastIndexOf("."));
36
- if (!allowedExtensions.includes(fileExtension)) {
39
+ if (!hasAllowedExtension(file.filename, CONTENT_UPLOAD_EXTENSIONS)) {
37
40
  throw createError({
38
41
  statusCode: 400,
39
42
  statusMessage: "Invalid file type. Only PDF, JPG, JPEG, PNG, GIF, WEBP, MD, DOCX, TXT, CSV, and JSON files are allowed."
40
43
  });
41
44
  }
42
- const filePath = [sanePath, sanitizeFilename(file.filename)].filter(Boolean).join(":");
45
+ assertUploadSize(file.data, maxUploadBytes);
46
+ const filePath = normalizeContentKey([contentPrefix, sanitizeFilename(file.filename)].filter(Boolean).join(":"));
43
47
  await useStorage("content").setItemRaw(filePath, Buffer.from(file.data));
44
48
  try {
45
49
  await syncGitContent("Datei hinzugef\xFCgt", [filePath]);
46
50
  } catch (error) {
47
- console.error("Git-Fehler:", error);
51
+ console.error("Git-Fehler:", toGitErrorMessage(error, "Git sync failed"));
48
52
  }
49
53
  return { success: true, path: filePath };
50
54
  });
@@ -3,18 +3,20 @@ import { createError, defineEventHandler, getValidatedRouterParams, send } from
3
3
  import { useStorage } from "nitropack/runtime";
4
4
  import { toNodeBuffer } from "../../utils/toNodeBuffer.js";
5
5
  import { parsedFile } from "../../utils/parsedFile.js";
6
+ import { normalizeContentKey } from "../../utils/contentKey.js";
7
+ import { isCsvPath, isImagePath, isJsonPath, isMarkdownPath, isPdfPath, toFileExtension } from "../../../shared/contentFiles.js";
6
8
  function getFileType(path) {
7
- const isImage = path.match(/\.(png|jpg|jpeg|gif|webp)$/i);
8
- const isPdf = path.endsWith(".pdf");
9
- const isJson = path.endsWith(".json");
10
- const isCSV = path.endsWith(".csv");
11
- const isMarkdown = path.endsWith(".md");
9
+ const isImage = isImagePath(path);
10
+ const isPdf = isPdfPath(path);
11
+ const isJson = isJsonPath(path);
12
+ const isCSV = isCsvPath(path);
13
+ const isMarkdown = isMarkdownPath(path);
12
14
  return { isImage, isPdf, isJson, isCSV, isMarkdown };
13
15
  }
14
16
  function getContentType(path) {
15
17
  const { isImage, isPdf, isJson, isCSV, isMarkdown } = getFileType(path);
16
18
  if (isImage) {
17
- const ext = path.split(".").pop()?.toLowerCase();
19
+ const ext = toFileExtension(path).slice(1);
18
20
  if (ext === "jpg") {
19
21
  return "image/jpeg";
20
22
  } else {
@@ -33,12 +35,12 @@ const paramsSchema = z.object({
33
35
  });
34
36
  export default defineEventHandler(async (event) => {
35
37
  const { path } = await getValidatedRouterParams(event, (params) => paramsSchema.parse(params));
36
- const decodedPath = decodeURIComponent(path);
37
- const { isImage, isPdf } = getFileType(decodedPath);
38
- event.node.res.setHeader("Content-Type", getContentType(decodedPath));
38
+ const contentKey = normalizeContentKey(path);
39
+ const { isImage, isPdf } = getFileType(contentKey);
40
+ event.node.res.setHeader("Content-Type", getContentType(contentKey));
39
41
  const storage = useStorage("content");
40
42
  if (isImage || isPdf) {
41
- const raw = await storage.getItemRaw(decodedPath);
43
+ const raw = await storage.getItemRaw(contentKey);
42
44
  if (!raw) {
43
45
  throw createError({
44
46
  statusCode: 404,
@@ -49,12 +51,12 @@ export default defineEventHandler(async (event) => {
49
51
  event.node.res.setHeader("Content-Length", String(body.byteLength));
50
52
  return send(event, body);
51
53
  }
52
- const file = await storage.getItem(decodedPath);
54
+ const file = await storage.getItem(contentKey);
53
55
  if (!file) {
54
56
  throw createError({
55
57
  statusCode: 404,
56
58
  statusMessage: "File not found"
57
59
  });
58
60
  }
59
- return parsedFile(decodedPath, file);
61
+ return parsedFile(contentKey, file);
60
62
  });
@@ -3,30 +3,32 @@ import { useStorage } from "nitropack/runtime";
3
3
  import { defineEventHandler, getValidatedQuery } from "h3";
4
4
  import { marked } from "marked";
5
5
  import { parseFrontmatter } from "../../utils/parseFrontmatter.js";
6
+ import { normalizeContentPrefix } from "../../utils/contentKey.js";
7
+ import { isCsvPath, isImagePath, isJsonPath, isMarkdownPath, isTextPath } from "../../../shared/contentFiles.js";
6
8
  const querySchema = z.object({
7
9
  path: z.string().optional(),
8
10
  type: z.string()
9
11
  });
10
12
  export default defineEventHandler(async (event) => {
11
13
  const { path, type } = await getValidatedQuery(event, (query) => querySchema.parse(query));
12
- const decodedPath = path ? decodeURIComponent(path) : void 0;
14
+ const contentPrefix = normalizeContentPrefix(path);
13
15
  const storage = useStorage("content");
14
- const keys = await storage.getKeys(decodedPath);
16
+ const keys = await storage.getKeys(contentPrefix);
15
17
  const filteredKeys = keys.filter((key) => {
16
18
  if (type === "md") {
17
- return key.endsWith(".md");
19
+ return isMarkdownPath(key);
18
20
  }
19
21
  if (type === "json") {
20
- return key.endsWith(".json");
22
+ return isJsonPath(key);
21
23
  }
22
24
  if (type === "csv") {
23
- return key.endsWith(".csv");
25
+ return isCsvPath(key);
24
26
  }
25
27
  if (type === "txt") {
26
- return key.endsWith(".txt");
28
+ return isTextPath(key);
27
29
  }
28
30
  if (type === "image") {
29
- return key.match(/\.(png|jpg|jpeg|gif|webp)$/i);
31
+ return isImagePath(key);
30
32
  }
31
33
  return false;
32
34
  });
@@ -1,5 +1,6 @@
1
1
  import { useRuntimeConfig } from "nitropack/runtime";
2
2
  import { createError, defineEventHandler, getCookie, getRequestURL, sendRedirect } from "h3";
3
+ import { ADMIN_AUTH_COOKIE_NAME } from "../utils/authCookie.js";
3
4
  export default defineEventHandler(async (event) => {
4
5
  const pathname = getRequestURL(event).pathname;
5
6
  const isAdminLoginRoute = pathname === "/admin/login" || pathname === "/api/admin/login";
@@ -7,7 +8,7 @@ export default defineEventHandler(async (event) => {
7
8
  const isAdminApiRoute = pathname === "/api/admin" || pathname.startsWith("/api/admin/");
8
9
  if (isAdminLoginRoute || !isAdminRoute && !isAdminApiRoute) return;
9
10
  const { mktcms: { adminAuthKey } } = useRuntimeConfig();
10
- const authKeyCookie = getCookie(event, "mktcms_admin_auth_key");
11
+ const authKeyCookie = getCookie(event, ADMIN_AUTH_COOKIE_NAME);
11
12
  if (!authKeyCookie || authKeyCookie !== adminAuthKey.toString() || adminAuthKey === "") {
12
13
  if (isAdminApiRoute) {
13
14
  throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
@@ -0,0 +1,9 @@
1
+ import type { H3Event } from 'h3';
2
+ export declare const ADMIN_AUTH_COOKIE_NAME = "mktcms_admin_auth_key";
3
+ export declare function getAuthCookieOptions(event: H3Event): {
4
+ httpOnly: boolean;
5
+ maxAge: number;
6
+ path: any;
7
+ sameSite: "lax" | "strict" | "none";
8
+ secure: boolean;
9
+ };
@@ -0,0 +1,12 @@
1
+ import { useRuntimeConfig } from "nitropack/runtime";
2
+ export const ADMIN_AUTH_COOKIE_NAME = "mktcms_admin_auth_key";
3
+ export function getAuthCookieOptions(event) {
4
+ const { mktcms: { authCookieMaxAgeSeconds, authCookiePath, authCookieSameSite, authCookieSecure } } = useRuntimeConfig(event);
5
+ return {
6
+ httpOnly: true,
7
+ maxAge: Number(authCookieMaxAgeSeconds) || 7 * 24 * 60 * 60,
8
+ path: authCookiePath || "/",
9
+ sameSite: authCookieSameSite || "lax",
10
+ secure: Boolean(authCookieSecure)
11
+ };
12
+ }
@@ -0,0 +1,2 @@
1
+ export declare function normalizeContentKey(input: string): string;
2
+ export declare function normalizeContentPrefix(input?: string): string;
@@ -0,0 +1,67 @@
1
+ import { createError } from "h3";
2
+ function invalidContentKeyError() {
3
+ return createError({
4
+ statusCode: 400,
5
+ statusMessage: "Invalid content path"
6
+ });
7
+ }
8
+ function hasControlCharacters(value) {
9
+ for (const char of value) {
10
+ const code = char.charCodeAt(0);
11
+ if (code >= 0 && code <= 31 || code === 127) {
12
+ return true;
13
+ }
14
+ }
15
+ return false;
16
+ }
17
+ function normalizeContentKeyInternal(input, options = {}) {
18
+ const raw = input.trim();
19
+ if (!raw) {
20
+ if (options.allowEmpty) {
21
+ return "";
22
+ }
23
+ throw invalidContentKeyError();
24
+ }
25
+ let decoded;
26
+ try {
27
+ decoded = decodeURIComponent(raw);
28
+ } catch {
29
+ throw invalidContentKeyError();
30
+ }
31
+ if (!decoded.trim()) {
32
+ if (options.allowEmpty) {
33
+ return "";
34
+ }
35
+ throw invalidContentKeyError();
36
+ }
37
+ if (decoded.startsWith("/") || decoded.startsWith("\\") || /^[a-z]:[\\/]/i.test(decoded)) {
38
+ throw invalidContentKeyError();
39
+ }
40
+ const unifiedSeparators = decoded.replace(/[\\/]+/g, ":").trim();
41
+ if (!unifiedSeparators) {
42
+ if (options.allowEmpty) {
43
+ return "";
44
+ }
45
+ throw invalidContentKeyError();
46
+ }
47
+ if (hasControlCharacters(unifiedSeparators)) {
48
+ throw invalidContentKeyError();
49
+ }
50
+ const segments = unifiedSeparators.split(":");
51
+ if (segments.some((segment) => !segment)) {
52
+ throw invalidContentKeyError();
53
+ }
54
+ if (segments.some((segment) => segment === "." || segment === "..")) {
55
+ throw invalidContentKeyError();
56
+ }
57
+ return segments.join(":");
58
+ }
59
+ export function normalizeContentKey(input) {
60
+ return normalizeContentKeyInternal(input);
61
+ }
62
+ export function normalizeContentPrefix(input) {
63
+ if (!input) {
64
+ return "";
65
+ }
66
+ return normalizeContentKeyInternal(input, { allowEmpty: true });
67
+ }
@@ -0,0 +1 @@
1
+ export declare function toGitErrorMessage(error: unknown, fallback: string): string;
@@ -0,0 +1,17 @@
1
+ export function toGitErrorMessage(error, fallback) {
2
+ const rawMessage = error instanceof Error ? error.message : String(error);
3
+ const message = rawMessage.replace(/https?:\/\/[^\s@]+@/gi, "https://***@").replace(/(ghp_[A-Za-z0-9]+)/g, "***").replace(/(github_pat_\w+)/g, "***").replace(/(token=)[^&\s]+/gi, "$1***").replace(/(x-access-token:)[^@\s]+/gi, "$1***");
4
+ if (/conflict|merge conflict|could not apply|needs merge|merge failed/i.test(message)) {
5
+ return "Git operation failed due to merge conflicts. Resolve conflicts and retry.";
6
+ }
7
+ if (/authentication failed|could not read username|access denied|permission denied/i.test(message)) {
8
+ return "Git operation failed due to authentication error. Check NUXT_MKTCMS_GIT_USER, NUXT_MKTCMS_GIT_REPO and NUXT_MKTCMS_GIT_TOKEN.";
9
+ }
10
+ if (/not possible to fast-forward|non-fast-forward|fetch first/i.test(message)) {
11
+ return "Git operation failed because the target branch cannot be fast-forwarded. Pull and reconcile the branch state first.";
12
+ }
13
+ if (/timed out|network is unreachable|could not resolve host|econnreset|econnrefused|etimedout/i.test(message)) {
14
+ return "Git operation failed due to a network error. Please retry.";
15
+ }
16
+ return fallback;
17
+ }
@@ -1,10 +1,10 @@
1
+ export { toGitErrorMessage } from './gitErrorSanitization.js';
1
2
  export declare const MKTCMS_GIT_BOT_NAME = "Kunde";
2
3
  export declare const MKTCMS_GIT_BOT_EMAIL = "admin@mktcode.de";
3
4
  export declare function gitBotIdentityArgs(): string[];
4
5
  export declare const SUPPORTED_WEBSITE_BRANCHES: readonly ["main", "staging"];
5
6
  export type WebsiteBranch = typeof SUPPORTED_WEBSITE_BRANCHES[number];
6
7
  export declare function isSupportedWebsiteBranch(branch: string): branch is WebsiteBranch;
7
- export declare function toGitErrorMessage(error: unknown, fallback: string): string;
8
8
  type GitClientOptions = {
9
9
  baseDir?: string;
10
10
  authUrlOverride?: string;
@@ -51,4 +51,3 @@ export declare function mergeCounterpartBranchIntoCurrent(options?: MergeOptions
51
51
  sourceBranch: "main" | "staging";
52
52
  targetBranch: "main" | "staging";
53
53
  }>;
54
- export {};
@@ -1,5 +1,7 @@
1
1
  import { useRuntimeConfig } from "nitropack/runtime";
2
2
  import { simpleGit } from "simple-git";
3
+ import { toGitErrorMessage } from "./gitErrorSanitization.js";
4
+ export { toGitErrorMessage } from "./gitErrorSanitization.js";
3
5
  export const MKTCMS_GIT_BOT_NAME = "Kunde";
4
6
  export const MKTCMS_GIT_BOT_EMAIL = "admin@mktcode.de";
5
7
  export function gitBotIdentityArgs() {
@@ -14,19 +16,6 @@ export const SUPPORTED_WEBSITE_BRANCHES = ["main", "staging"];
14
16
  export function isSupportedWebsiteBranch(branch) {
15
17
  return SUPPORTED_WEBSITE_BRANCHES.includes(branch);
16
18
  }
17
- export function toGitErrorMessage(error, fallback) {
18
- const message = error instanceof Error ? error.message : String(error);
19
- if (/conflict|merge conflict|could not apply|needs merge|merge failed/i.test(message)) {
20
- return "Git operation failed due to merge conflicts. Resolve conflicts and retry.";
21
- }
22
- if (/authentication failed|could not read username|access denied|permission denied/i.test(message)) {
23
- return "Git operation failed due to authentication error. Check NUXT_MKTCMS_GIT_USER, NUXT_MKTCMS_GIT_REPO and NUXT_MKTCMS_GIT_TOKEN.";
24
- }
25
- if (/not possible to fast-forward|non-fast-forward|fetch first/i.test(message)) {
26
- return "Git operation failed because the target branch cannot be fast-forwarded. Pull and reconcile the branch state first.";
27
- }
28
- return `${fallback}: ${message}`;
29
- }
30
19
  export function isVersioningEnabled() {
31
20
  const { public: { mktcms: { showVersioning } } } = useRuntimeConfig();
32
21
  return Boolean(showVersioning);
@@ -0,0 +1,4 @@
1
+ import type { H3Event } from 'h3';
2
+ export declare function assertLoginNotRateLimited(event: H3Event): void;
3
+ export declare function recordFailedLoginAttempt(event: H3Event): void;
4
+ export declare function clearFailedLoginAttempts(event: H3Event): void;
@@ -0,0 +1,72 @@
1
+ import { createError, getRequestIP } from "h3";
2
+ import { useRuntimeConfig } from "nitropack/runtime";
3
+ const attempts = /* @__PURE__ */ new Map();
4
+ function getState(clientId) {
5
+ const state = attempts.get(clientId);
6
+ if (state) {
7
+ return state;
8
+ }
9
+ const next = {
10
+ failedAt: [],
11
+ blockedUntil: 0
12
+ };
13
+ attempts.set(clientId, next);
14
+ return next;
15
+ }
16
+ function getClientId(event) {
17
+ const forwarded = getRequestIP(event, { xForwardedFor: true });
18
+ return forwarded || event.node.req.socket.remoteAddress || "unknown";
19
+ }
20
+ function getRateLimitSettings(event) {
21
+ const {
22
+ mktcms: {
23
+ loginRateLimitMaxAttempts,
24
+ loginRateLimitWindowSeconds,
25
+ loginRateLimitBlockSeconds
26
+ }
27
+ } = useRuntimeConfig(event);
28
+ const maxAttempts = Math.max(1, Number(loginRateLimitMaxAttempts) || 5);
29
+ const windowMs = Math.max(1, Number(loginRateLimitWindowSeconds) || 300) * 1e3;
30
+ const blockMs = Math.max(1, Number(loginRateLimitBlockSeconds) || 600) * 1e3;
31
+ return {
32
+ maxAttempts,
33
+ windowMs,
34
+ blockMs
35
+ };
36
+ }
37
+ function clearStaleAttempts(state, now, windowMs) {
38
+ state.failedAt = state.failedAt.filter((ts) => now - ts <= windowMs);
39
+ }
40
+ export function assertLoginNotRateLimited(event) {
41
+ const { windowMs } = getRateLimitSettings(event);
42
+ const clientId = getClientId(event);
43
+ const now = Date.now();
44
+ const state = getState(clientId);
45
+ clearStaleAttempts(state, now, windowMs);
46
+ if (state.blockedUntil > now) {
47
+ const retryAfterSeconds = Math.ceil((state.blockedUntil - now) / 1e3);
48
+ throw createError({
49
+ statusCode: 429,
50
+ statusMessage: "Too many login attempts. Please try again later.",
51
+ data: {
52
+ retryAfterSeconds
53
+ }
54
+ });
55
+ }
56
+ }
57
+ export function recordFailedLoginAttempt(event) {
58
+ const { maxAttempts, windowMs, blockMs } = getRateLimitSettings(event);
59
+ const clientId = getClientId(event);
60
+ const now = Date.now();
61
+ const state = getState(clientId);
62
+ clearStaleAttempts(state, now, windowMs);
63
+ state.failedAt.push(now);
64
+ if (state.failedAt.length >= maxAttempts) {
65
+ state.blockedUntil = now + blockMs;
66
+ state.failedAt = [];
67
+ }
68
+ }
69
+ export function clearFailedLoginAttempts(event) {
70
+ const clientId = getClientId(event);
71
+ attempts.delete(clientId);
72
+ }
@@ -0,0 +1,2 @@
1
+ export declare function getMaxUploadBytes(): number;
2
+ export declare function assertUploadSize(fileData: Buffer | Uint8Array | string, maxBytes: number): void;
@@ -0,0 +1,20 @@
1
+ import { createError } from "h3";
2
+ import { useRuntimeConfig } from "nitropack/runtime";
3
+ import { DEFAULT_MAX_UPLOAD_BYTES } from "../../shared/contentFiles.js";
4
+ export function getMaxUploadBytes() {
5
+ const { mktcms: { uploadMaxBytes } } = useRuntimeConfig();
6
+ const parsed = Number(uploadMaxBytes);
7
+ if (!Number.isFinite(parsed) || parsed <= 0) {
8
+ return DEFAULT_MAX_UPLOAD_BYTES;
9
+ }
10
+ return Math.floor(parsed);
11
+ }
12
+ export function assertUploadSize(fileData, maxBytes) {
13
+ const byteLength = typeof fileData === "string" ? Buffer.byteLength(fileData) : fileData.byteLength;
14
+ if (byteLength > maxBytes) {
15
+ throw createError({
16
+ statusCode: 413,
17
+ statusMessage: `Upload too large. Maximum allowed size is ${maxBytes} bytes.`
18
+ });
19
+ }
20
+ }
@@ -0,0 +1,15 @@
1
+ export declare const IMAGE_EXTENSIONS: readonly [".jpg", ".jpeg", ".png", ".gif", ".webp"];
2
+ export declare const PDF_EXTENSIONS: readonly [".pdf"];
3
+ export declare const TEXT_EXTENSIONS: readonly [".txt", ".json"];
4
+ export declare const EDITABLE_EXTENSIONS: readonly [".md", ".csv", ".txt", ".json"];
5
+ export declare const CONTENT_UPLOAD_EXTENSIONS: readonly [".pdf", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".md", ".docx", ".txt", ".csv", ".json"];
6
+ export declare const DEFAULT_MAX_UPLOAD_BYTES: number;
7
+ export declare function toFileExtension(filePath: string): string;
8
+ export declare function hasAllowedExtension(filePath: string, allowedExtensions: readonly string[]): boolean;
9
+ export declare function isImagePath(filePath: string): boolean;
10
+ export declare function isPdfPath(filePath: string): boolean;
11
+ export declare function isTextPath(filePath: string): boolean;
12
+ export declare function isMarkdownPath(filePath: string): boolean;
13
+ export declare function isCsvPath(filePath: string): boolean;
14
+ export declare function isJsonPath(filePath: string): boolean;
15
+ export declare function toAcceptAttribute(extensions: readonly string[]): string;
@@ -0,0 +1,38 @@
1
+ export const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
2
+ export const PDF_EXTENSIONS = [".pdf"];
3
+ export const TEXT_EXTENSIONS = [".txt", ".json"];
4
+ export const EDITABLE_EXTENSIONS = [".md", ".csv", ".txt", ".json"];
5
+ export const CONTENT_UPLOAD_EXTENSIONS = [".pdf", ...IMAGE_EXTENSIONS, ".md", ".docx", ".txt", ".csv", ".json"];
6
+ export const DEFAULT_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
7
+ export function toFileExtension(filePath) {
8
+ const dotIndex = filePath.lastIndexOf(".");
9
+ if (dotIndex < 0) {
10
+ return "";
11
+ }
12
+ return filePath.slice(dotIndex).toLowerCase();
13
+ }
14
+ export function hasAllowedExtension(filePath, allowedExtensions) {
15
+ const extension = toFileExtension(filePath);
16
+ return extension !== "" && allowedExtensions.includes(extension);
17
+ }
18
+ export function isImagePath(filePath) {
19
+ return hasAllowedExtension(filePath, IMAGE_EXTENSIONS);
20
+ }
21
+ export function isPdfPath(filePath) {
22
+ return hasAllowedExtension(filePath, PDF_EXTENSIONS);
23
+ }
24
+ export function isTextPath(filePath) {
25
+ return hasAllowedExtension(filePath, TEXT_EXTENSIONS);
26
+ }
27
+ export function isMarkdownPath(filePath) {
28
+ return hasAllowedExtension(filePath, [".md"]);
29
+ }
30
+ export function isCsvPath(filePath) {
31
+ return hasAllowedExtension(filePath, [".csv"]);
32
+ }
33
+ export function isJsonPath(filePath) {
34
+ return hasAllowedExtension(filePath, [".json"]);
35
+ }
36
+ export function toAcceptAttribute(extensions) {
37
+ return extensions.join(",");
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mktcms",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "Simple CMS module for Nuxt",
5
5
  "repository": "mktcode/mktcms",
6
6
  "license": "MIT",
@@ -1,6 +0,0 @@
1
- export default function useImport(): {
2
- uploadError: import("vue").Ref<string | null, string | null>;
3
- isUploading: import("vue").Ref<boolean, boolean>;
4
- fileInput: import("vue").Ref<HTMLInputElement | null, HTMLInputElement | null>;
5
- uploadFile: (event: Event) => Promise<void>;
6
- };
@@ -1,37 +0,0 @@
1
- import { ref } from "vue";
2
- export default function useImport() {
3
- const uploadError = ref(null);
4
- const fileInput = ref(null);
5
- const isUploading = ref(false);
6
- async function uploadFile(event) {
7
- if (isUploading.value) return;
8
- isUploading.value = true;
9
- uploadError.value = null;
10
- const input = event.target;
11
- if (!input.files) {
12
- return;
13
- }
14
- const file = input.files[0];
15
- if (!file) {
16
- return;
17
- }
18
- const formData = new FormData();
19
- formData.append("file", file);
20
- try {
21
- await $fetch("/api/admin/import", {
22
- method: "POST",
23
- body: formData
24
- });
25
- } catch (error) {
26
- uploadError.value = error.data?.statusMessage || "Fehler beim Hochladen der Datei";
27
- input.value = "";
28
- }
29
- isUploading.value = false;
30
- }
31
- return {
32
- uploadError,
33
- isUploading,
34
- fileInput,
35
- uploadFile
36
- };
37
- }
@@ -1,5 +0,0 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
- success: boolean;
3
- count: number;
4
- }>>;
5
- export default _default;