diff --git a/.output/nitro.json b/.output/nitro.json index e439e7c..61d7a12 100644 --- a/.output/nitro.json +++ b/.output/nitro.json @@ -1,5 +1,5 @@ { - "date": "2025-10-22T13:11:03.062Z", + "date": "2025-10-22T20:59:39.715Z", "preset": "node-server", "framework": { "name": "nuxt", diff --git a/.output/public/_nuxt/builds/latest.json b/.output/public/_nuxt/builds/latest.json index 52f9830..7da699a 100644 --- a/.output/public/_nuxt/builds/latest.json +++ b/.output/public/_nuxt/builds/latest.json @@ -1 +1 @@ -{"id":"e0e6f75d-0441-44da-af83-0c8ad93a6ce1","timestamp":1761138655435} \ No newline at end of file +{"id":"45b20f41-8a8a-4096-94d4-3b6174a1f364","timestamp":1761166771198} \ No newline at end of file diff --git a/.output/server/chunks/build/client.manifest.mjs b/.output/server/chunks/build/client.manifest.mjs index 892ad58..4350400 100644 --- a/.output/server/chunks/build/client.manifest.mjs +++ b/.output/server/chunks/build/client.manifest.mjs @@ -87,6 +87,17 @@ const client_manifest = { "node_modules/nuxt/dist/app/entry.js" ] }, + "_C_U-NUAd.js": { + "resourceType": "script", + "module": true, + "prefetch": true, + "preload": true, + "file": "C_U-NUAd.js", + "name": "composables", + "imports": [ + "node_modules/nuxt/dist/app/entry.js" + ] + }, "_CkzaQq3X.js": { "resourceType": "script", "module": true, @@ -216,17 +227,6 @@ const client_manifest = { "node_modules/nuxt/dist/app/entry.js" ] }, - "_Qy3ajxTk.js": { - "resourceType": "script", - "module": true, - "prefetch": true, - "preload": true, - "file": "Qy3ajxTk.js", - "name": "composables", - "imports": [ - "node_modules/nuxt/dist/app/entry.js" - ] - }, "_R6Iy1jPP.js": { "resourceType": "script", "module": true, @@ -294,7 +294,7 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "D-Zvexq_.js", + "file": "63DEGUI_.js", "name": "auth", "src": "middleware/auth.js", "isDynamicEntry": true, @@ -307,14 +307,14 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "ZrOCUSmD.js", + "file": "C9SglkVL.js", "name": "error-404", "src": "node_modules/nuxt/dist/app/components/error-404.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", "_DlAUqK2U.js", - "_Qy3ajxTk.js" + "_C_U-NUAd.js" ], "css": [] }, @@ -329,13 +329,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "BVRiFo7f.js", + "file": "CW9krljs.js", "name": "error-500", "src": "node_modules/nuxt/dist/app/components/error-500.vue", "isDynamicEntry": true, "imports": [ "_DlAUqK2U.js", - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "node_modules/nuxt/dist/app/entry.js" ], "css": [] @@ -351,7 +351,7 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "CmbXHhwn.js", + "file": "KrCelFbA.js", "name": "entry", "src": "node_modules/nuxt/dist/app/entry.js", "isEntry": true, @@ -361,14 +361,14 @@ const client_manifest = { "node_modules/nuxt/dist/app/components/error-500.vue" ], "css": [ - "entry.DX4WLXSP.css" + "entry.BpzTEo9t.css" ], "assets": [ "Harheimer TC.CKfYAfp1.svg" ] }, - "entry.DX4WLXSP.css": { - "file": "entry.DX4WLXSP.css", + "entry.BpzTEo9t.css": { + "file": "entry.BpzTEo9t.css", "resourceType": "style", "prefetch": true, "preload": true @@ -384,13 +384,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "D7LlSYAz.js", + "file": "C3627_Er.js", "name": "anlagen", "src": "pages/anlagen.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js" + "_C_U-NUAd.js" ] }, "pages/cms/benutzer.vue": { @@ -398,13 +398,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "CtoHBsGq.js", + "file": "CzgMfPlN.js", "name": "benutzer", "src": "pages/cms/benutzer.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_C8kQt0fa.js", "_DaSgy0Cl.js" ] @@ -414,13 +414,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "BlTx75vv.js", + "file": "CIPPGKqt.js", "name": "einstellungen", "src": "pages/cms/einstellungen.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_CUq_0rkE.js", "_DUm-savV.js", "_YJHbYJtA.js", @@ -436,12 +436,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "G3v2TcOj.js", + "file": "DxKvlgrz.js", "name": "geschichte", "src": "pages/cms/geschichte.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "node_modules/nuxt/dist/app/entry.js" ] }, @@ -450,31 +450,45 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "CcTzWjtb.js", + "file": "CfIOs31W.js", "name": "index", "src": "pages/cms/index.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_DAACT36i.js", "_YJHbYJtA.js", "_DkeYb0_S.js", "_DUm-savV.js" ] }, + "pages/cms/mitgliedschaftsantraege.vue": { + "resourceType": "script", + "module": true, + "prefetch": true, + "preload": true, + "file": "DUL8f07u.js", + "name": "mitgliedschaftsantraege", + "src": "pages/cms/mitgliedschaftsantraege.vue", + "isDynamicEntry": true, + "imports": [ + "node_modules/nuxt/dist/app/entry.js", + "_C_U-NUAd.js" + ] + }, "pages/cms/satzung.vue": { "resourceType": "script", "module": true, "prefetch": true, "preload": true, - "file": "CJOOzUp1.js", + "file": "DzTYmaPw.js", "name": "satzung", "src": "pages/cms/satzung.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js" + "_C_U-NUAd.js" ] }, "pages/cms/termine.vue": { @@ -482,13 +496,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "BP5itt78.js", + "file": "DDYB2ueJ.js", "name": "termine", "src": "pages/cms/termine.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_R6Iy1jPP.js", "_CUq_0rkE.js", "_FF_cyd6S.js", @@ -500,12 +514,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "C8Sqpv2D.js", + "file": "CPS6rtgg.js", "name": "tt-regeln", "src": "pages/cms/tt-regeln.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "node_modules/nuxt/dist/app/entry.js" ] }, @@ -514,12 +528,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "CfC7HTR7.js", + "file": "lhUIkRXy.js", "name": "ueber-uns", "src": "pages/cms/ueber-uns.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "node_modules/nuxt/dist/app/entry.js" ] }, @@ -528,13 +542,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "Bu6whu8C.js", + "file": "CJ4iaRop.js", "name": "galerie", "src": "pages/galerie.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js" + "_C_U-NUAd.js" ] }, "pages/geschichte.vue": { @@ -542,12 +556,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "Bqdk1y3w.js", + "file": "B23trXK4.js", "name": "geschichte", "src": "pages/geschichte.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "node_modules/nuxt/dist/app/entry.js" ] }, @@ -556,13 +570,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "DU1f7CIy.js", + "file": "B2n3gjaF.js", "name": "impressum", "src": "pages/impressum.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_BteKZQ9T.js", "_Czdc6-TI.js" ] @@ -598,7 +612,7 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "5IKOyEE8.js", + "file": "B_w5gbrC.js", "name": "kontakt", "src": "pages/kontakt.vue", "isDynamicEntry": true, @@ -608,7 +622,7 @@ const client_manifest = { "_DdHhmCne.js", "_BC4PNGtJ.js", "_C8kQt0fa.js", - "_Qy3ajxTk.js" + "_C_U-NUAd.js" ] }, "pages/login.vue": { @@ -616,13 +630,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "TnZylaYP.js", + "file": "DCC-mN0A.js", "name": "login", "src": "pages/login.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_C8kQt0fa.js", "_DaSgy0Cl.js", "_CUq_0rkE.js" @@ -633,13 +647,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "DrS211Qg.js", + "file": "BKO8ChwC.js", "name": "_slug_", "src": "pages/mannschaften/[slug].vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_jVj3QaoK.js" ] }, @@ -648,13 +662,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "48ve60fm.js", + "file": "LPF2GIYR.js", "name": "damen", "src": "pages/mannschaften/damen.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js" + "_C_U-NUAd.js" ] }, "pages/mannschaften/herren.vue": { @@ -662,12 +676,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "BEK-x74F.js", + "file": "sVyj_WZX.js", "name": "herren", "src": "pages/mannschaften/herren.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "node_modules/nuxt/dist/app/entry.js" ] }, @@ -676,7 +690,7 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "CGF4oWdy.js", + "file": "Bivc7aFF.js", "name": "index", "src": "pages/mannschaften/index.vue", "isDynamicEntry": true, @@ -684,7 +698,7 @@ const client_manifest = { "node_modules/nuxt/dist/app/entry.js", "_jVj3QaoK.js", "_DkeYb0_S.js", - "_Qy3ajxTk.js" + "_C_U-NUAd.js" ] }, "pages/mannschaften/jugend.vue": { @@ -692,13 +706,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "BOTV4xuv.js", + "file": "BZLaJF8o.js", "name": "jugend", "src": "pages/mannschaften/jugend.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js" + "_C_U-NUAd.js" ] }, "pages/mannschaften/spielplaene.vue": { @@ -706,12 +720,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "DmeaandR.js", + "file": "S5xR3JqC.js", "name": "spielplaene", "src": "pages/mannschaften/spielplaene.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_BteKZQ9T.js", "node_modules/nuxt/dist/app/entry.js", "_Cx4UcKGu.js" @@ -722,13 +736,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "DBoACobG.js", + "file": "rgKTeSYE.js", "name": "index", "src": "pages/mitgliederbereich/index.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_DkeYb0_S.js", "_DAACT36i.js", "_DaSgy0Cl.js" @@ -739,13 +753,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "CmqI4hAm.js", + "file": "Cx_3U4cr.js", "name": "mitglieder", "src": "pages/mitgliederbereich/mitglieder.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_oN0_bS6A.js", "_CUq_0rkE.js", "_6EY4_GXp.js", @@ -762,13 +776,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "DNeqJiJt.js", + "file": "pO5XAgdL.js", "name": "news", "src": "pages/mitgliederbereich/news.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_R6Iy1jPP.js", "_CUq_0rkE.js", "_KxVBmS-6.js", @@ -784,13 +798,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "C7eIgk1J.js", + "file": "CDaMcsB4.js", "name": "profil", "src": "pages/mitgliederbereich/profil.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_CUq_0rkE.js", "_C8kQt0fa.js", "_DaSgy0Cl.js" @@ -801,7 +815,7 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "zfimBmFS.js", + "file": "BlI1POL_.js", "name": "mitgliedschaft", "src": "pages/mitgliedschaft.vue", "isDynamicEntry": true, @@ -813,7 +827,7 @@ const client_manifest = { "_DkeYb0_S.js", "_BteKZQ9T.js", "_Czdc6-TI.js", - "_Qy3ajxTk.js" + "_C_U-NUAd.js" ] }, "pages/passwort-vergessen.vue": { @@ -821,13 +835,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "D8fhH48e.js", + "file": "DYva3pFh.js", "name": "passwort-vergessen", "src": "pages/passwort-vergessen.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_C8kQt0fa.js", "_DaSgy0Cl.js", "_CUq_0rkE.js" @@ -838,13 +852,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "DCJCzUpS.js", + "file": "DeoKPvBx.js", "name": "registrieren", "src": "pages/registrieren.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_C8kQt0fa.js", "_DaSgy0Cl.js", "_CUq_0rkE.js" @@ -855,12 +869,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "B45eiNl0.js", + "file": "CTuRK0lH.js", "name": "satzung", "src": "pages/satzung.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_BteKZQ9T.js", "node_modules/nuxt/dist/app/entry.js" ] @@ -870,12 +884,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "98faI9Ca.js", + "file": "DjW4xBSP.js", "name": "spielsysteme", "src": "pages/spielsysteme.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_DkeYb0_S.js", "_YJHbYJtA.js", "node_modules/nuxt/dist/app/entry.js", @@ -890,12 +904,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "CNecvSw-.js", + "file": "BqZP274n.js", "name": "termine", "src": "pages/termine.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_YJHbYJtA.js", "node_modules/nuxt/dist/app/entry.js" ] @@ -905,13 +919,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "BejXl4Ry.js", + "file": "DXBW1M-0.js", "name": "anfaenger", "src": "pages/training/anfaenger.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_DaSgy0Cl.js" ] }, @@ -920,13 +934,13 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "BKvBGWYj.js", + "file": "Q0RpvB7T.js", "name": "index", "src": "pages/training/index.vue", "isDynamicEntry": true, "imports": [ "node_modules/nuxt/dist/app/entry.js", - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_C5SyyWEb.js" ] }, @@ -935,12 +949,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "B7Xj1aAc.js", + "file": "BkzaDkuN.js", "name": "trainer", "src": "pages/training/trainer.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "node_modules/nuxt/dist/app/entry.js" ] }, @@ -949,12 +963,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "DInduCQ0.js", + "file": "DjcJk1g8.js", "name": "tt-regeln", "src": "pages/tt-regeln.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_KxVBmS-6.js", "_BteKZQ9T.js", "_B4mSF5Ac.js", @@ -969,7 +983,7 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "BLO7WCWA.js", + "file": "B0zS1aUD.js", "name": "ueber-uns", "src": "pages/ueber-uns.vue", "isDynamicEntry": true, @@ -977,7 +991,7 @@ const client_manifest = { "node_modules/nuxt/dist/app/entry.js", "_CWEkTB1z.js", "_B4mSF5Ac.js", - "_Qy3ajxTk.js" + "_C_U-NUAd.js" ] }, "pages/verein/geschichte.vue": { @@ -985,12 +999,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "B1qBTDuC.js", + "file": "KBGCy6kF.js", "name": "geschichte", "src": "pages/verein/geschichte.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "node_modules/nuxt/dist/app/entry.js" ] }, @@ -999,12 +1013,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "BZfUaD4r.js", + "file": "BKEdaV_4.js", "name": "satzung", "src": "pages/verein/satzung.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "node_modules/nuxt/dist/app/entry.js" ] }, @@ -1013,12 +1027,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "D565ijOU.js", + "file": "Bh7iK8Ct.js", "name": "tt-regeln", "src": "pages/verein/tt-regeln.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "node_modules/nuxt/dist/app/entry.js" ] }, @@ -1027,12 +1041,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "NvbKvUY5.js", + "file": "3DraxWaO.js", "name": "ueber-uns", "src": "pages/verein/ueber-uns.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "node_modules/nuxt/dist/app/entry.js" ] }, @@ -1041,12 +1055,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "BwIK_w7L.js", + "file": "DF1dYnic.js", "name": "vereinsmeisterschaften", "src": "pages/vereinsmeisterschaften.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "_CrCcIvVp.js", "node_modules/nuxt/dist/app/entry.js" ] @@ -1056,12 +1070,12 @@ const client_manifest = { "module": true, "prefetch": true, "preload": true, - "file": "B78Xcyyd.js", + "file": "BajRASxo.js", "name": "vorstand", "src": "pages/vorstand.vue", "isDynamicEntry": true, "imports": [ - "_Qy3ajxTk.js", + "_C_U-NUAd.js", "node_modules/nuxt/dist/app/entry.js" ] } diff --git a/.output/server/chunks/build/server.mjs b/.output/server/chunks/build/server.mjs index b05e72a..5c98b61 100644 --- a/.output/server/chunks/build/server.mjs +++ b/.output/server/chunks/build/server.mjs @@ -1,5 +1,5 @@ import process from 'node:process';globalThis._importMeta_=globalThis._importMeta_||{url:"file:///_entry.js",env:process.env};import { defineComponent, shallowRef, h, resolveComponent, hasInjectionContext, inject, computed, getCurrentInstance, createElementBlock, provide, cloneVNode, ref, Suspense, Fragment, createApp, shallowReactive, mergeProps, withCtx, createVNode, createTextVNode, unref, toDisplayString, toRef, onErrorCaptured, onServerPrefetch, resolveDynamicComponent, reactive, effectScope, isReadonly, isRef, isShallow, isReactive, toRaw, defineAsyncComponent, getCurrentScope, useSSRContext } from 'vue'; -import { p as parseQuery, c as createError$1, o as hasProtocol, q as isScriptProtocol, m as joinURL, w as withQuery, t as sanitizeStatusCode, v as withTrailingSlash, x as withoutTrailingSlash, y as getContext, $ as $fetch$1, z as createHooks, A as executeAsync, B as toRouteMatcher, C as createRouter$1, D as defu } from '../nitro/nitro.mjs'; +import { q as parseQuery, c as createError$1, t as hasProtocol, v as isScriptProtocol, o as joinURL, w as withQuery, x as sanitizeStatusCode, y as withTrailingSlash, z as withoutTrailingSlash, A as getContext, $ as $fetch$1, B as createHooks, C as executeAsync, D as toRouteMatcher, E as createRouter$1, F as defu } from '../nitro/nitro.mjs'; import { b as baseURL } from '../routes/renderer.mjs'; import { defineStore, createPinia, setActivePinia, shouldHydrate } from 'pinia'; import { RouterView, useRoute as useRoute$1, useRouter as useRouter$1, createMemoryHistory, createRouter, START_LOCATION } from 'vue-router'; @@ -538,7 +538,7 @@ const _routes = [ { name: "mitgliedschaft", path: "/mitgliedschaft", - component: () => import('./mitgliedschaft-C0k1hAkJ.mjs') + component: () => import('./mitgliedschaft-ChvgsgBw.mjs') }, { name: "training", @@ -639,6 +639,11 @@ const _routes = [ meta: { ...__nuxt_page_meta$1 || {}, ...{ "middleware": "auth" } }, component: () => import('./profil-Dm_3uuTL.mjs') }, + { + name: "cms-mitgliedschaftsantraege", + path: "/cms/mitgliedschaftsantraege", + component: () => import('./mitgliedschaftsantraege-UTBvvWQL.mjs') + }, { name: "mitgliederbereich-mitglieder", path: "/mitgliederbereich/mitglieder", @@ -2064,6 +2069,22 @@ const _sfc_main$4 = { }), _: 1 }, _parent)); + _push(ssrRenderComponent(_component_NuxtLink, { + to: "/cms/mitgliedschaftsantraege", + onClick: ($event) => showCmsDropdown.value = false, + class: "block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors" + }, { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + _push2(` Mitgliedschaftsanträge `); + } else { + return [ + createTextVNode(" Mitgliedschaftsanträge ") + ]; + } + }), + _: 1 + }, _parent)); _push(ssrRenderComponent(_component_NuxtLink, { to: "/cms/benutzer", onClick: ($event) => showCmsDropdown.value = false, @@ -2647,6 +2668,22 @@ const _sfc_main$4 = { }), _: 1 }, _parent)); + _push(ssrRenderComponent(_component_NuxtLink, { + to: "/cms/mitgliedschaftsantraege", + onClick: ($event) => isMobileMenuOpen.value = false, + class: "block px-4 py-2 text-sm text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors" + }, { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + _push2(` Mitgliedschaftsanträge `); + } else { + return [ + createTextVNode(" Mitgliedschaftsanträge ") + ]; + } + }), + _: 1 + }, _parent)); _push(ssrRenderComponent(_component_NuxtLink, { to: "/cms/benutzer", onClick: ($event) => isMobileMenuOpen.value = false, @@ -2718,7 +2755,7 @@ const _sfc_main$3 = { computed(() => authStore.isAdmin); return (_ctx, _push, _parent, _attrs) => { const _component_NuxtLink = __nuxt_component_0$1; - _push(`

© ${ssrInterpolate(unref(currentYear))} Harheimer TC

`); + _push(`

© ${ssrInterpolate(unref(currentYear))} Harheimer TC 1954 e.V.

`); _push(ssrRenderComponent(_component_NuxtLink, { to: "/impressum", class: "text-gray-400 hover:text-primary-400 transition-colors" diff --git a/.output/server/chunks/nitro/nitro.mjs b/.output/server/chunks/nitro/nitro.mjs index 02b2aa4..1df1d8b 100644 --- a/.output/server/chunks/nitro/nitro.mjs +++ b/.output/server/chunks/nitro/nitro.mjs @@ -986,6 +986,20 @@ function isError(input) { function getQuery(event) { return getQuery$1(event.path || ""); } +function getRouterParams(event, opts = {}) { + let params = event.context.params || {}; + if (opts.decode) { + params = { ...params }; + for (const key in params) { + params[key] = decode$1(params[key]); + } + } + return params; +} +function getRouterParam(event, name, opts = {}) { + const params = getRouterParams(event, opts); + return params[name]; +} function isMethod(event, expected, allowHead) { if (typeof expected === "string") { if (event.method === expected) { @@ -1421,6 +1435,7 @@ const setHeaders = setResponseHeaders; function setResponseHeader(event, name, value) { event.node.res.setHeader(name, value); } +const setHeader = setResponseHeader; function appendResponseHeader(event, name, value) { let current = event.node.res.getHeader(name); if (!current) { @@ -4293,7 +4308,7 @@ function _expandFromEnv(value) { const _inlineRuntimeConfig = { "app": { "baseURL": "/", - "buildId": "e0e6f75d-0441-44da-af83-0c8ad93a6ce1", + "buildId": "45b20f41-8a8a-4096-94d4-3b6174a1f364", "buildAssetsDir": "/_nuxt/", "cdnURL": "" }, @@ -4320,7 +4335,16 @@ const _inlineRuntimeConfig = { } } }, - "public": {} + "public": { + "baseUrl": "http://localhost:3100", + "nodeEnv": "production" + }, + "jwtSecret": "local_development_secret_key_change_in_production", + "encryptionKey": "local_development_encryption_key_change_in_production", + "smtpHost": "smtp.gmail.com", + "smtpPort": "587", + "smtpUser": "tsschulz@tsschulz.de", + "smtpPass": "hitomisan" }; const envOptions = { prefix: "NITRO_", @@ -4741,646 +4765,653 @@ const assets = { "/documents/Tischtennisregeln light.pdf": { "type": "application/pdf", "etag": "\"5177b-y/88q2+Y3RRechJMqWhse21KRdQ\"", - "mtime": "2025-10-22T13:11:00.216Z", + "mtime": "2025-10-22T20:59:36.495Z", "size": 333691, "path": "../public/documents/Tischtennisregeln light.pdf" }, "/documents/satzung.pdf": { "type": "application/pdf", "etag": "\"5c7cf-L0A3nT8D24T9sD57FFbij3QRpzw\"", - "mtime": "2025-10-22T13:11:00.216Z", + "mtime": "2025-10-22T20:59:36.495Z", "size": 378831, "path": "../public/documents/satzung.pdf" }, "/data/mannschaften.csv": { "type": "text/csv; charset=utf-8", "etag": "\"858-l94GKn8Q0I5RQnhrM0ZPJsYUmcw\"", - "mtime": "2025-10-22T13:11:00.216Z", + "mtime": "2025-10-22T20:59:36.495Z", "size": 2136, "path": "../public/data/mannschaften.csv" }, "/data/spielsysteme.csv": { "type": "text/csv; charset=utf-8", "etag": "\"9bc-4npLrNHYClsD0TKV5vSifxitfV0\"", - "mtime": "2025-10-22T13:11:00.216Z", + "mtime": "2025-10-22T20:59:36.495Z", "size": 2492, "path": "../public/data/spielsysteme.csv" }, "/data/termine.csv": { "type": "text/csv; charset=utf-8", "etag": "\"91-TO3ZO4MdjBcYgGXZQ1ZuM6BIMbg\"", - "mtime": "2025-10-22T13:11:00.216Z", + "mtime": "2025-10-22T20:59:36.495Z", "size": 145, "path": "../public/data/termine.csv" }, "/data/vereinsmeisterschaften.csv": { "type": "text/csv; charset=utf-8", "etag": "\"989-X8AB+Zegy2xUbjDtbQcXhuuyBDQ\"", - "mtime": "2025-10-22T13:11:00.216Z", + "mtime": "2025-10-22T20:59:36.495Z", "size": 2441, "path": "../public/data/vereinsmeisterschaften.csv" }, "/images/club_about_us.png": { "type": "image/png", "etag": "\"202e56-s4fLsHEgoAgKJeBRuI1qxPmqHV0\"", - "mtime": "2025-10-22T13:11:00.216Z", + "mtime": "2025-10-22T20:59:36.495Z", "size": 2109014, "path": "../public/images/club_about_us.png" }, - "/_nuxt/48ve60fm.js": { + "/_nuxt/3DraxWaO.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"557-e04q6bcr4Wja5BTiX/uCY9jcaOk\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 1367, - "path": "../public/_nuxt/48ve60fm.js" + "etag": "\"280-jM2Q0d5+GWxpIUcG0GYvafqZndU\"", + "mtime": "2025-10-22T20:59:36.489Z", + "size": 640, + "path": "../public/_nuxt/3DraxWaO.js" }, - "/_nuxt/5IKOyEE8.js": { + "/_nuxt/63DEGUI_.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"1dc0-sdXqwLgmwgjTpdupnog1D2XvTCI\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 7616, - "path": "../public/_nuxt/5IKOyEE8.js" + "etag": "\"3519-+oX2Gt3f4Ol8LbiLD8kjREapFrI\"", + "mtime": "2025-10-22T20:59:36.489Z", + "size": 13593, + "path": "../public/_nuxt/63DEGUI_.js" }, "/_nuxt/6EY4_GXp.js": { "type": "text/javascript; charset=utf-8", "etag": "\"19e-CzjelZROfgCPWyXIjP0DPtd+UHQ\"", - "mtime": "2025-10-22T13:11:00.212Z", + "mtime": "2025-10-22T20:59:36.489Z", "size": 414, "path": "../public/_nuxt/6EY4_GXp.js" }, - "/_nuxt/98faI9Ca.js": { + "/_nuxt/B0zS1aUD.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"176f-Gul01Vy6H1nIj/J6Evkza40euzs\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 5999, - "path": "../public/_nuxt/98faI9Ca.js" + "etag": "\"10c2-dv0Fl2k5xVTgZGSg+Mb3Lw57By0\"", + "mtime": "2025-10-22T20:59:36.489Z", + "size": 4290, + "path": "../public/_nuxt/B0zS1aUD.js" }, - "/_nuxt/B1qBTDuC.js": { + "/_nuxt/B23trXK4.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"28f-QYm6Get+5Mzb1lzPJ8hvAe28acY\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 655, - "path": "../public/_nuxt/B1qBTDuC.js" + "etag": "\"1168-Ulrp7wDqGkKdNtiJFHTxmUmXldI\"", + "mtime": "2025-10-22T20:59:36.489Z", + "size": 4456, + "path": "../public/_nuxt/B23trXK4.js" }, - "/_nuxt/B45eiNl0.js": { + "/_nuxt/B2n3gjaF.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"177f-3YD26+ncRnQbpp7rVZ0QJr+HX68\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 6015, - "path": "../public/_nuxt/B45eiNl0.js" + "etag": "\"284d-StTmVE2goIj/bkeU+DQWtF5djqY\"", + "mtime": "2025-10-22T20:59:36.489Z", + "size": 10317, + "path": "../public/_nuxt/B2n3gjaF.js" }, "/_nuxt/B4mSF5Ac.js": { "type": "text/javascript; charset=utf-8", "etag": "\"185-hHs3mU4qOcQAkGQaPrUYGaG0yao\"", - "mtime": "2025-10-22T13:11:00.212Z", + "mtime": "2025-10-22T20:59:36.489Z", "size": 389, "path": "../public/_nuxt/B4mSF5Ac.js" }, "/_nuxt/B6R2Q32I.js": { "type": "text/javascript; charset=utf-8", "etag": "\"201e-GGMCGamCalF6U5Sbg/B20sKxrIo\"", - "mtime": "2025-10-22T13:11:00.212Z", + "mtime": "2025-10-22T20:59:36.489Z", "size": 8222, "path": "../public/_nuxt/B6R2Q32I.js" }, - "/_nuxt/B78Xcyyd.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"1c75-rFu7RUc6kc60RdqL8XR0NdFDnc4\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 7285, - "path": "../public/_nuxt/B78Xcyyd.js" - }, - "/_nuxt/B7Xj1aAc.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"576-wvTUKwMg9qOA14pTnqI8eQAEtNQ\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 1398, - "path": "../public/_nuxt/B7Xj1aAc.js" - }, "/_nuxt/BC4PNGtJ.js": { "type": "text/javascript; charset=utf-8", "etag": "\"175-33lu59Ps/+kwbPv/hVeUdrq4wmI\"", - "mtime": "2025-10-22T13:11:00.212Z", + "mtime": "2025-10-22T20:59:36.489Z", "size": 373, "path": "../public/_nuxt/BC4PNGtJ.js" }, - "/_nuxt/BEK-x74F.js": { + "/_nuxt/BKEdaV_4.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"4d4-kZ1wV31cCE82/gi3TwVt9FXlMjY\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 1236, - "path": "../public/_nuxt/BEK-x74F.js" - }, - "/_nuxt/BKvBGWYj.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"dbb-looAEvNluQQQG2rCd36WkGzY0bg\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 3515, - "path": "../public/_nuxt/BKvBGWYj.js" - }, - "/_nuxt/BLO7WCWA.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"10c2-FX+SfYWWIpQ5Wr9+WYC4vLBgqFo\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 4290, - "path": "../public/_nuxt/BLO7WCWA.js" - }, - "/_nuxt/BOTV4xuv.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"685-DGanTIL4vuEjlVxsu2YztXQU4TU\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 1669, - "path": "../public/_nuxt/BOTV4xuv.js" - }, - "/_nuxt/BP5itt78.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"1bf1-PFn9LpgUce7LseLwE8kwk8PYIjU\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 7153, - "path": "../public/_nuxt/BP5itt78.js" - }, - "/_nuxt/BVRiFo7f.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"dbf-bs5Lp4co8JtdiCR4NBZg2xEEpZE\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 3519, - "path": "../public/_nuxt/BVRiFo7f.js" - }, - "/_nuxt/BZfUaD4r.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"575-c1zjR9YC0NJ+luznUKhlHywnLIU\"", - "mtime": "2025-10-22T13:11:00.212Z", + "etag": "\"575-7dArWk+Lq+idxLmRYNuhsVs6dyY\"", + "mtime": "2025-10-22T20:59:36.489Z", "size": 1397, - "path": "../public/_nuxt/BZfUaD4r.js" + "path": "../public/_nuxt/BKEdaV_4.js" }, - "/_nuxt/BejXl4Ry.js": { + "/_nuxt/BKO8ChwC.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"c21-THfBhB+phiMWwj52TWqKEc46S68\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 3105, - "path": "../public/_nuxt/BejXl4Ry.js" + "etag": "\"1556-oBoOEvng5fLTMsnncUpTAorP7OA\"", + "mtime": "2025-10-22T20:59:36.489Z", + "size": 5462, + "path": "../public/_nuxt/BKO8ChwC.js" }, - "/_nuxt/BlTx75vv.js": { + "/_nuxt/BZLaJF8o.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"4b0d-SYN+JGx+AoiD4WQzkl8eBc5bCKE\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 19213, - "path": "../public/_nuxt/BlTx75vv.js" + "etag": "\"685-ABktMZGm1wLg51VMeeUc8FwDm3U\"", + "mtime": "2025-10-22T20:59:36.489Z", + "size": 1669, + "path": "../public/_nuxt/BZLaJF8o.js" }, - "/_nuxt/Bqdk1y3w.js": { + "/_nuxt/B_w5gbrC.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"1168-vzXKi1TdkggT+/pFZ+jnojZnkb0\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 4456, - "path": "../public/_nuxt/Bqdk1y3w.js" + "etag": "\"1dc0-VemUVNnKtYX+T2aPkZsAvATr8cU\"", + "mtime": "2025-10-22T20:59:36.489Z", + "size": 7616, + "path": "../public/_nuxt/B_w5gbrC.js" + }, + "/_nuxt/BajRASxo.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"1c75-LIoxtfDXb6Jr1B6UMiyvEaj5Z4E\"", + "mtime": "2025-10-22T20:59:36.489Z", + "size": 7285, + "path": "../public/_nuxt/BajRASxo.js" + }, + "/_nuxt/Bh7iK8Ct.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"285-BwOxA71t8tuKLd2eyZbRUTw9zNI\"", + "mtime": "2025-10-22T20:59:36.489Z", + "size": 645, + "path": "../public/_nuxt/Bh7iK8Ct.js" + }, + "/_nuxt/Bivc7aFF.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"2029-cRwtIdVsJWLW1S5Bl927TM/rr0o\"", + "mtime": "2025-10-22T20:59:36.489Z", + "size": 8233, + "path": "../public/_nuxt/Bivc7aFF.js" + }, + "/_nuxt/BkzaDkuN.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"576-NhTZbQReSTTs1ZfN6M1Z4HmU0nw\"", + "mtime": "2025-10-22T20:59:36.489Z", + "size": 1398, + "path": "../public/_nuxt/BkzaDkuN.js" + }, + "/_nuxt/BlI1POL_.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"4ffa-A8rd0CPC0t7g53lsZYsAJjptrnU\"", + "mtime": "2025-10-22T20:59:36.489Z", + "size": 20474, + "path": "../public/_nuxt/BlI1POL_.js" + }, + "/_nuxt/BqZP274n.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"c5c-+adaLKVcGsLD+mC5x2NddJAVdgw\"", + "mtime": "2025-10-22T20:59:36.489Z", + "size": 3164, + "path": "../public/_nuxt/BqZP274n.js" }, "/_nuxt/BteKZQ9T.js": { "type": "text/javascript; charset=utf-8", "etag": "\"1ea-kmrGdt5SPmt15EiBI7kR9gXMQM0\"", - "mtime": "2025-10-22T13:11:00.212Z", + "mtime": "2025-10-22T20:59:36.489Z", "size": 490, "path": "../public/_nuxt/BteKZQ9T.js" }, - "/_nuxt/Bu6whu8C.js": { + "/_nuxt/C3627_Er.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"b10-nKEX1NVrhxQEZmlAcTkwKBrNtLw\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 2832, - "path": "../public/_nuxt/Bu6whu8C.js" - }, - "/_nuxt/BwIK_w7L.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"1822-6dEahpAKXI/TK0hFyCl49ArZqEY\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 6178, - "path": "../public/_nuxt/BwIK_w7L.js" + "etag": "\"1860-WfaogXmfix1Il+z3+9JvyOxfUWc\"", + "mtime": "2025-10-22T20:59:36.489Z", + "size": 6240, + "path": "../public/_nuxt/C3627_Er.js" }, "/_nuxt/C5SyyWEb.js": { "type": "text/javascript; charset=utf-8", "etag": "\"2a5-06iX+CL3i0ysaqW9nu7Eg2YzDhQ\"", - "mtime": "2025-10-22T13:11:00.212Z", + "mtime": "2025-10-22T20:59:36.490Z", "size": 677, "path": "../public/_nuxt/C5SyyWEb.js" }, - "/_nuxt/C7eIgk1J.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"1744-lAvECLLy5SEbev3ptOubSf6ERts\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 5956, - "path": "../public/_nuxt/C7eIgk1J.js" - }, - "/_nuxt/C8Sqpv2D.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"ce3-YwLppZpDboI+NkmnaPh2t48UfIc\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 3299, - "path": "../public/_nuxt/C8Sqpv2D.js" - }, "/_nuxt/C8kQt0fa.js": { "type": "text/javascript; charset=utf-8", "etag": "\"197-7X99z1xphxry8OnMwU7Ofs/uE0Q\"", - "mtime": "2025-10-22T13:11:00.212Z", + "mtime": "2025-10-22T20:59:36.489Z", "size": 407, "path": "../public/_nuxt/C8kQt0fa.js" }, - "/_nuxt/CGF4oWdy.js": { + "/_nuxt/C9SglkVL.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"2029-aijTO8hjQ/8anru9bdrWBbssOQ0\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 8233, - "path": "../public/_nuxt/CGF4oWdy.js" + "etag": "\"ee2-953PjDhBAep38tbBTU3/pMqFyww\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 3810, + "path": "../public/_nuxt/C9SglkVL.js" }, - "/_nuxt/CJOOzUp1.js": { + "/_nuxt/CDaMcsB4.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"e7c-abEOF8FnxTHl8uqZzfvQtwM7xQ0\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 3708, - "path": "../public/_nuxt/CJOOzUp1.js" + "etag": "\"1744-EVBf4KmUuGbFen1F02KGsrF+hGI\"", + "mtime": "2025-10-22T20:59:36.489Z", + "size": 5956, + "path": "../public/_nuxt/CDaMcsB4.js" }, - "/_nuxt/CNecvSw-.js": { + "/_nuxt/CIPPGKqt.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"c5c-Vxx1Vl2cy4DKfApPrnfqjJGu4mM\"", - "mtime": "2025-10-22T13:11:00.212Z", - "size": 3164, - "path": "../public/_nuxt/CNecvSw-.js" + "etag": "\"4b0d-816oZHek7FCyY/6MP6B+bBO2gp0\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 19213, + "path": "../public/_nuxt/CIPPGKqt.js" + }, + "/_nuxt/CJ4iaRop.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"b10-OECled9SRrrXt0OinwRJj2Ix5YM\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 2832, + "path": "../public/_nuxt/CJ4iaRop.js" + }, + "/_nuxt/CPS6rtgg.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"ce3-xn3ASS1lK+fGckHc82Lgq4Yq+m8\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 3299, + "path": "../public/_nuxt/CPS6rtgg.js" + }, + "/_nuxt/CTuRK0lH.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"177f-9AHJLxzT1QXHm05RkMKEx5CuaVU\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 6015, + "path": "../public/_nuxt/CTuRK0lH.js" }, "/_nuxt/CUq_0rkE.js": { "type": "text/javascript; charset=utf-8", "etag": "\"12d-JV4KW1fgT85/V3Ap13X4q2h9U3g\"", - "mtime": "2025-10-22T13:11:00.212Z", + "mtime": "2025-10-22T20:59:36.490Z", "size": 301, "path": "../public/_nuxt/CUq_0rkE.js" }, + "/_nuxt/CW9krljs.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"dbf-R7lqTpP/JoeELqrwiRklrTVdlP0\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 3519, + "path": "../public/_nuxt/CW9krljs.js" + }, "/_nuxt/CWEkTB1z.js": { "type": "text/javascript; charset=utf-8", "etag": "\"198-ej4DRqc3/5nSwWU3c6wbOD3Ib9w\"", - "mtime": "2025-10-22T13:11:00.213Z", + "mtime": "2025-10-22T20:59:36.490Z", "size": 408, "path": "../public/_nuxt/CWEkTB1z.js" }, - "/_nuxt/CcTzWjtb.js": { + "/_nuxt/C_U-NUAd.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"1b53-7sXS9Eo6BF70losMTtjfeTnzuMc\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 6995, - "path": "../public/_nuxt/CcTzWjtb.js" + "etag": "\"13f-zgvIssSMnG3JGf/eGC7PlZzbsiY\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 319, + "path": "../public/_nuxt/C_U-NUAd.js" }, - "/_nuxt/CfC7HTR7.js": { + "/_nuxt/CfIOs31W.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"ce3-f6E+TQZ0cn0DngZFZ/llILKhR+U\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 3299, - "path": "../public/_nuxt/CfC7HTR7.js" + "etag": "\"1b53-/AyR6v+gM/GpLFgDD/LVb+RcB5E\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 6995, + "path": "../public/_nuxt/CfIOs31W.js" }, "/_nuxt/CkzaQq3X.js": { "type": "text/javascript; charset=utf-8", "etag": "\"17d-+xKrHjeww4bpFFkkjUNLD/ebn5A\"", - "mtime": "2025-10-22T13:11:00.213Z", + "mtime": "2025-10-22T20:59:36.490Z", "size": 381, "path": "../public/_nuxt/CkzaQq3X.js" }, - "/_nuxt/CmbXHhwn.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"34627-sJs3VvEFZsQc97235P81vL3fQ7w\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 214567, - "path": "../public/_nuxt/CmbXHhwn.js" - }, - "/_nuxt/CmqI4hAm.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"3303-qQEt/I4NkGGsu6UXajdFXo6tV2g\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 13059, - "path": "../public/_nuxt/CmqI4hAm.js" - }, "/_nuxt/CrCcIvVp.js": { "type": "text/javascript; charset=utf-8", "etag": "\"274-9U7hEMtgHqdnQopnKeJsBKqKyKw\"", - "mtime": "2025-10-22T13:11:00.213Z", + "mtime": "2025-10-22T20:59:36.490Z", "size": 628, "path": "../public/_nuxt/CrCcIvVp.js" }, - "/_nuxt/CtoHBsGq.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"1d9a-CRLOLLJKQ7hJpuU/khDqpJwb7Gs\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 7578, - "path": "../public/_nuxt/CtoHBsGq.js" - }, "/_nuxt/Cx4UcKGu.js": { "type": "text/javascript; charset=utf-8", "etag": "\"19d-5AMD0EnFEjOkM3qKDpC/NZZzwDI\"", - "mtime": "2025-10-22T13:11:00.213Z", + "mtime": "2025-10-22T20:59:36.490Z", "size": 413, "path": "../public/_nuxt/Cx4UcKGu.js" }, + "/_nuxt/Cx_3U4cr.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"3303-kA/sRWGwuhzDWqIGIpOPH/t2uxY\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 13059, + "path": "../public/_nuxt/Cx_3U4cr.js" + }, "/_nuxt/Czdc6-TI.js": { "type": "text/javascript; charset=utf-8", "etag": "\"165-EMJ/yP2qajGIw0CL3y+L/hvMM/8\"", - "mtime": "2025-10-22T13:11:00.213Z", + "mtime": "2025-10-22T20:59:36.490Z", "size": 357, "path": "../public/_nuxt/Czdc6-TI.js" }, - "/_nuxt/D-Zvexq_.js": { + "/_nuxt/CzgMfPlN.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"3513-Iq9EH7eoFoUFbEDaBuNl4Kn0hIs\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 13587, - "path": "../public/_nuxt/D-Zvexq_.js" - }, - "/_nuxt/D565ijOU.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"285-YgZrqminf9zIdLY+bX14vmiLhi8\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 645, - "path": "../public/_nuxt/D565ijOU.js" - }, - "/_nuxt/D7LlSYAz.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"1860-NkUAN/E2dYGxU5Dbn31phV3D+Ao\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 6240, - "path": "../public/_nuxt/D7LlSYAz.js" - }, - "/_nuxt/D8fhH48e.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"bf0-AlFLYfCSbLnqtkmmy37eoh53bns\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 3056, - "path": "../public/_nuxt/D8fhH48e.js" + "etag": "\"1d9a-uI9Gu5wonCfTrcA2TPR74Z738js\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 7578, + "path": "../public/_nuxt/CzgMfPlN.js" }, "/_nuxt/DAACT36i.js": { "type": "text/javascript; charset=utf-8", "etag": "\"1f9-dVOk5jAwb0VlMLJevIcT+s2NTgM\"", - "mtime": "2025-10-22T13:11:00.213Z", + "mtime": "2025-10-22T20:59:36.490Z", "size": 505, "path": "../public/_nuxt/DAACT36i.js" }, - "/_nuxt/DBoACobG.js": { + "/_nuxt/DCC-mN0A.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"e71-ep/UQrCdlnMhZY0/vKCU0dyPSbs\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 3697, - "path": "../public/_nuxt/DBoACobG.js" + "etag": "\"faa-gR+Y5ET9eX9AUBll6ClCrUgMFfM\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 4010, + "path": "../public/_nuxt/DCC-mN0A.js" }, - "/_nuxt/DCJCzUpS.js": { + "/_nuxt/DDYB2ueJ.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"16eb-eWqLnHuigtG6QjOQsbMjBg5Xbzw\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 5867, - "path": "../public/_nuxt/DCJCzUpS.js" + "etag": "\"1bf1-tbqPDIoIIOyOPuxr7lu+9bz6I9A\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 7153, + "path": "../public/_nuxt/DDYB2ueJ.js" }, - "/_nuxt/DInduCQ0.js": { + "/_nuxt/DF1dYnic.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"201d-j9TZmWiLkTlPv0ZYQr+Vp6iuVNM\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 8221, - "path": "../public/_nuxt/DInduCQ0.js" + "etag": "\"1822-F6/LMMvYSDyJLbWtmIcGXOjwdVI\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 6178, + "path": "../public/_nuxt/DF1dYnic.js" }, - "/_nuxt/DNeqJiJt.js": { + "/_nuxt/DUL8f07u.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"1caa-vEKuI7T+n+iulyWeqwhfliuJTbY\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 7338, - "path": "../public/_nuxt/DNeqJiJt.js" - }, - "/_nuxt/DU1f7CIy.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"284d-DrllkU2HFUngvk4U350kzsgEuW8\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 10317, - "path": "../public/_nuxt/DU1f7CIy.js" + "etag": "\"2078-oBpmJYe4/ACOJIjr6c2SOleXnZU\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 8312, + "path": "../public/_nuxt/DUL8f07u.js" }, "/_nuxt/DUm-savV.js": { "type": "text/javascript; charset=utf-8", "etag": "\"378-gmau5tfuGGvvXo5HRs29CR7slTs\"", - "mtime": "2025-10-22T13:11:00.213Z", + "mtime": "2025-10-22T20:59:36.490Z", "size": 888, "path": "../public/_nuxt/DUm-savV.js" }, + "/_nuxt/DXBW1M-0.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"c21-nyO/XGf9b9rgF4K7nt/w5/wQfCc\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 3105, + "path": "../public/_nuxt/DXBW1M-0.js" + }, + "/_nuxt/DYva3pFh.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"bf0-VZJ4omCiGedJWNFfqojHT+R0eEg\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 3056, + "path": "../public/_nuxt/DYva3pFh.js" + }, "/_nuxt/DaSgy0Cl.js": { "type": "text/javascript; charset=utf-8", "etag": "\"11f-soKnh1qfNJj5nvt+IcgQXYvg/z4\"", - "mtime": "2025-10-22T13:11:00.213Z", + "mtime": "2025-10-22T20:59:36.490Z", "size": 287, "path": "../public/_nuxt/DaSgy0Cl.js" }, "/_nuxt/DdHhmCne.js": { "type": "text/javascript; charset=utf-8", "etag": "\"22d-uKYfhsDcUsz2NrXOJmxptUGZdyE\"", - "mtime": "2025-10-22T13:11:00.213Z", + "mtime": "2025-10-22T20:59:36.490Z", "size": 557, "path": "../public/_nuxt/DdHhmCne.js" }, + "/_nuxt/DeoKPvBx.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"16eb-TpYobwJ2U3v+YsJtPupLa/phq5M\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 5867, + "path": "../public/_nuxt/DeoKPvBx.js" + }, + "/_nuxt/DjW4xBSP.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"176f-tMmDsyv4oOPCPALGcnStj1TcZ+0\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 5999, + "path": "../public/_nuxt/DjW4xBSP.js" + }, + "/_nuxt/DjcJk1g8.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"201d-oQ9NMDE0anKxIZA105IRItTlM2w\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 8221, + "path": "../public/_nuxt/DjcJk1g8.js" + }, "/_nuxt/DkeYb0_S.js": { "type": "text/javascript; charset=utf-8", "etag": "\"1ce-xiaAbRvqQ+zffTXF3Gc7rq14R0U\"", - "mtime": "2025-10-22T13:11:00.213Z", + "mtime": "2025-10-22T20:59:36.490Z", "size": 462, "path": "../public/_nuxt/DkeYb0_S.js" }, "/_nuxt/DlAUqK2U.js": { "type": "text/javascript; charset=utf-8", "etag": "\"5b-eFCz/UrraTh721pgAl0VxBNR1es\"", - "mtime": "2025-10-22T13:11:00.213Z", + "mtime": "2025-10-22T20:59:36.490Z", "size": 91, "path": "../public/_nuxt/DlAUqK2U.js" }, - "/_nuxt/DmeaandR.js": { + "/_nuxt/DxKvlgrz.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"16e9-qqmKzBVd7HSTvIWa0EcLiJJecrM\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 5865, - "path": "../public/_nuxt/DmeaandR.js" + "etag": "\"ce5-KfKdhndea0VFXu67agen+xsPIzc\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 3301, + "path": "../public/_nuxt/DxKvlgrz.js" }, - "/_nuxt/DrS211Qg.js": { + "/_nuxt/DzTYmaPw.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"1556-Uo0KgZra//87enUwKV/clVnAeAQ\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 5462, - "path": "../public/_nuxt/DrS211Qg.js" + "etag": "\"e7c-cRvhexRpH0kUK+4eKy4JEsaXzes\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 3708, + "path": "../public/_nuxt/DzTYmaPw.js" }, "/_nuxt/FF_cyd6S.js": { "type": "text/javascript; charset=utf-8", "etag": "\"203-rairGPfunSg/yCk4txod3zRSZus\"", - "mtime": "2025-10-22T13:11:00.213Z", + "mtime": "2025-10-22T20:59:36.490Z", "size": 515, "path": "../public/_nuxt/FF_cyd6S.js" }, - "/_nuxt/G3v2TcOj.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"ce5-BD+fl10oS89Pjqn10CK6MM93jjI\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 3301, - "path": "../public/_nuxt/G3v2TcOj.js" - }, "/_nuxt/Harheimer TC.CKfYAfp1.svg": { "type": "image/svg+xml", "etag": "\"1d2535-Tx2lTuuFn2hBqGZOnDan3/OdRU0\"", - "mtime": "2025-10-22T13:11:00.213Z", + "mtime": "2025-10-22T20:59:36.491Z", "size": 1910069, "path": "../public/_nuxt/Harheimer TC.CKfYAfp1.svg" }, + "/_nuxt/KBGCy6kF.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"28f-SmKklly/B2H47JXlCNKIMKctiEc\"", + "mtime": "2025-10-22T20:59:36.490Z", + "size": 655, + "path": "../public/_nuxt/KBGCy6kF.js" + }, + "/_nuxt/KrCelFbA.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"349c7-O4bl7V7+gOCd8ms+yjxFTVsNyFA\"", + "mtime": "2025-10-22T20:59:36.491Z", + "size": 215495, + "path": "../public/_nuxt/KrCelFbA.js" + }, "/_nuxt/KxVBmS-6.js": { "type": "text/javascript; charset=utf-8", "etag": "\"190-F0LVEAqwB2LwyGzW0v9yzLx0v/0\"", - "mtime": "2025-10-22T13:11:00.213Z", + "mtime": "2025-10-22T20:59:36.491Z", "size": 400, "path": "../public/_nuxt/KxVBmS-6.js" }, - "/_nuxt/NvbKvUY5.js": { + "/_nuxt/LPF2GIYR.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"280-Vp6LJtn9tKHeLC622DQhxBeBGGQ\"", - "mtime": "2025-10-22T13:11:00.213Z", - "size": 640, - "path": "../public/_nuxt/NvbKvUY5.js" + "etag": "\"557-80ya3yG6VaGTcTbyJrpOfIs1ttU\"", + "mtime": "2025-10-22T20:59:36.491Z", + "size": 1367, + "path": "../public/_nuxt/LPF2GIYR.js" }, - "/_nuxt/Qy3ajxTk.js": { + "/_nuxt/Q0RpvB7T.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"13f-LgrH17St2xFg+RPGvT3uJRaRfFw\"", - "mtime": "2025-10-22T13:11:00.214Z", - "size": 319, - "path": "../public/_nuxt/Qy3ajxTk.js" + "etag": "\"dbb-upDaFU+NtVLect5iCrq6L3skEmM\"", + "mtime": "2025-10-22T20:59:36.491Z", + "size": 3515, + "path": "../public/_nuxt/Q0RpvB7T.js" }, "/_nuxt/R6Iy1jPP.js": { "type": "text/javascript; charset=utf-8", "etag": "\"137-QCUizOitouzMVC2drCYFTAZmqPU\"", - "mtime": "2025-10-22T13:11:00.214Z", + "mtime": "2025-10-22T20:59:36.491Z", "size": 311, "path": "../public/_nuxt/R6Iy1jPP.js" }, - "/_nuxt/TnZylaYP.js": { + "/_nuxt/S5xR3JqC.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"faa-3YLDJ0Sk0bu1CU/lpGLzt/gdHnk\"", - "mtime": "2025-10-22T13:11:00.214Z", - "size": 4010, - "path": "../public/_nuxt/TnZylaYP.js" + "etag": "\"16e9-iU8tNfwzZDCfrS7SDnGc1zdluhk\"", + "mtime": "2025-10-22T20:59:36.491Z", + "size": 5865, + "path": "../public/_nuxt/S5xR3JqC.js" }, "/_nuxt/XZ6RV9KH.js": { "type": "text/javascript; charset=utf-8", "etag": "\"175-xr1poEaGS4yjOp907AsRAr6XHLI\"", - "mtime": "2025-10-22T13:11:00.214Z", + "mtime": "2025-10-22T20:59:36.491Z", "size": 373, "path": "../public/_nuxt/XZ6RV9KH.js" }, "/_nuxt/YJHbYJtA.js": { "type": "text/javascript; charset=utf-8", "etag": "\"19f-nQw578pUen9o8yYaMA8Bwag6xho\"", - "mtime": "2025-10-22T13:11:00.214Z", + "mtime": "2025-10-22T20:59:36.491Z", "size": 415, "path": "../public/_nuxt/YJHbYJtA.js" }, - "/_nuxt/ZrOCUSmD.js": { - "type": "text/javascript; charset=utf-8", - "etag": "\"ee2-knvq//8tl4tcmRjFy6nWAy0dRBk\"", - "mtime": "2025-10-22T13:11:00.214Z", - "size": 3810, - "path": "../public/_nuxt/ZrOCUSmD.js" - }, - "/_nuxt/entry.DX4WLXSP.css": { + "/_nuxt/entry.BpzTEo9t.css": { "type": "text/css; charset=utf-8", - "etag": "\"c678-CRIAMOlIBHSJE0IJAgCeZIWv6Lo\"", - "mtime": "2025-10-22T13:11:00.214Z", - "size": 50808, - "path": "../public/_nuxt/entry.DX4WLXSP.css" + "etag": "\"c9a3-NCgrzt0JLiO8glyROOfnz74Ctbs\"", + "mtime": "2025-10-22T20:59:36.491Z", + "size": 51619, + "path": "../public/_nuxt/entry.BpzTEo9t.css" }, "/_nuxt/error-404.CbXQcqJW.css": { "type": "text/css; charset=utf-8", "etag": "\"97e-Ty5bTTSEudJkO/DsGUoIf37xYxc\"", - "mtime": "2025-10-22T13:11:00.214Z", + "mtime": "2025-10-22T20:59:36.491Z", "size": 2430, "path": "../public/_nuxt/error-404.CbXQcqJW.css" }, "/_nuxt/error-500.L485xXhD.css": { "type": "text/css; charset=utf-8", "etag": "\"773-jNt1QdCa+iqaSZb1mv/IQWC5p6w\"", - "mtime": "2025-10-22T13:11:00.214Z", + "mtime": "2025-10-22T20:59:36.491Z", "size": 1907, "path": "../public/_nuxt/error-500.L485xXhD.css" }, "/_nuxt/index.ByttcLyP.css": { "type": "text/css; charset=utf-8", "etag": "\"1db-P3imbnjv59PWVm0HNpwpfdEtAK4\"", - "mtime": "2025-10-22T13:11:00.214Z", + "mtime": "2025-10-22T20:59:36.491Z", "size": 475, "path": "../public/_nuxt/index.ByttcLyP.css" }, "/_nuxt/jVj3QaoK.js": { "type": "text/javascript; charset=utf-8", "etag": "\"196-xWXv220Sy3kJeouwzrQ/gnXllWQ\"", - "mtime": "2025-10-22T13:11:00.214Z", + "mtime": "2025-10-22T20:59:36.491Z", "size": 406, "path": "../public/_nuxt/jVj3QaoK.js" }, + "/_nuxt/lhUIkRXy.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"ce3-9mGjWM+ri0cpGUFdcKT3mPtT0MI\"", + "mtime": "2025-10-22T20:59:36.491Z", + "size": 3299, + "path": "../public/_nuxt/lhUIkRXy.js" + }, "/_nuxt/oN0_bS6A.js": { "type": "text/javascript; charset=utf-8", "etag": "\"1d3-DKhiaT2RUlSXk55jBttctUuTQQI\"", - "mtime": "2025-10-22T13:11:00.214Z", + "mtime": "2025-10-22T20:59:36.491Z", "size": 467, "path": "../public/_nuxt/oN0_bS6A.js" }, - "/_nuxt/zfimBmFS.js": { + "/_nuxt/pO5XAgdL.js": { "type": "text/javascript; charset=utf-8", - "etag": "\"1348-nNn41yY4F5mSJKhm7fvAhhbV8HE\"", - "mtime": "2025-10-22T13:11:00.214Z", - "size": 4936, - "path": "../public/_nuxt/zfimBmFS.js" + "etag": "\"1caa-8s46v8ZVjnRPK/d5dpFk/XgPjHM\"", + "mtime": "2025-10-22T20:59:36.491Z", + "size": 7338, + "path": "../public/_nuxt/pO5XAgdL.js" + }, + "/_nuxt/rgKTeSYE.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"e71-IzPUyj76F9mR9c9DaEV7x6UlmP0\"", + "mtime": "2025-10-22T20:59:36.491Z", + "size": 3697, + "path": "../public/_nuxt/rgKTeSYE.js" + }, + "/_nuxt/sVyj_WZX.js": { + "type": "text/javascript; charset=utf-8", + "etag": "\"4d4-T+i1jfWN+C61xV/Shhjh8+auLuI\"", + "mtime": "2025-10-22T20:59:36.491Z", + "size": 1236, + "path": "../public/_nuxt/sVyj_WZX.js" }, "/spielplaene/1. Mannschaft 2025⁄2026.pdf": { "type": "application/pdf", "etag": "\"64c6-+477M+gD/spwpWR9NO/tMJ/inCc\"", - "mtime": "2025-10-22T13:11:00.216Z", + "mtime": "2025-10-22T20:59:36.495Z", "size": 25798, "path": "../public/spielplaene/1. Mannschaft 2025⁄2026.pdf" }, "/spielplaene/2. Mannschaft 2025⁄2026.pdf": { "type": "application/pdf", "etag": "\"5bfa-DRJMHLV15iss67lEISoGqSYmZjE\"", - "mtime": "2025-10-22T13:11:00.216Z", + "mtime": "2025-10-22T20:59:36.495Z", "size": 23546, "path": "../public/spielplaene/2. Mannschaft 2025⁄2026.pdf" }, "/spielplaene/3. Mannschaft 2025⁄2026.pdf": { "type": "application/pdf", "etag": "\"7447-w933CPQdXhkWJ2AZOVdY0UgJnPo\"", - "mtime": "2025-10-22T13:11:00.216Z", + "mtime": "2025-10-22T20:59:36.495Z", "size": 29767, "path": "../public/spielplaene/3. Mannschaft 2025⁄2026.pdf" }, "/spielplaene/4. Mannschaft 2025⁄2026.pdf": { "type": "application/pdf", "etag": "\"6a9b-4TPGn1yQlFUMRj7oB43SN//Np9o\"", - "mtime": "2025-10-22T13:11:00.216Z", + "mtime": "2025-10-22T20:59:36.495Z", "size": 27291, "path": "../public/spielplaene/4. Mannschaft 2025⁄2026.pdf" }, "/spielplaene/5. Mannschaft 2025⁄2026.pdf": { "type": "application/pdf", "etag": "\"6523-5VUfCMaoiNhcwHhptHHTVJ3lSwQ\"", - "mtime": "2025-10-22T13:11:00.217Z", + "mtime": "2025-10-22T20:59:36.495Z", "size": 25891, "path": "../public/spielplaene/5. Mannschaft 2025⁄2026.pdf" }, "/spielplaene/Jugend 11 2025⁄2026.pdf": { "type": "application/pdf", "etag": "\"52e9-3Rrk9UKUxPh80pBJ0w9oLVbe5dA\"", - "mtime": "2025-10-22T13:11:00.216Z", + "mtime": "2025-10-22T20:59:36.495Z", "size": 21225, "path": "../public/spielplaene/Jugend 11 2025⁄2026.pdf" }, "/_nuxt/builds/latest.json": { "type": "application/json", - "etag": "\"47-tRaFdY23bxqlMKg9QwRKnIwjatk\"", - "mtime": "2025-10-22T13:11:00.205Z", + "etag": "\"47-pPnZHFavtyL3EeMBBGnscgWaUBU\"", + "mtime": "2025-10-22T20:59:36.479Z", "size": 71, "path": "../public/_nuxt/builds/latest.json" }, - "/_nuxt/builds/meta/e0e6f75d-0441-44da-af83-0c8ad93a6ce1.json": { + "/_nuxt/builds/meta/45b20f41-8a8a-4096-94d4-3b6174a1f364.json": { "type": "application/json", - "etag": "\"8b-Yl1oKf1vSDtHk7/YZ36RMo5I5Pk\"", - "mtime": "2025-10-22T13:11:00.199Z", + "etag": "\"8b-yZdIx2ZJ0JxJsWVmRZIafVtrrdE\"", + "mtime": "2025-10-22T20:59:36.474Z", "size": 139, - "path": "../public/_nuxt/builds/meta/e0e6f75d-0441-44da-af83-0c8ad93a6ce1.json" + "path": "../public/_nuxt/builds/meta/45b20f41-8a8a-4096-94d4-3b6174a1f364.json" } }; @@ -5611,6 +5642,10 @@ const _lazy_EK_x5_ = () => import('../routes/api/galerie.get.mjs'); const _lazy_jYLuY1 = () => import('../routes/api/members.delete.mjs'); const _lazy_HGAbG3 = () => import('../routes/api/members.get.mjs'); const _lazy_XNetVh = () => import('../routes/api/members.post.mjs'); +const _lazy_AgsX_N = () => import('../routes/api/membership/applications.get.mjs'); +const _lazy_7df2qh = () => import('../routes/api/membership/download/_id_.get.mjs'); +const _lazy_Od0rSJ = () => import('../routes/api/membership/generate-pdf.post.mjs'); +const _lazy_LVkPS8 = () => import('../routes/api/membership/update-status.put.mjs'); const _lazy_BC53DZ = () => import('../routes/api/news-public.get.mjs'); const _lazy_fmI2VY = () => import('../routes/api/news.delete.mjs'); const _lazy_lBurQc = () => import('../routes/api/news.get.mjs'); @@ -5644,6 +5679,10 @@ const handlers = [ { route: '/api/members', handler: _lazy_jYLuY1, lazy: true, middleware: false, method: "delete" }, { route: '/api/members', handler: _lazy_HGAbG3, lazy: true, middleware: false, method: "get" }, { route: '/api/members', handler: _lazy_XNetVh, lazy: true, middleware: false, method: "post" }, + { route: '/api/membership/applications', handler: _lazy_AgsX_N, lazy: true, middleware: false, method: "get" }, + { route: '/api/membership/download/:id', handler: _lazy_7df2qh, lazy: true, middleware: false, method: "get" }, + { route: '/api/membership/generate-pdf', handler: _lazy_Od0rSJ, lazy: true, middleware: false, method: "post" }, + { route: '/api/membership/update-status', handler: _lazy_LVkPS8, lazy: true, middleware: false, method: "put" }, { route: '/api/news-public', handler: _lazy_BC53DZ, lazy: true, middleware: false, method: "get" }, { route: '/api/news', handler: _lazy_fmI2VY, lazy: true, middleware: false, method: "delete" }, { route: '/api/news', handler: _lazy_lBurQc, lazy: true, middleware: false, method: "get" }, @@ -6112,5 +6151,5 @@ trapUnhandledNodeErrors(); setupGracefulShutdown(listener, nitroApp); const nodeServer = {}; -export { $fetch as $, executeAsync as A, toRouteMatcher as B, createRouter$1 as C, defu as D, hash$1 as E, nodeServer as F, deleteCookie as a, getQuery as b, createError$1 as c, defineEventHandler as d, getResponseStatusText as e, getResponseStatus as f, getCookie as g, defineRenderHandler as h, destr as i, joinRelativeURL as j, getRouteRules as k, relative as l, joinURL as m, useNitroApp as n, hasProtocol as o, parseQuery as p, isScriptProtocol as q, readBody as r, setCookie as s, sanitizeStatusCode as t, useRuntimeConfig as u, withTrailingSlash as v, withQuery as w, withoutTrailingSlash as x, getContext as y, createHooks as z }; +export { $fetch as $, getContext as A, createHooks as B, executeAsync as C, toRouteMatcher as D, createRouter$1 as E, defu as F, hash$1 as G, nodeServer as H, deleteCookie as a, getRouterParam as b, createError$1 as c, defineEventHandler as d, setHeader as e, getQuery as f, getCookie as g, getResponseStatusText as h, getResponseStatus as i, joinRelativeURL as j, defineRenderHandler as k, destr as l, getRouteRules as m, relative as n, joinURL as o, useNitroApp as p, parseQuery as q, readBody as r, setCookie as s, hasProtocol as t, useRuntimeConfig as u, isScriptProtocol as v, withQuery as w, sanitizeStatusCode as x, withTrailingSlash as y, withoutTrailingSlash as z }; //# sourceMappingURL=nitro.mjs.map diff --git a/.output/server/chunks/routes/api/auth/reset-password.post.mjs b/.output/server/chunks/routes/api/auth/reset-password.post.mjs index e126dc9..0ba1f09 100644 --- a/.output/server/chunks/routes/api/auth/reset-password.post.mjs +++ b/.output/server/chunks/routes/api/auth/reset-password.post.mjs @@ -1,7 +1,7 @@ import { d as defineEventHandler, r as readBody, c as createError } from '../../../nitro/nitro.mjs'; import { r as readUsers, h as hashPassword, w as writeUsers } from '../../../_/auth.mjs'; import nodemailer from 'nodemailer'; -import require$$1 from 'crypto'; +import crypto from 'crypto'; import 'node:http'; import 'node:https'; import 'node:events'; @@ -33,7 +33,7 @@ const resetPassword_post = defineEventHandler(async (event) => { message: "Falls ein Konto mit dieser E-Mail existiert, wurde eine E-Mail gesendet." }; } - const tempPassword = require$$1.randomBytes(8).toString("hex"); + const tempPassword = crypto.randomBytes(8).toString("hex"); const hashedPassword = await hashPassword(tempPassword); user.password = hashedPassword; user.passwordResetRequired = true; diff --git a/.output/server/chunks/routes/api/auth/reset-password.post.mjs.map b/.output/server/chunks/routes/api/auth/reset-password.post.mjs.map index c9367cc..91b5976 100644 --- a/.output/server/chunks/routes/api/auth/reset-password.post.mjs.map +++ b/.output/server/chunks/routes/api/auth/reset-password.post.mjs.map @@ -1 +1 @@ -{"version":3,"file":"reset-password.post.mjs","sources":["../../../../../../server/api/auth/reset-password.post.js"],"sourcesContent":null,"names":["crypto"],"mappings":";;;;;;;;;;;;;;;;;AAIA,2BAAA,kBAAA,CAAA,OAAA,KAAA,KAAA;AACA,EAAA,IAAA;AACA,IAAA,MAAA,IAAA,GAAA,MAAA,QAAA,CAAA,KAAA,CAAA;AACA,IAAA,MAAA,EAAA,OAAA,GAAA,IAAA;AAEA,IAAA,IAAA,CAAA,KAAA,EAAA;AACA,MAAA,MAAA,WAAA,CAAA;AAAA,QACA,UAAA,EAAA,GAAA;AAAA,QACA,OAAA,EAAA;AAAA,OACA,CAAA;AAAA,IACA;AAGA,IAAA,MAAA,KAAA,GAAA,MAAA,SAAA,EAAA;AACA,IAAA,MAAA,IAAA,GAAA,KAAA,CAAA,IAAA,CAAA,CAAA,CAAA,KAAA,CAAA,CAAA,MAAA,WAAA,EAAA,KAAA,KAAA,CAAA,WAAA,EAAA,CAAA;AAGA,IAAA,IAAA,CAAA,IAAA,EAAA;AACA,MAAA,OAAA;AAAA,QACA,OAAA,EAAA,IAAA;AAAA,QACA,OAAA,EAAA;AAAA,OACA;AAAA,IACA;AAGA,IAAA,MAAA,eAAAA,UAAA,CAAA,WAAA,CAAA,CAAA,CAAA,CAAA,SAAA,KAAA,CAAA;AACA,IAAA,MAAA,cAAA,GAAA,MAAA,YAAA,CAAA,YAAA,CAAA;AAGA,IAAA,IAAA,CAAA,QAAA,GAAA,cAAA;AACA,IAAA,IAAA,CAAA,qBAAA,GAAA,IAAA;AACA,IAAA,MAAA,YAAA,GAAA,MAAA,GAAA,CAAA,CAAA,CAAA,KAAA,EAAA,EAAA,KAAA,IAAA,CAAA,EAAA,GAAA,IAAA,GAAA,CAAA,CAAA;AACA,IAAA,MAAA,WAAA,YAAA,CAAA;AAGA,IAAA,MAAA,WAAA,GAAA,WAAA,eAAA,CAAA;AAAA,MACA,IAAA,EAAA,OAAA,CAAA,GAAA,CAAA,SAAA,IAAA,gBAAA;AAAA,MACA,IAAA,EAAA,OAAA,CAAA,GAAA,CAAA,SAAA,IAAA,GAAA;AAAA,MACA,MAAA,EAAA,KAAA;AAAA,MACA,IAAA,EAAA;AAAA,QACA,IAAA,EAAA,QAAA,GAAA,CAAA,SAAA;AAAA,QACA,IAAA,EAAA,QAAA,GAAA,CAAA;AAAA;AACA,KACA,CAAA;AAEA,IAAA,MAAA,WAAA,GAAA;AAAA,MACA,IAAA,EAAA,OAAA,CAAA,GAAA,CAAA,SAAA,IAAA,wBAAA;AAAA,MACA,IAAA,IAAA,CAAA,KAAA;AAAA,MACA,OAAA,EAAA,yCAAA;AAAA,MACA,IAAA,EAAA;AAAA;AAAA,iBAAA,EAEA,KAAA,IAAA,CAAA;AAAA;AAAA,sDAAA,EAEA,YAAA,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAAA;AAAA,KAOA;AAEA,IAAA,MAAA,WAAA,CAAA,SAAA,WAAA,CAAA;AAEA,IAAA,OAAA;AAAA,MACA,OAAA,EAAA,IAAA;AAAA,MACA,OAAA,EAAA;AAAA,KACA;AAAA,EACA,SAAA,KAAA,EAAA;AACA,IAAA,OAAA,CAAA,KAAA,CAAA,0BAAA,KAAA,CAAA;AAEA,IAAA,OAAA;AAAA,MACA,OAAA,EAAA,IAAA;AAAA,MACA,OAAA,EAAA;AAAA,KACA;AAAA,EACA;AACA,CAAA,CAAA;;;;"} \ No newline at end of file +{"version":3,"file":"reset-password.post.mjs","sources":["../../../../../../server/api/auth/reset-password.post.js"],"sourcesContent":null,"names":[],"mappings":";;;;;;;;;;;;;;;;;AAIA,2BAAA,kBAAA,CAAA,OAAA,KAAA,KAAA;AACA,EAAA,IAAA;AACA,IAAA,MAAA,IAAA,GAAA,MAAA,QAAA,CAAA,KAAA,CAAA;AACA,IAAA,MAAA,EAAA,OAAA,GAAA,IAAA;AAEA,IAAA,IAAA,CAAA,KAAA,EAAA;AACA,MAAA,MAAA,WAAA,CAAA;AAAA,QACA,UAAA,EAAA,GAAA;AAAA,QACA,OAAA,EAAA;AAAA,OACA,CAAA;AAAA,IACA;AAGA,IAAA,MAAA,KAAA,GAAA,MAAA,SAAA,EAAA;AACA,IAAA,MAAA,IAAA,GAAA,KAAA,CAAA,IAAA,CAAA,CAAA,CAAA,KAAA,CAAA,CAAA,MAAA,WAAA,EAAA,KAAA,KAAA,CAAA,WAAA,EAAA,CAAA;AAGA,IAAA,IAAA,CAAA,IAAA,EAAA;AACA,MAAA,OAAA;AAAA,QACA,OAAA,EAAA,IAAA;AAAA,QACA,OAAA,EAAA;AAAA,OACA;AAAA,IACA;AAGA,IAAA,MAAA,eAAA,MAAA,CAAA,WAAA,CAAA,CAAA,CAAA,CAAA,SAAA,KAAA,CAAA;AACA,IAAA,MAAA,cAAA,GAAA,MAAA,YAAA,CAAA,YAAA,CAAA;AAGA,IAAA,IAAA,CAAA,QAAA,GAAA,cAAA;AACA,IAAA,IAAA,CAAA,qBAAA,GAAA,IAAA;AACA,IAAA,MAAA,YAAA,GAAA,MAAA,GAAA,CAAA,CAAA,CAAA,KAAA,EAAA,EAAA,KAAA,IAAA,CAAA,EAAA,GAAA,IAAA,GAAA,CAAA,CAAA;AACA,IAAA,MAAA,WAAA,YAAA,CAAA;AAGA,IAAA,MAAA,WAAA,GAAA,WAAA,eAAA,CAAA;AAAA,MACA,IAAA,EAAA,OAAA,CAAA,GAAA,CAAA,SAAA,IAAA,gBAAA;AAAA,MACA,IAAA,EAAA,OAAA,CAAA,GAAA,CAAA,SAAA,IAAA,GAAA;AAAA,MACA,MAAA,EAAA,KAAA;AAAA,MACA,IAAA,EAAA;AAAA,QACA,IAAA,EAAA,QAAA,GAAA,CAAA,SAAA;AAAA,QACA,IAAA,EAAA,QAAA,GAAA,CAAA;AAAA;AACA,KACA,CAAA;AAEA,IAAA,MAAA,WAAA,GAAA;AAAA,MACA,IAAA,EAAA,OAAA,CAAA,GAAA,CAAA,SAAA,IAAA,wBAAA;AAAA,MACA,IAAA,IAAA,CAAA,KAAA;AAAA,MACA,OAAA,EAAA,yCAAA;AAAA,MACA,IAAA,EAAA;AAAA;AAAA,iBAAA,EAEA,KAAA,IAAA,CAAA;AAAA;AAAA,sDAAA,EAEA,YAAA,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAAA;AAAA,KAOA;AAEA,IAAA,MAAA,WAAA,CAAA,SAAA,WAAA,CAAA;AAEA,IAAA,OAAA;AAAA,MACA,OAAA,EAAA,IAAA;AAAA,MACA,OAAA,EAAA;AAAA,KACA;AAAA,EACA,SAAA,KAAA,EAAA;AACA,IAAA,OAAA,CAAA,KAAA,CAAA,0BAAA,KAAA,CAAA;AAEA,IAAA,OAAA;AAAA,MACA,OAAA,EAAA,IAAA;AAAA,MACA,OAAA,EAAA;AAAA,KACA;AAAA,EACA;AACA,CAAA,CAAA;;;;"} \ No newline at end of file diff --git a/.output/server/chunks/routes/api/news.delete.mjs b/.output/server/chunks/routes/api/news.delete.mjs index ec3f6f3..9079295 100644 --- a/.output/server/chunks/routes/api/news.delete.mjs +++ b/.output/server/chunks/routes/api/news.delete.mjs @@ -1,4 +1,4 @@ -import { d as defineEventHandler, g as getCookie, c as createError, b as getQuery } from '../../nitro/nitro.mjs'; +import { d as defineEventHandler, g as getCookie, c as createError, f as getQuery } from '../../nitro/nitro.mjs'; import { b as verifyToken, e as getUserById } from '../../_/auth.mjs'; import { d as deleteNews } from '../../_/news.mjs'; import 'node:http'; diff --git a/.output/server/chunks/routes/api/termine-manage.delete.mjs b/.output/server/chunks/routes/api/termine-manage.delete.mjs index 917255b..191e325 100644 --- a/.output/server/chunks/routes/api/termine-manage.delete.mjs +++ b/.output/server/chunks/routes/api/termine-manage.delete.mjs @@ -1,4 +1,4 @@ -import { d as defineEventHandler, g as getCookie, c as createError, b as getQuery } from '../../nitro/nitro.mjs'; +import { d as defineEventHandler, g as getCookie, c as createError, f as getQuery } from '../../nitro/nitro.mjs'; import { b as verifyToken, e as getUserById } from '../../_/auth.mjs'; import { d as deleteTermin } from '../../_/termine.mjs'; import 'node:http'; diff --git a/.output/server/chunks/routes/renderer.mjs b/.output/server/chunks/routes/renderer.mjs index 8148525..f1d7ba5 100644 --- a/.output/server/chunks/routes/renderer.mjs +++ b/.output/server/chunks/routes/renderer.mjs @@ -1,5 +1,5 @@ import { createRenderer, getRequestDependencies, getPreloadLinks, getPrefetchLinks } from 'vue-bundle-renderer/runtime'; -import { j as joinRelativeURL, u as useRuntimeConfig, e as getResponseStatusText, f as getResponseStatus, h as defineRenderHandler, b as getQuery, c as createError, i as destr, k as getRouteRules, l as relative, m as joinURL, n as useNitroApp } from '../nitro/nitro.mjs'; +import { j as joinRelativeURL, u as useRuntimeConfig, h as getResponseStatusText, i as getResponseStatus, k as defineRenderHandler, f as getQuery, c as createError, l as destr, m as getRouteRules, n as relative, o as joinURL, p as useNitroApp } from '../nitro/nitro.mjs'; import { renderToString } from 'vue/server-renderer'; import { createHead as createHead$1, propsToString, renderSSRHead } from 'unhead/server'; import { stringify, uneval } from 'devalue'; @@ -272,7 +272,7 @@ async function renderInlineStyles(usedModules) { const renderSSRHeadOptions = {"omitLineBreaks":true}; -const entryFileName = "CmbXHhwn.js"; +const entryFileName = "KrCelFbA.js"; globalThis.__buildAssetsURL = buildAssetsURL; globalThis.__publicAssetsURL = publicAssetsURL; diff --git a/.output/server/index.mjs b/.output/server/index.mjs index c35cd81..fbc5b40 100644 --- a/.output/server/index.mjs +++ b/.output/server/index.mjs @@ -1,6 +1,6 @@ import process from 'node:process';globalThis._importMeta_={url:import.meta.url,env:process.env};import 'node:http'; import 'node:https'; -export { F as default } from './chunks/nitro/nitro.mjs'; +export { H as default } from './chunks/nitro/nitro.mjs'; import 'node:events'; import 'node:buffer'; import 'node:fs'; diff --git a/DATENSCHUTZ.md b/DATENSCHUTZ.md new file mode 100644 index 0000000..902b61e --- /dev/null +++ b/DATENSCHUTZ.md @@ -0,0 +1,144 @@ +# Datenschutz und Verschlüsselung - Harheimer TC + +## 🔒 **Verschlüsselung implementiert** + +### **Verschlüsselte Daten:** +- **Mitgliedschaftsanträge:** Alle persönlichen Daten (Name, Adresse, E-Mail, Telefon, Bankdaten) +- **Benutzerdaten:** E-Mail, Name, Telefon (in `users.json`) +- **Mitgliederdaten:** Name, E-Mail, Telefon, Adresse (in `members.json`) + +### **Nicht verschlüsselte Daten:** +- **News-Artikel:** Autor-Namen (öffentlich sichtbar) +- **Konfiguration:** Vereinsdaten, Trainingszeiten (öffentlich) +- **Termine:** Event-Daten (öffentlich) + +## 🛡️ **Sicherheitsmaßnahmen** + +### **Verschlüsselung:** +- **Algorithmus:** AES-256-GCM +- **Schlüsselableitung:** PBKDF2 mit 100.000 Iterationen +- **Salt:** 64 Bytes zufällig generiert +- **IV:** 16 Bytes zufällig generiert +- **Auth-Tag:** 16 Bytes für Integrität + +### **Passwort-Sicherheit:** +- **Hashing:** bcryptjs mit Salt +- **Rounds:** 10 (konfigurierbar) + +### **Datenzugriff:** +- **Authentifizierung:** JWT-Token +- **Autorisierung:** Rollenbasierte Zugriffskontrolle +- **Session-Management:** Sichere Cookies + +## 📋 **Datenverarbeitung** + +### **Mitgliedschaftsanträge:** +1. **Eingabe:** Formular auf Website +2. **Verschlüsselung:** Sofortige Verschlüsselung aller persönlichen Daten +3. **Speicherung:** Verschlüsselte Dateien in `server/data/membership-applications/` +4. **E-Mail:** Benachrichtigung an Vorstand/Trainer +5. **Genehmigung:** Entschlüsselung und Übertragung in Mitgliederliste +6. **Archivierung:** Original-Anträge bleiben verschlüsselt gespeichert + +### **Benutzerdaten:** +- **Registrierung:** E-Mail, Name, Telefon +- **Login:** E-Mail, Passwort (gehashed) +- **Profil:** Bearbeitbare Kontaktdaten +- **Rollen:** admin, vorstand, mitglied + +### **Mitgliederdaten:** +- **Manuell hinzugefügt:** Direkt in `members.json` +- **Aus Anträgen:** Nach Genehmigung übertragen +- **Zugriff:** Nur für "vorstand" Rolle sichtbar + +## 🔄 **Datenfluss** + +``` +Mitgliedschaftsformular + ↓ +Verschlüsselung (AES-256-GCM) + ↓ +Speicherung (verschlüsselt) + ↓ +E-Mail-Benachrichtigung + ↓ +CMS-Verwaltung + ↓ +Genehmigung/Ablehnung + ↓ +Bei Genehmigung: Übertragung in Mitgliederliste +``` + +## ⚖️ **GDPR-Konformität** + +### **Rechtmäßigkeit:** +- **Einwilligung:** Explizite Zustimmung im Formular +- **Vertragserfüllung:** Mitgliedschaftsvertrag +- **Berechtigtes Interesse:** Vereinsverwaltung + +### **Betroffenenrechte:** +- **Auskunft:** Über gespeicherte Daten +- **Berichtigung:** Korrektur falscher Daten +- **Löschung:** Recht auf Vergessenwerden +- **Einschränkung:** Verarbeitung einschränken +- **Datenübertragbarkeit:** Export der Daten + +### **Datenspeicherung:** +- **Zweckbindung:** Nur für Vereinszwecke +- **Minimierung:** Nur notwendige Daten +- **Genauigkeit:** Aktuelle und korrekte Daten +- **Speicherbegrenzung:** Löschung nach Zweckerfüllung + +## 🔧 **Technische Implementierung** + +### **Verschlüsselungsfunktionen:** +```javascript +// Verschlüsselung +encryptObject(data, password) + +// Entschlüsselung +decryptObject(encryptedData, password) + +// Passwort-Hashing +hashPassword(password, salt) + +// Passwort-Verifikation +verifyPassword(password, hash, salt) +``` + +### **Environment-Variablen:** +```bash +# Verschlüsselungsschlüssel (MUSS in Produktion geändert werden!) +ENCRYPTION_KEY=your_secure_encryption_key_here + +# JWT-Secret +JWT_SECRET=your_jwt_secret_here +``` + +## 🚨 **Sicherheitshinweise** + +### **Produktionsumgebung:** +- **Verschlüsselungsschlüssel:** MUSS geändert werden! +- **JWT-Secret:** MUSS geändert werden! +- **HTTPS:** Zwingend erforderlich +- **Backup:** Verschlüsselte Backups + +### **Entwicklungsumgebung:** +- **E-Mails:** Nur an `tsschulz@tsschulz.de` +- **Test-Daten:** Keine echten persönlichen Daten +- **Logs:** Keine sensiblen Daten in Logs + +## 📞 **Kontakt** + +Bei Fragen zum Datenschutz: +- **E-Mail:** tsschulz@tsschulz.de +- **Verantwortlicher:** Torsten Schulz + +--- + +**Stand:** Januar 2025 +**Version:** 1.0 +**Letzte Aktualisierung:** Implementierung der Verschlüsselung + + + diff --git a/components/Footer.vue b/components/Footer.vue index 9045372..0afdf6c 100644 --- a/components/Footer.vue +++ b/components/Footer.vue @@ -3,7 +3,7 @@

- © {{ currentYear }} Harheimer TC + © {{ currentYear }} Harheimer TC 1954 e.V.

diff --git a/components/MembershipNoQuestions.vue b/components/MembershipNoQuestions.vue new file mode 100644 index 0000000..88072eb --- /dev/null +++ b/components/MembershipNoQuestions.vue @@ -0,0 +1,149 @@ + + + + + + diff --git a/components/Navigation.vue b/components/Navigation.vue index fc0c45e..bb7d826 100644 --- a/components/Navigation.vue +++ b/components/Navigation.vue @@ -252,6 +252,11 @@ class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors"> Einstellungen + + Mitgliedschaftsanträge + @@ -464,6 +469,10 @@ class="block px-4 py-2 text-sm text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"> Einstellungen + + Mitgliedschaftsanträge + Benutzerverwaltung diff --git a/env.example b/env.example index a65ae19..ff2f80d 100644 --- a/env.example +++ b/env.example @@ -1,16 +1,32 @@ -# SMTP-Konfiguration für E-Mail-Versand +# Environment-Konfiguration für lokale Entwicklung # Diese Datei sollte in .env umbenannt werden und nicht in Git committet werden -# SMTP-Server (z.B. Gmail, GMX, etc.) +# Node.js Environment +NODE_ENV=development + +# Server-Konfiguration +PORT=3100 +NUXT_PUBLIC_BASE_URL=http://localhost:3100 + +# SMTP-Konfiguration für E-Mail-Versand (nur für lokale Tests) SMTP_HOST=smtp.gmail.com SMTP_PORT=587 -SMTP_USER=j.dichmann@gmx.de -SMTP_PASS=your_email_password_here +SMTP_USER=tsschulz@tsschulz.de +SMTP_PASS=your_app_password_here # Alternative für GMX: # SMTP_HOST=mail.gmx.net # SMTP_PORT=587 -# Alternative für andere Provider: -# SMTP_HOST=smtp.your-provider.com -# SMTP_PORT=587 +# JWT Secret für lokale Entwicklung +JWT_SECRET=local_development_secret_key_change_in_production + +# Verschlüsselungsschlüssel für persönliche Daten (MUSS in Produktion geändert werden!) +ENCRYPTION_KEY=local_development_encryption_key_change_in_production + +# Datenbank/Datei-Pfade +DATA_PATH=server/data + +# Debug-Modi +DEBUG=true +VERBOSE_LOGGING=true diff --git a/nuxt.config.js b/nuxt.config.js index 0d569c4..314d0d0 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -6,7 +6,29 @@ export default defineNuxtConfig({ nitro: { preset: 'node-server', - dev: false + dev: process.env.NODE_ENV !== 'production' + }, + + // Erzwinge Dev-Port und Host zuverlässig für `npm run dev` + devServer: { + port: 3100, + host: '0.0.0.0' + }, + + runtimeConfig: { + // Private keys (only available on server-side) + jwtSecret: process.env.JWT_SECRET || 'local_development_secret_key_change_in_production', + encryptionKey: process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production', + smtpHost: process.env.SMTP_HOST || 'smtp.gmail.com', + smtpPort: process.env.SMTP_PORT || 587, + smtpUser: process.env.SMTP_USER || 'tsschulz@tsschulz.de', + smtpPass: process.env.SMTP_PASS || '', + + // Public keys (exposed to client-side) + public: { + baseUrl: process.env.NUXT_PUBLIC_BASE_URL || 'http://localhost:3100', + nodeEnv: process.env.NODE_ENV || 'development' + } }, app: { @@ -37,6 +59,54 @@ export default defineNuxtConfig({ css: ['~/assets/css/main.css'], - compatibilityDate: '2024-04-03' + compatibilityDate: '2024-04-03', + + hooks: { + 'build:before': async () => { + const fs = await import('fs/promises') + const path = await import('path') + + // Erstelle uploads Verzeichnis im public Ordner + const uploadsDir = path.join(process.cwd(), '.output', 'public', 'uploads') + try { + await fs.mkdir(uploadsDir, { recursive: true }) + console.log('✅ Uploads-Verzeichnis erstellt:', uploadsDir) + } catch (error) { + console.log('ℹ️ Uploads-Verzeichnis bereits vorhanden oder Fehler:', error.message) + } + + // Erstelle temp Verzeichnis für LaTeX-Kompilierung + const tempDir = path.join(process.cwd(), '.output', 'temp') + try { + await fs.mkdir(tempDir, { recursive: true }) + console.log('✅ Temp-Verzeichnis erstellt:', tempDir) + } catch (error) { + console.log('ℹ️ Temp-Verzeichnis bereits vorhanden oder Fehler:', error.message) + } + }, + + 'build:after': async () => { + const fs = await import('fs/promises') + const path = await import('path') + + // Erstelle uploads Verzeichnis im public Ordner (nach dem Build) + const uploadsDir = path.join(process.cwd(), '.output', 'public', 'uploads') + try { + await fs.mkdir(uploadsDir, { recursive: true }) + console.log('✅ Uploads-Verzeichnis nach Build erstellt:', uploadsDir) + } catch (error) { + console.log('ℹ️ Uploads-Verzeichnis bereits vorhanden oder Fehler:', error.message) + } + + // Erstelle temp Verzeichnis für LaTeX-Kompilierung (nach dem Build) + const tempDir = path.join(process.cwd(), '.output', 'temp') + try { + await fs.mkdir(tempDir, { recursive: true }) + console.log('✅ Temp-Verzeichnis nach Build erstellt:', tempDir) + } catch (error) { + console.log('ℹ️ Temp-Verzeichnis bereits vorhanden oder Fehler:', error.message) + } + } + } }) diff --git a/package-lock.json b/package-lock.json index 57a5acc..ef7ad6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "multer": "^2.0.2", "nodemailer": "^7.0.9", "nuxt": "^4.1.3", + "pdf-lib": "^1.17.1", "pdf-parse": "^2.4.5", "pinia": "^3.0.3", "vue": "^3.5.22" @@ -2718,6 +2719,24 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@pinia/nuxt": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/@pinia/nuxt/-/nuxt-0.11.2.tgz", @@ -8254,6 +8273,12 @@ "integrity": "sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==", "license": "MIT" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parse-ms": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", @@ -8375,6 +8400,24 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/pdf-parse": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz", diff --git a/package.json b/package.json index c4e0fe0..199811a 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "multer": "^2.0.2", "nodemailer": "^7.0.9", "nuxt": "^4.1.3", + "pdf-lib": "^1.17.1", "pdf-parse": "^2.4.5", "pinia": "^3.0.3", "vue": "^3.5.22" diff --git a/pages/cms/mitgliedschaftsantraege.vue b/pages/cms/mitgliedschaftsantraege.vue new file mode 100644 index 0000000..b970f0c --- /dev/null +++ b/pages/cms/mitgliedschaftsantraege.vue @@ -0,0 +1,317 @@ + + + + + + diff --git a/pages/mitgliedschaft.vue b/pages/mitgliedschaft.vue index 65a44a7..adde999 100644 --- a/pages/mitgliedschaft.vue +++ b/pages/mitgliedschaft.vue @@ -1,14 +1,562 @@ - + \ No newline at end of file diff --git a/public/uploads/Beitrittserklärung_Mustermann_Max-3.pdf b/public/uploads/Beitrittserklärung_Mustermann_Max-3.pdf new file mode 100644 index 0000000..8695781 Binary files /dev/null and b/public/uploads/Beitrittserklärung_Mustermann_Max-3.pdf differ diff --git a/public/uploads/beitrittserklärung_1761170630561.data b/public/uploads/beitrittserklärung_1761170630561.data new file mode 100644 index 0000000..2bc8fc4 --- /dev/null +++ b/public/uploads/beitrittserklärung_1761170630561.data @@ -0,0 +1 @@ +gNCsLBXSvmB3axKuSGH1YPk8nnWkAXzK8gu81Xfc2MvGz4iZ4IBhCtl+geidl/ZkdZC+qYXgb3BnrY4dmRdVb++IJU3TpLdkRthdkVKuTZOwXE/YxqlNApFT+bWUw21V0riC1Clkx0zY4kW33zZHCDk+rpPfjW+fk8jRp9uvKFChu9SlT5DMriO/s3R0IU/fU5YN9DG1FCnZT1LkpH6Lr7FhbqHAo6VFpF6Xo4KkuyS+WMIvdxS/mf3tyx1Th1Wc3kE/ljHeJviRBOXTQlOK4DkJZfir4JAhPdgXzYZan2z9WDCVq5DjzsAGZ1x7FSJHbM62Fg3NWlrnnY+FtTkHaBHRfTa7tVSeCE/re0b03HOQtwzt12gxt9/LwYXiKidFaRpceXP7oJVurOJaW/KnquAJMRs9XaY6EZQ0G3+HrDy6yx4/uj9lnlhIAGAvWEAKrVDLk2HWc6B7ud7lbU3J+a8AYRMz2rvdkdXUs0NgPJ7CfyCoQTIcvK9VVL5h3+8o/54L6qQEAV2rmcCqt32/0XXB3+ZlkpXZFvqWBn3hAV7WhKou3R8wdxBZimPZn7gYb0R4IsN1+cYCblz3pzgehss0hIewHRy+nnzE/aw5zNeE/m2ia0YAJFGtt31F9JqmQc2kvtcQWn4MmB1rJYbV5Htw79ihCCU38ZfIWIThjea8Sbx1DzxKJyr7NlAx7XzlUs4m66QUQOd3O7GP2d3L6Q== \ No newline at end of file diff --git a/public/uploads/beitrittserklärung_1761170630561.pdf b/public/uploads/beitrittserklärung_1761170630561.pdf new file mode 100644 index 0000000..ca8aa57 Binary files /dev/null and b/public/uploads/beitrittserklärung_1761170630561.pdf differ diff --git a/public/uploads/debug-beitritt-1761169973.pdf b/public/uploads/debug-beitritt-1761169973.pdf new file mode 100644 index 0000000..8695781 Binary files /dev/null and b/public/uploads/debug-beitritt-1761169973.pdf differ diff --git a/public/uploads/debug-beitritt-1761170410.pdf b/public/uploads/debug-beitritt-1761170410.pdf new file mode 100644 index 0000000..8695781 Binary files /dev/null and b/public/uploads/debug-beitritt-1761170410.pdf differ diff --git a/scripts/create-fillable-template.js b/scripts/create-fillable-template.js new file mode 100644 index 0000000..20582db --- /dev/null +++ b/scripts/create-fillable-template.js @@ -0,0 +1,357 @@ +import { PDFDocument, StandardFonts, rgb } from 'pdf-lib' +import fs from 'fs' + +async function create() { + const pdfDoc = await PDFDocument.create() + const page = pdfDoc.addPage([595.28, 841.89]) // A4 + const helv = await pdfDoc.embedFont(StandardFonts.Helvetica) + const helvBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold) + const { width, height } = page.getSize() + + // left column moved further left to align with checkboxes + const leftX = 48 + const rightX = 320 + // baseY is the vertical anchor for page-1 content. Increase it by ~5.2mm (≈14.739pt) + const baseY = height - 160 + 14.739 + // shift to apply only to labels and input fields (move those 1cm down) + const labelsShift = -28.35 + const gap = 36 + const labelWidth = 100 + const fieldXOffset = 100 + // Make fields 10% narrower + const fieldWidth = Math.round(180 * 0.9) + // shrink most fields by another 20% on request (applied selectively later) + const fieldShrinkFactor = 0.8 + const fieldHeight = 14 + // raise date fields by 0.4cm (≈11.34pt) when requested; add extra 1.2mm (≈3.4016pt) per user request + const dateRaise = 11.34 + 3.4016 // now ≈14.7416pt + // fixed checkbox positions (do not move with leftX) + const cbX = 48 + const cbYOffset = 5 + + // Header: centered club name and a full-width horizontal bar underneath (~2mm high) + const headerText = 'Harheimer Tischtennis-Club 1954 e.V.' + const headerSize = 20 + const textWidth = helv.widthOfTextAtSize(headerText, headerSize) + const headerX = (width - textWidth) / 2 + const headerY = height - 72 + page.drawText(headerText, { x: headerX, y: headerY, size: headerSize, font: helv }) + // draw full-width bar directly under the header; 2mm ≈ 5.67 points + const barHeight = 5.67 + const barY = headerY - 20 + page.drawRectangle({ x: 0, y: barY, width: width, height: barHeight, color: rgb(0,0,0) }) + + // Labels and lines + // Labels left-aligned in their columns + // Add form title above the fields + const titleText = 'Beitrittserklärung' + const titleSize = 14 + const titleWidth = helvBold.widthOfTextAtSize(titleText, titleSize) + // left-align title above the left labels and move up ~0.7cm (≈20pt) + const titleX = leftX + // move title down by 0.5cm (≈14.17pt) + const titleY = baseY + 24 + 20 - 14.17 + page.drawText(titleText, { x: titleX, y: titleY, size: titleSize, font: helvBold }) + + const subtitle = 'Hiermit beantrage ich,' + const subtitleSize = 12 + const subtitleX = leftX + const subtitleY = titleY - 18 - 14.17 + page.drawText(subtitle, { x: subtitleX, y: subtitleY, size: subtitleSize, font: helv }) + // apply same vertical shift as fields so labels align + const fieldsShift = -14.17 + page.drawText('Name:', { x: leftX, y: baseY + fieldsShift + labelsShift, size: 12, font: helv }) + page.drawText('Vorname:', { x: rightX, y: baseY + fieldsShift + labelsShift, size: 12, font: helv }) + + page.drawText('Straße:', { x: leftX, y: baseY - gap + fieldsShift + labelsShift, size: 12, font: helv }) + page.drawText('PLZ/Wohnort:', { x: rightX, y: baseY - gap + fieldsShift + labelsShift, size: 12, font: helv }) + + page.drawText('Geburtsdatum:', { x: leftX, y: baseY - gap * 2 + fieldsShift + labelsShift, size: 12, font: helv }) + page.drawText('Telefon(privat):', { x: rightX, y: baseY - gap * 2 + fieldsShift + labelsShift, size: 12, font: helv }) + + page.drawText('E-Mail:', { x: leftX, y: baseY - gap * 3 + fieldsShift + labelsShift, size: 12, font: helv }) + page.drawText('Telefon(Mobil):', { x: rightX, y: baseY - gap * 3 + fieldsShift + labelsShift, size: 12, font: helv }) + + // Create form fields + const form = pdfDoc.getForm() + // Place fields on the same baseline as their labels + // We need to move only the input fields on page 1 up by 0.6cm (≈17.01pt) without moving labels. + const labelToFieldYDelta = 2 // small vertical offset so field baseline matches label visually + const lift = 0 // original lift value + // previously raised inputs by 17.01pt (0.6cm); move them down by 5.67pt (0.2cm) + const inputFieldRaise = 11.34 // net upward offset now ~11.34pt + form.createTextField('nachname').addToPage(page, { x: leftX + fieldXOffset, y: baseY - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv }) + form.createTextField('vorname').addToPage(page, { x: rightX + fieldXOffset, y: baseY - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv }) + form.createTextField('strasse').addToPage(page, { x: leftX + fieldXOffset, y: baseY - gap - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv }) + form.createTextField('plz_ort').addToPage(page, { x: rightX + fieldXOffset, y: baseY - gap - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv }) + form.createTextField('geburtsdatum').addToPage(page, { x: leftX + fieldXOffset, y: baseY - gap * 2 - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv }) + form.createTextField('telefon').addToPage(page, { x: rightX + fieldXOffset, y: baseY - gap * 2 - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv }) + form.createTextField('email').addToPage(page, { x: leftX + fieldXOffset, y: baseY - gap * 3 - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv }) + form.createTextField('telefon_mobil').addToPage(page, { x: rightX + fieldXOffset, y: baseY - gap * 3 - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv }) + + // read membership amounts from config (fall back to defaults) + let erw = 120, jug = 72, passv = 30 + try { + const cfg = JSON.parse(fs.readFileSync('server/data/config.json', 'utf8')) + const ms = cfg.mitgliedschaft || [] + erw = (ms.find(m => /Erwachsene/i.test(m.typ)) || ms.find(m => m.typ === 'Erwachsene') || {}).preis || erw + jug = (ms.find(m => /Jugend|Kinder/i.test(m.typ)) || ms.find(m => m.typ === 'Kinder/Jugend') || {}).preis || jug + passv = (ms.find(m => /Passiv/i.test(m.typ)) || {}).preis || passv + } catch (e) { + console.error('Could not read config for membership amounts', e) + } + + const paraLines = [ + 'Den derzeitigen jährlichen Mitgliedsbeitrag in Höhe von', + `€ ${erw},-- (Erwachsene)`, + `€ ${jug},-- (Jugendliche bis zum vollendeten 18. Lebensjahr)`, + `€ ${passv},-- (passive Mitglieder)`, + 'bitte ich per Lastschrift jährlich von meinem Konto einzuziehen.', + 'Hierzu erteile ich beigefügtes SEPA-Lastschriftmandat.', + 'Mir ist bekannt, dass die Mitgliedschaft im Harheimer Tischtennis-Club erst nach Bestätigung durch den', + 'Vorstand Wirksamkeit erlangt. Die Beitragspflicht beginnt mit dem darauf folgenden Monat.', + 'Ich erkenne die Vereinssatzung (erhältlich beim Vorstand bzw. auf der Vereinshomepage) an.' + ] + + // compute intermediate line position and move it down by additional 0.3cm ≈ 8.5pt + const intermediateY = baseY - gap * 3 + fieldsShift - 6 - 14.17 - 8.5 - 8.5 + labelsShift + page.drawText('meine Aufnahme in den Harheimer Tischtennis-Club 1954 e.V. als', { x: leftX, y: intermediateY, size: 12, font: helv }) + + // membership checkboxes + // Keep checkbox labels and boxes at fixed left positions, but move them down by 0.8cm ≈ 22.68pt + const cbLift = -22.68 + page.drawText('aktives Mitglied', { x: cbX + 16, y: baseY - gap * 5 + cbLift + labelsShift, size: 12, font: helv }) + page.drawText('passives Mitglied', { x: cbX + 16, y: baseY - gap * 6 + cbLift + labelsShift, size: 12, font: helv }) + form.createCheckBox('mitglied_aktiv').addToPage(page, { x: cbX, y: baseY - gap * 5 - cbYOffset + cbLift + labelsShift, width: 12, height: 12 }) + form.createCheckBox('mitglied_passiv').addToPage(page, { x: cbX, y: baseY - gap * 6 - cbYOffset + cbLift + labelsShift, width: 12, height: 12 }) + + // place the paragraph below the checkboxes with extra spacing before and after the cost lines (~0.3cm) + try { + // increase gap around cost lines for better readability (~0.6cm) + const beforeCostGap = 17 + const afterCostGap = 17 + const paraStartY = baseY - gap * 6 + cbLift - 28 - beforeCostGap + const lineHeight = 14 + // insert explicit gaps before and after the cost lines (lines 1-3) + let y = paraStartY + // first line + page.drawText(paraLines[0], { x: leftX, y: y, size: 11, font: helv }) + y -= lineHeight + // gap before costs + y -= beforeCostGap + // cost lines (1..3) + for (let k = 1; k <= 3; k++) { + page.drawText(paraLines[k], { x: leftX + 6, y: y, size: 11, font: helv }) + y -= lineHeight + } + // gap after costs + y -= afterCostGap + // remaining lines + for (let i = 4; i < paraLines.length; i++) { + page.drawText(paraLines[i], { x: leftX, y: y, size: 11, font: helv }) + y -= lineHeight + } + // shift following content down by afterCostGap if needed (none after currently) + // now add signature/city line 2 cm below the current text + const twoCm = 56.7 + const signY = y - twoCm + const sigText = 'Frankfurt/Main-Harheim, den' + const sigSize = 12 + page.drawText(sigText + ' ', { x: leftX, y: signY, size: sigSize, font: helv }) + const sigStartX = leftX + helv.widthOfTextAtSize(sigText + ' ', sigSize) + const sigWidth = 220 + // draw thin line for signature + page.drawRectangle({ x: sigStartX, y: signY - 2, width: sigWidth, height: 1, color: rgb(0,0,0) }) + // draw 'Datum' centered under the signature line + const datum = 'Datum' + const datumSize = 11 + const datumX = sigStartX + sigWidth / 2 - helv.widthOfTextAtSize(datum, datumSize) / 2 + page.drawText(datum, { x: datumX, y: signY - 18, size: datumSize, font: helv }) + // create a date form field on page 1 centered under the date line + const dateFieldWidth = 120 + const dateFieldX = sigStartX + sigWidth / 2 - dateFieldWidth / 2 + // position date field so its bottom edge is exactly on the signature line + const signatureLineY = signY - 2 // the 1pt-high rectangle was drawn at signY-2 + // raise date fields by 0.4cm (≈11.34pt) upward relative to the line + const dateRaise = 11.34 + // For page 1 we need to move only the date input up by 5.2mm (≈14.739pt) + const signDatumExtraRaise = 14.739 + form.createTextField('sign_datum').addToPage(page, { x: dateFieldX, y: signatureLineY - fieldHeight + signDatumExtraRaise, width: dateFieldWidth, height: fieldHeight, font: helv }) + // second signature line 3cm below first + const threeCm = 85.04 + const secondY = signY - threeCm + const line2Width = 300 + page.drawRectangle({ x: leftX, y: secondY - 2, width: line2Width, height: 1, color: rgb(0,0,0) }) + const label2 = 'Unterschrift (bei Jugendlichen gesetzlicher Vertreter)' + page.drawText(label2, { x: leftX, y: secondY - 18, size: 11, font: helv }) + } catch (e) { + // ignore + } + + // footer: right-aligned 'Seite 1 von 3' 2cm from bottom + const footerY = 56.7 + const footerText = 'Seite 1 von 3' + const footerSize = 10 + const footerWidth = helv.widthOfTextAtSize(footerText, footerSize) + page.drawText(footerText, { x: width - footerWidth - leftX, y: footerY, size: footerSize, font: helv }) + + // --- Add a second page with the same header and horizontal bar --- + const page2 = pdfDoc.addPage([595.28, 841.89]) + const { width: width2, height: height2 } = page2.getSize() + const textWidth2 = helv.widthOfTextAtSize(headerText, headerSize) + const headerX2 = (width2 - textWidth2) / 2 + const headerY2 = height2 - 72 + page2.drawText(headerText, { x: headerX2, y: headerY2, size: headerSize, font: helv }) + const barY2 = headerY2 - 20 + page2.drawRectangle({ x: 0, y: barY2, width: width2, height: barHeight, color: rgb(0,0,0) }) + + // --- Page 2: SEPA-Lastschriftmandat text and fields --- + // move SEPA section slightly up so title sits closer to the header bar + const sepaYStart = headerY2 - 54 + const sepaLeft = leftX + // increase line gap for SEPA section (~22pt) + const lineGap = 22 + let sy = sepaYStart + const small = 11 + page2.drawText('Erteilung eines SEPA-Lastschriftmandates', { x: sepaLeft, y: sy, size: 12, font: helvBold }) + sy -= lineGap + // draw these two lines as a tight block (no extra gap) + page2.drawText('Harheimer Tischtennis-Club 1954 e.V.', { x: sepaLeft, y: sy, size: small, font: helv }) + sy -= 12 + page2.drawText('Unsere Gläubiger-Identifikationsnummer: DE46ZZZ00000745362', { x: sepaLeft, y: sy, size: small, font: helv }) + // add 0.7cm vertical gap (≈19.8pt) before the authorization paragraph + sy -= 19.8 + // draw the authorization text as wrapped lines within page margins + const wrapMaxWidth = width2 - sepaLeft - 48 + function drawWrapped(text, x, y, size, font) { + const words = text.split(' ') + let line = '' + let curY = y + for (const w of words) { + const test = line ? line + ' ' + w : w + const testWidth = helv.widthOfTextAtSize(test, size) + if (testWidth > wrapMaxWidth) { + page2.drawText(line, { x, y: curY, size, font }) + line = w + curY -= size + 4 + } else { + line = test + } + } + if (line) { + page2.drawText(line, { x, y: curY, size, font }) + curY -= size + 4 + } + return curY + } + sy = drawWrapped('Hiermit ermächtige ich den Harheimer Tischtennis-Club 1954 e.V. die jährlichen Mitgliedsbeiträge von meinem untenstehenden Konto per Lastschrift einzuziehen. Zugleich weise ich mein Kreditinstitut an, die vom Harheimer Tischtennis-Club 1954 e.V. auf mein Konto gezogenen Lastschriften einzulösen.', sepaLeft, sy, small, helv) + sy -= lineGap * 0.2 + // draw mandate reference and validity with a small gap + page2.drawText('Mandatsreferenz: HTC0000 _ _ _', { x: sepaLeft, y: sy, size: small, font: helv }) + // add 0.7cm space after Mandatsreferenz as requested + sy -= 19.8 + page2.drawText('Dieses Mandat gilt für die zugrundeliegende Beitrittserklärung ab sofort.', { x: sepaLeft, y: sy, size: small, font: helv }) + sy -= lineGap * 0.6 + + // Draw labeled lines and create text fields for mandate details + const fieldHeight2 = 14 + const fieldWidth2 = 380 + const labelOffset = 0 + // place SEPA inputs 1cm left (≈28.35pt) from previous and slightly up; adjust to align with labels + const inputX = sepaLeft + 220 - 28.35 + // reduce vertical raise so inputs sit on same baseline as labels (previously too high) + const inputYAdjust = 6 + + // apply labelsShift only to the SEPA form labels/inputs so paragraphs remain unaffected + let syFields = sy + labelsShift + page2.drawText('Mitglied (Vorname und Name)', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv }) + form.createTextField('sepa_mitglied').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv }) + syFields -= lineGap * 1.1 + + page2.drawText('Kontoinhaber (Vorname und Name):', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv }) + form.createTextField('sepa_kontoinhaber').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv }) + syFields -= lineGap * 1.1 + + page2.drawText('Straße und Hausnummer:', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv }) + form.createTextField('sepa_strasse').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv }) + syFields -= lineGap * 1.1 + + page2.drawText('PLZ und Ort:', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv }) + form.createTextField('sepa_plz_ort').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv }) + syFields -= lineGap * 1.1 + + page2.drawText('Kreditinstitut:', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv }) + form.createTextField('sepa_bank').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv }) + syFields -= lineGap * 1.1 + + page2.drawText('IBAN:', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv }) + form.createTextField('sepa_iban').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv }) + syFields -= lineGap * 1.1 + + page2.drawText('BIC:', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv }) + // BIC remains full width as requested + form.createTextField('sepa_bic').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust, width: 220, height: fieldHeight2, font: helv }) + syFields -= lineGap * 1.1 + + // add signature and date lines 2cm below last field + const twoCm = 56.7 + const signY2 = syFields - twoCm + // date text and line + const sigDateText = 'Frankfurt/Main-Harheim, den' + page2.drawText(sigDateText + ' ', { x: sepaLeft, y: signY2, size: 12, font: helv }) + const sigDateStartX = sepaLeft + helv.widthOfTextAtSize(sigDateText + ' ', 12) + const sigDateWidth = 160 + page2.drawRectangle({ x: sigDateStartX, y: signY2 - 2, width: sigDateWidth, height: 1, color: rgb(0,0,0) }) + page2.drawText('Datum', { x: sigDateStartX + sigDateWidth / 2 - helv.widthOfTextAtSize('Datum', 11) / 2, y: signY2 - 18, size: 11, font: helv }) + // create a date form field on page 2 centered under the date line + const sepaDateFieldWidth = 120 + const sepaDateFieldX = sigDateStartX + sigDateWidth / 2 - sepaDateFieldWidth / 2 + // position sepa date field so its bottom edge is on the signature line + // raise SEPA date field by the same amount so its top/bottom alignment matches requested position + form.createTextField('sepa_datum').addToPage(page2, { x: sepaDateFieldX, y: signY2 - 2 - fieldHeight2 + dateRaise, width: sepaDateFieldWidth, height: fieldHeight2, font: helv }) + + // footer on page 2: right-aligned 'Seite 2 von 3' 2cm from bottom + const footerText2 = 'Seite 2 von 3' + const footerSize2 = 10 + const footerWidth2 = helv.widthOfTextAtSize(footerText2, footerSize2) + page2.drawText(footerText2, { x: width2 - footerWidth2 - leftX, y: 56.7, size: footerSize2, font: helv }) + + // signature line + // move signature label/line 2cm (≈56.7pt) further down + const signLineY = signY2 - 36 - 56.7 + const signLineWidth = 300 + page2.drawRectangle({ x: sepaLeft, y: signLineY - 2, width: signLineWidth, height: 1, color: rgb(0,0,0) }) + page2.drawText('Unterschrift des Kontoinhabers', { x: sepaLeft, y: signLineY - 18, size: 11, font: helv }) + // no form field for signature on page 2 (physical signature) + + const pdfBytes = await pdfDoc.save() + fs.writeFileSync('server/templates/mitgliedschaft-fillable.pdf', pdfBytes) + console.log('Wrote server/templates/mitgliedschaft-fillable.pdf') + + // --- Add a third page with same header/bar/footer and title 'Einwilligungserklärung' --- + const page3 = pdfDoc.addPage([595.28, 841.89]) + const { width: width3, height: height3 } = page3.getSize() + const headerX3 = (width3 - textWidth) / 2 + const headerY3 = height3 - 72 + page3.drawText(headerText, { x: headerX3, y: headerY3, size: headerSize, font: helv }) + const barY3 = headerY3 - 20 + page3.drawRectangle({ x: 0, y: barY3, width: width3, height: barHeight, color: rgb(0,0,0) }) + + // title for page 3 + const page3Title = 'Einwilligungserklärung' + const page3TitleSize = 14 + const page3TitleX = leftX + const page3TitleY = headerY3 - 48 + page3.drawText(page3Title, { x: page3TitleX, y: page3TitleY, size: page3TitleSize, font: helvBold }) + + // footer on page 3 + const footerText3 = 'Seite 3 von 3' + const footerSize3 = 10 + const footerWidth3 = helv.widthOfTextAtSize(footerText3, footerSize3) + page3.drawText(footerText3, { x: width3 - footerWidth3 - leftX, y: 56.7, size: footerSize3, font: helv }) +} + +create().catch(e => { + console.error(e) + process.exit(1) +}) diff --git a/scripts/fetch-template.sh b/scripts/fetch-template.sh new file mode 100644 index 0000000..03c66af --- /dev/null +++ b/scripts/fetch-template.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Lädt das offizielle Aufnahmeantrag-PDF in server/templates/ +# Usage: ./scripts/fetch-template.sh [URL] +# Default URL: https://harheimertc.de/Aufnahmeantrag%202025.pdf + +TEMPLATE_URL=${1:-"https://harheimertc.de/Aufnahmeantrag%202025.pdf"} +TEMPLATES_DIR="$(pwd)/server/templates" +mkdir -p "$TEMPLATES_DIR" + +echo "Lade Template von: $TEMPLATE_URL" +curl -fL "$TEMPLATE_URL" -o "$TEMPLATES_DIR/Aufnahmeantrag 2025.pdf" + +echo "Template gespeichert als: $TEMPLATES_DIR/Aufnahmeantrag 2025.pdf" +chmod 644 "$TEMPLATES_DIR/Aufnahmeantrag 2025.pdf" + +echo "Fertig." diff --git a/server/api/membership/applications.get.js b/server/api/membership/applications.get.js new file mode 100644 index 0000000..dae6d8d --- /dev/null +++ b/server/api/membership/applications.get.js @@ -0,0 +1,72 @@ +import fs from 'fs/promises' +import path from 'path' +import { decryptObject } from '../../utils/encryption.js' + +export default defineEventHandler(async (event) => { + try { + const config = useRuntimeConfig() + const encryptionKey = config.encryptionKey + + if (!encryptionKey) { + throw createError({ + statusCode: 500, + statusMessage: 'Verschlüsselungsschlüssel nicht konfiguriert' + }) + } + + const dataDir = path.join(process.cwd(), 'server/data/membership-applications') + + // Prüfen ob Verzeichnis existiert + try { + await fs.access(dataDir) + } catch { + return [] + } + + // Alle Anträge laden + const files = await fs.readdir(dataDir) + const applications = [] + + for (const file of files) { + if (file.endsWith('.json')) { + try { + const filePath = path.join(dataDir, file) + const fileContent = await fs.readFile(filePath, 'utf8') + const applicationData = JSON.parse(fileContent) + + // Verschlüsselte Daten entschlüsseln + const decryptedData = decryptObject(applicationData.encryptedData, encryptionKey) + + applications.push({ + id: applicationData.id, + timestamp: applicationData.timestamp, + status: applicationData.status, + metadata: applicationData.metadata, + // Entschlüsselte persönliche Daten + personalData: { + nachname: decryptedData.nachname, + vorname: decryptedData.vorname, + email: decryptedData.email, + telefon_privat: decryptedData.telefon_privat, + telefon_mobil: decryptedData.telefon_mobil + } + }) + } catch (error) { + console.error(`Fehler beim Laden von ${file}:`, error) + } + } + } + + // Nach Zeitstempel sortieren (neueste zuerst) + applications.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)) + + return applications + + } catch (error) { + console.error('Fehler beim Laden der Mitgliedschaftsanträge:', error) + throw createError({ + statusCode: 500, + statusMessage: 'Fehler beim Laden der Anträge' + }) + } +}) diff --git a/server/api/membership/download/[id].get.js b/server/api/membership/download/[id].get.js new file mode 100644 index 0000000..707ba54 --- /dev/null +++ b/server/api/membership/download/[id].get.js @@ -0,0 +1,100 @@ +import fs from 'fs/promises' +import path from 'path' +import { getUserFromToken } from '../../../utils/auth.js' + +export default defineEventHandler(async (event) => { + try { + // Datei-ID aus der URL extrahieren + const fileId = decodeURIComponent(getRouterParam(event, 'id')) + if (!fileId) { + throw createError({ + statusCode: 400, + statusMessage: 'Datei-ID fehlt' + }) + } + + // Upload-Verzeichnis finden + const uploadDir = path.join(process.cwd(), 'public', 'uploads') + console.log('Upload-Verzeichnis:', uploadDir) + + // Alle Dateien im Upload-Verzeichnis durchsuchen + const files = await fs.readdir(uploadDir) + console.log('Verfügbare Dateien:', files) + console.log('Gesuchte Datei-ID:', fileId) + + const matchingFile = files.find(file => file.includes(fileId)) + console.log('Gefundene Datei:', matchingFile) + + if (!matchingFile) { + throw createError({ + statusCode: 404, + statusMessage: 'Datei nicht gefunden' + }) + } + + // Prüfen ob der Benutzer berechtigt ist, diese Datei herunterzuladen + const token = getCookie(event, 'auth_token') + let isAuthorized = false + + if (token) { + // Authentifizierte Benutzer prüfen + const user = await getUserFromToken(token) + if (user && ['admin', 'vorstand'].includes(user.role)) { + // Admin/Vorstand kann alle Dateien herunterladen + isAuthorized = true + } + } + + // Prüfen ob es sich um eine aktuelle Session handelt (innerhalb der letzten 30 Minuten) + const sessionKey = `download_${fileId}` + const sessionValue = getCookie(event, sessionKey) + + if (sessionValue === 'authorized') { + // Session-basierte Berechtigung für Antragsteller + isAuthorized = true + } + + if (!isAuthorized) { + throw createError({ + statusCode: 403, + statusMessage: 'Keine Berechtigung für diesen Download' + }) + } + + const filePath = path.join(uploadDir, matchingFile) + + // Datei lesen + const fileBuffer = await fs.readFile(filePath) + + // MIME-Type basierend auf Dateiendung bestimmen + const ext = path.extname(matchingFile).toLowerCase() + let mimeType = 'application/octet-stream' + let filename = matchingFile + + if (ext === '.pdf') { + mimeType = 'application/pdf' + } else if (ext === '.txt') { + mimeType = 'text/plain' + filename = matchingFile.replace('.txt', '.pdf') // Für Download als PDF benennen + } + + // Datei als Download senden + setHeader(event, 'Content-Type', mimeType) + setHeader(event, 'Content-Disposition', `attachment; filename="${filename}"`) + setHeader(event, 'Content-Length', fileBuffer.length.toString()) + + return fileBuffer + + } catch (error) { + console.error('Download-Fehler:', error) + + if (error.statusCode) { + throw error + } + + throw createError({ + statusCode: 500, + statusMessage: 'Interner Serverfehler' + }) + } +}) diff --git a/server/api/membership/generate-pdf.post.js b/server/api/membership/generate-pdf.post.js new file mode 100644 index 0000000..212f4dc --- /dev/null +++ b/server/api/membership/generate-pdf.post.js @@ -0,0 +1,708 @@ +import { createRequire } from 'module' +import { exec } from 'child_process' +import { promisify } from 'util' +import fs from 'fs/promises' +import path from 'path' +import { encrypt } from '../../utils/encryption.js' +import { PDFDocument, rgb, StandardFonts } from 'pdf-lib' + +const require = createRequire(import.meta.url) +const execAsync = promisify(exec) + +function generateLaTeXContent(data) { + const heute = new Date().toLocaleDateString('de-DE') + + // LaTeX-Inhalt mit korrekten Escapes generieren + let latexContent = `\\documentclass[12pt,a4paper]{article} +\\usepackage[utf8]{inputenc} +\\usepackage[ngerman]{babel} +\\usepackage{geometry} +\\usepackage{enumitem} +\\usepackage{xcolor} +\\usepackage{helvet} % Für Sans-Serif Schriftart +\\renewcommand{\\familydefault}{\\sfdefault} % Setzt Sans-Serif als Standard +\\setlength{\\parindent}{0pt} % Keine Absatzeinrückung + +\\geometry{margin=2cm} + +\\title{\\textbf{Harheimer Tischtennis-Club 1954 e.V.}\\\\ +\\vspace{0.3cm} +\\Large Beitrittserklärung} +\\date{} + +\\begin{document} +\\maketitle + +\\vspace{0.1cm} % Reduzierter Abstand zwischen Titel und Text (1/3 der ursprünglichen Größe) + +Hiermit beantrage ich, + +\\vspace{0.5cm} + +Name: \\underline{${data.nachname} \\hspace{6cm}} \\\\ +\\vspace{0.3cm} +Vorname: \\underline{${data.vorname} \\hspace{6cm}} \\\\ +\\vspace{0.3cm} +Straße: \\underline{${data.strasse} \\hspace{6cm}} \\\\ +\\vspace{0.3cm} +PLZ/Wohnort: \\underline{${data.plz} ${data.ort} \\hspace{6cm}} \\\\ +\\vspace{0.3cm} +Geburtsdatum: \\underline{${new Date(data.geburtsdatum).toLocaleDateString('de-DE')} \\hspace{6cm}} \\\\ +\\vspace{0.3cm} +Telefon (privat): \\underline{${data.telefon_privat || ''} \\hspace{6cm}} \\\\ +\\vspace{0.3cm} +E-Mail: \\underline{${data.email} \\hspace{6cm}} \\\\ +\\vspace{0.3cm} +Telefon (Mobil): \\underline{${data.telefon_mobil || ''} \\hspace{6cm}} + +\\vspace{0.5cm} + +meine Aufnahme in den Harheimer Tischtennis-Club 1954 e.V. als + +\\vspace{0.3cm} + +\\begin{itemize}[label={}] +\\item[\\framebox(4,4){}] ${data.mitgliedschaftsart === 'aktiv' ? '\\framebox(4,4){X}' : '\\framebox(4,4){}'} aktives Mitglied +\\item[\\framebox(4,4){}] ${data.mitgliedschaftsart === 'passiv' ? '\\framebox(4,4){X}' : '\\framebox(4,4){}'} passives Mitglied +\\end{itemize} + +\\vspace{0.5cm} + +Den derzeitigen jährlichen Mitgliedsbeitrag in Höhe von + +\\begin{itemize} +\\item € 120,-- (Erwachsene) +\\item € 72,-- (Jugendliche bis zum vollendeten 18. Lebensjahr) +\\item € 30,-- (passive Mitglieder) +\\end{itemize} + +bitte ich per Lastschrift jährlich von meinem Konto einzuziehen. +Hierzu erteile ich beigefügtes SEPA-Lastschriftmandat. + +\\vspace{0.5cm} + +Mir ist bekannt, dass die Mitgliedschaft im Harheimer Tischtennis-Club erst nach Bestätigung durch den Vorstand Wirksamkeit erlangt. Die Beitragspflicht beginnt mit dem darauf folgenden Monat. + +Ich erkenne die Vereinssatzung (erhältlich beim Vorstand bzw. auf der Vereinshomepage) an. + +\\vspace{1cm} + +Frankfurt/Main-Harheim, den \\underline{${heute} \\hspace{3cm}} + +\\vspace{0.5cm} + +Unterschrift ${data.isVolljaehrig ? '' : '(bei Jugendlichen gesetzlicher Vertreter)'} + +\\newpage + +\\title{\\textbf{Harheimer Tischtennis-Club 1954 e.V.}\\\\ +\\Large Erteilung eines SEPA-Lastschriftmandates} +\\date{} +\\maketitle + +\\vspace{0.5cm} + +\\textbf{Harheimer Tischtennis-Club 1954 e.V.}\\\\ +Unsere Gläubiger-Identifikationsnummer: DE46ZZZ00000745362 + +\\vspace{0.5cm} + +Hiermit ermächtige ich den Harheimer Tischtennis-Club 1954 e.V. die jährlichen Mitgliedsbeiträge von meinem untenstehenden Konto per Lastschrift einzuziehen. Zugleich weise ich mein Kreditinstitut an, die vom Harheimer Tischtennis-Club 1954 e.V. auf mein Konto gezogenen Lastschriften einzulösen. + +\\vspace{0.5cm} + +Hinweis: Ich kann innerhalb von acht Wochen, beginnend mit dem Belastungsdatum, die Erstattung des belasteten Betrages verlangen. Es gelten dabei die mit meinem Kreditinstitut vereinbarten Bedingungen. + +\\vspace{1cm} + +Kontoinhaber: \\underline{${data.kontoinhaber} \\hspace{6cm}} \\\\ +\\vspace{0.3cm} +IBAN: \\underline{${data.iban} \\hspace{6cm}} \\\\ +\\vspace{0.3cm} +BIC: \\underline{${data.bic || ''} \\hspace{6cm}} \\\\ +\\vspace{0.3cm} +Kreditinstitut: \\underline{${data.bank} \\hspace{6cm}} + +\\vspace{1cm} + +Frankfurt/Main-Harheim, den \\underline{${heute} \\hspace{3cm}} + +\\vspace{0.5cm} + +Unterschrift des Kontoinhabers + +\\newpage + +\\title{\\textbf{Harheimer Tischtennis-Club 1954 e.V.}\\\\ +\\Large Einwilligungserklärung} +\\date{} +\\maketitle + +\\vspace{0.5cm} + +\\textbf{für die Veröffentlichung von Mitgliederdaten im Internet.} + +\\vspace{0.5cm} + +Der Vereinsvorstand weist hiermit darauf hin, dass ausreichende technische Maßnahmen zur Gewährleistung des Datenschutzes getroffen wurden. Dennoch kann bei einer Veröffentlichung von personenbezogenen Mitgliederdaten im Internet ein umfassender Datenschutz nicht garantiert werden. Daher nimmt das Vereinsmitglied die Risiken für eine eventuelle Persönlichkeitsrechtsverletzung zur Kenntnis und ist sich bewusst, dass: + +\\begin{itemize} +\\item die personenbezogenen Daten auch in Staaten abrufbar sind, die keine der Bundesrepublik Deutschland vergleichbaren Datenschutzbestimmungen kennen, +\\item die Vertraulichkeit, die Integrität (Unverletzlichkeit), die Authentizität (Echtheit) und die Verfügbarkeit der personenbezogenen Daten nicht garantiert ist. +\\end{itemize} + +Das Vereinsmitglied trifft die Entscheidung zur Veröffentlichung seiner Daten im Internet freiwillig und kann seine Einwilligung gegenüber dem Vereinsvorstand jederzeit widerrufen. + +\\vspace{0.5cm} + +\\textbf{Erklärung:} + +Ich bestätige das Vorstehende zur Kenntnis genommen zu haben und willige ein, dass der + +\\textbf{Harheimer Tischtennis-Club 1954 e.V.} + +folgende allgemeine Daten zu meiner Person: + +Vorname: \\underline{${data.vorname} \\hspace{4cm}} \\\\ +\\vspace{0.3cm} +Zuname: \\underline{${data.nachname} \\hspace{4cm}} + +Fotografien, sonstige Daten (Mannschaft, Leistungsergebnisse, Turnierteilnahmen, Lizenzen u.ä.) bzw. spezielle Daten von Funktionsträgern: + +Anschrift: \\underline{${data.strasse}, ${data.plz} ${data.ort} \\hspace{4cm}} \\\\ +\\vspace{0.3cm} +Telefonnummer: \\underline{${data.telefon_privat || data.telefon_mobil || ''} \\hspace{4cm}} \\\\ +\\vspace{0.3cm} +E-Mail-Adresse: \\underline{${data.email} \\hspace{4cm}} + +wie angegeben auf der Homepage des Vereins (www.harheimertc.de) veröffentlichen darf. + +\\vspace{1cm} + +Datum: \\underline{${heute} \\hspace{6cm}} + +\\vspace{0.5cm} + +Unterschrift ${data.isVolljaehrig ? '' : '(bei Minderjährigen Unterschrift eines Erziehungsberechtigten)'} + +\\end{document}` + + // Doppelte Backslashes zu einfachen Backslashes konvertieren, aber \\vspace und \\hspace beibehalten + // Erst alle \\vspace und \\hspace temporär ersetzen + let result = latexContent.replace(/\\\\vspace/g, 'TEMP_VSPACE') + result = result.replace(/\\\\hspace/g, 'TEMP_HSPACE') + + // Dann alle anderen doppelten Backslashes ersetzen + result = result.replace(/\\\\/g, '\\') + + // Dann die temporären Platzhalter wieder zurückersetzen + result = result.replace(/TEMP_VSPACE/g, '\\vspace') + result = result.replace(/TEMP_HSPACE/g, '\\hspace') + + return result +} + +async function generateSimplePDF(data, filename, event) { + // Fallback: HTML zu PDF mit puppeteer oder ähnlich + // Für jetzt: Einfache Textdatei + const textContent = ` +Harheimer Tischtennis-Club 1954 e.V. +Beitrittserklärung + +Antragsteller: ${data.vorname} ${data.nachname} +Mitgliedschaftsart: ${data.mitgliedschaftsart} +Volljährig: ${data.isVolljaehrig ? 'Ja' : 'Nein'} + +Das ausgefüllte Formular ist als Anhang verfügbar.` + + const textPath = path.join(process.cwd(), 'public', 'uploads', `${filename}.txt`) + await fs.writeFile(textPath, textContent, 'utf8') + + return `${filename}.txt` +} + +function getDataPath(filename) { + // Immer den absoluten Pfad zum Projekt-Root verwenden + // In der Entwicklung: process.cwd() ist bereits das Projekt-Root + // In der Produktion: process.cwd() ist .output, daher ein Verzeichnis zurück + const isDev = process.env.NODE_ENV === 'development' + const projectRoot = isDev ? process.cwd() : path.resolve(process.cwd(), '..') + return path.join(projectRoot, 'server', 'data', filename) +} + +async function sendMembershipEmail(data, filename, event) { + try { + const configPath = getDataPath('config.json') + const configData = await fs.readFile(configPath, 'utf8') + const config = JSON.parse(configData) + + let recipients = [] + let subject = `Neuer Mitgliedschaftsantrag - ${data.vorname} ${data.nachname}` + + // Sammle alle verfügbaren E-Mail-Adressen + const availableEmails = [] + + // Vorsitzender E-Mail hinzufügen (falls vorhanden) + if (config.vorstand.vorsitzender.email && config.vorstand.vorsitzender.email.trim() !== '') { + availableEmails.push(config.vorstand.vorsitzender.email) + } + + // Schriftführer E-Mail hinzufügen (falls vorhanden) + if (config.vorstand.schriftfuehrer.email && config.vorstand.schriftfuehrer.email.trim() !== '') { + availableEmails.push(config.vorstand.schriftfuehrer.email) + } + + // Fallback: Wenn keine E-Mails verfügbar sind, verwende tsschulz@tsschulz.de + if (availableEmails.length === 0) { + recipients = ['tsschulz@tsschulz.de'] + } else { + recipients = availableEmails + } + + // In nicht-Produktionsumgebung: Alle E-Mails an tsschulz@tsschulz.de + if (process.env.NODE_ENV !== 'production') { + recipients = ['tsschulz@tsschulz.de'] + } + + const message = `Ein neuer Mitgliedschaftsantrag wurde eingereicht. + +Antragsteller: ${data.vorname} ${data.nachname} +Mitgliedschaftsart: ${data.mitgliedschaftsart} +Volljährig: ${data.isVolljaehrig ? 'Ja' : 'Nein'} + +Das ausgefüllte Formular ist als Anhang verfügbar.` + + // E-Mail-Versand implementieren (hier würde normalerweise nodemailer verwendet) + console.log('E-Mail würde gesendet werden an:', recipients) + console.log('Betreff:', subject) + console.log('Nachricht:', message) + + return { success: true, recipients, subject, message } + } catch (error) { + console.error('Fehler beim Senden der E-Mail:', error) + return { success: false, error: error.message } + } +} + +export default defineEventHandler(async (event) => { + try { + const body = await readBody(event) + + // Validierung der Eingabedaten + const requiredFields = ['vorname', 'nachname', 'strasse', 'plz', 'ort', 'geburtsdatum', 'email', 'mitgliedschaftsart', 'kontoinhaber', 'iban'] + for (const field of requiredFields) { + if (!body[field]) { + throw createError({ + statusCode: 400, + statusMessage: `Feld '${field}' ist erforderlich` + }) + } + } + + // Volljährigkeit prüfen + const birthDate = new Date(body.geburtsdatum) + const today = new Date() + const age = today.getFullYear() - birthDate.getFullYear() + const monthDiff = today.getMonth() - birthDate.getMonth() + const isVolljaehrig = age > 18 || (age === 18 && monthDiff >= 0) + + const data = { + ...body, + isVolljaehrig + } + + // Eindeutige Datei-ID generieren + const timestamp = Date.now() + const filename = `beitrittserklärung_${timestamp}` + + // Temp-Verzeichnis erstellen + const tempDir = path.join(process.cwd(), '.output', 'temp', 'latex') + await fs.mkdir(tempDir, { recursive: true }) + + try { + // PDF-Template-Funktion aktiv: versuche Original-PDF-Template herunterzuladen und zu befüllen + // Versuch: Original-PDF-Template herunterladen und AcroForm-Felder befüllen + async function fillPdfTemplate(data) { + // Priorität: neues lokales Fillable-Template in server/templates, sonst ursprüngliches Template + const fillablePath = path.join(process.cwd(), 'server', 'templates', 'mitgliedschaft-fillable.pdf') + const localPath = (await fs.stat(fillablePath).then(() => fillablePath).catch(() => null)) || path.join(process.cwd(), 'server', 'templates', 'Aufnahmeantrag 2025.pdf') + let arrayBuffer + try { + const localExists = await fs.stat(localPath).then(() => true).catch(() => false) + if (localExists) { + const buf = await fs.readFile(localPath) + arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) + } else { + const TEMPLATE_URL = process.env.MEMBERSHIP_TEMPLATE_URL || 'https://harheimertc.de/Aufnahmeantrag%202025.pdf' + const res = await fetch(TEMPLATE_URL) + if (!res.ok) throw new Error(`Template konnte nicht geladen werden: ${res.status}`) + arrayBuffer = await res.arrayBuffer() + } + } catch (e) { + throw new Error('Template-Laden fehlgeschlagen: ' + e.message) + } + + const pdfDoc = await PDFDocument.load(arrayBuffer) + let form + try { + form = pdfDoc.getForm() + } catch (e) { + form = null + } + + if (!form || form.getFields().length === 0) { + // Keine Formularfelder vorhanden -> positional filler verwenden + // Koordinaten (in PDF-Punkten) müssen ggf. feinjustiert werden. + const pages = pdfDoc.getPages() + const firstPage = pages[0] + const { width, height } = firstPage.getSize() + + // Schätzwerte: (x, y) in Punkten von linker unteren Ecke + // Diese Werte müssen nach Sichtprüfung justiert werden. + // Angepasste Koordinaten — Text beginnt links an der Linie und sitzt oberhalb der Unterstreichung + // Linke Spalte (nachname, vorname, etc.) etwas weiter links setzen + const leftX = 156 + // Rechte Spalte (PLZ/Ort, Telefon, E-Mail) weiter nach rechts für klare Trennung + const rightX = 470 + // Baseline für erstes Feld (von oben aus gerechnet) + const baseY = height - 160 + const gap = 28 + // Kleine vertikale Verschiebung, damit Texte oberhalb der Unterstreichung beginnen + const yOffset = 9 + + const coords = { + // Row: Name (left) / Vorname (right) + nachname: { x: leftX, y: baseY + yOffset }, + vorname: { x: rightX, y: baseY + yOffset }, + // Row: Straße (left) / PLZ/Ort (right) + strasse: { x: leftX, y: baseY - gap + yOffset }, + plz_ort: { x: rightX, y: baseY - gap + yOffset }, + // Row: Geburtsdatum (left) / Telefon(privat) (right) + geburtsdatum: { x: leftX, y: baseY - gap * 2 + yOffset }, + telefon: { x: rightX, y: baseY - gap * 2 + yOffset }, + // Row: E-Mail (left) / Telefon(Mobil) (right) + email: { x: leftX, y: baseY - gap * 3 + yOffset }, + telefon_mobil: { x: rightX, y: baseY - gap * 3 + yOffset }, + // Membership checkbox positions (approx.) + mitglied_checkbox_aktiv: { x: leftX - 40, y: baseY - gap * 6 + yOffset }, + mitglied_checkbox_passiv: { x: leftX - 40, y: baseY - gap * 7 + yOffset }, + // Account details on subsequent page(s) + kontoinhaber: { x: leftX, y: baseY - gap * 12 + yOffset }, + iban: { x: leftX, y: baseY - gap * 13 + yOffset }, + bic: { x: leftX, y: baseY - gap * 14 + yOffset }, + bank: { x: leftX, y: baseY - gap * 15 + yOffset } + } + + const drawText = (page, text, x, y, size = 11) => { + page.drawText(text || '', { + x, + y, + size, + font: pdfDoc.embedStandardFont ? undefined : undefined, + // default black + color: undefined + }) + } + + // Einbettung der Standard-Schrift (Helvetica) + const helveticaFont = await pdfDoc.embedFont(PDFDocument.PDFName ? 'Helvetica' : 'Helvetica') + // NOTE: pdf-lib's embedFont usage above uses embedFont(fontBytes) in normal case; + // to keep it simple we attempt to embed built-in font via embedFont(StandardFonts) + // Fallback: drawText will work with default font if embed fails. + + // Zeichne die Felder + try { + // Zeichne Name / Vorname / Adresse / Kontakt (einmal) + firstPage.drawText(data.nachname || '', { x: coords.nachname.x, y: coords.nachname.y, size: 11, font: helveticaFont }) + firstPage.drawText(data.vorname || '', { x: coords.vorname.x, y: coords.vorname.y, size: 11, font: helveticaFont }) + firstPage.drawText(data.strasse || '', { x: coords.strasse.x, y: coords.strasse.y, size: 11, font: helveticaFont }) + firstPage.drawText(`${data.plz || ''} ${data.ort || ''}`.trim(), { x: coords.plz_ort.x, y: coords.plz_ort.y, size: 11, font: helveticaFont }) + firstPage.drawText(new Date(data.geburtsdatum).toLocaleDateString('de-DE') || '', { x: coords.geburtsdatum.x, y: coords.geburtsdatum.y, size: 11, font: helveticaFont }) + firstPage.drawText(data.telefon_privat || data.telefon_mobil || '', { x: coords.telefon.x, y: coords.telefon.y, size: 11, font: helveticaFont }) + firstPage.drawText(data.email || '', { x: coords.email.x, y: coords.email.y, size: 11, font: helveticaFont }) + firstPage.drawText(data.telefon_mobil || '', { x: coords.telefon_mobil.x, y: coords.telefon_mobil.y, size: 11, font: helveticaFont }) + // Kontodaten evtl. auf andere Seite: falls mehrere Seiten vorhanden, nutze last page + const lastPage = pages[pages.length - 1] + lastPage.drawText(data.kontoinhaber || '', { x: coords.kontoinhaber.x, y: coords.kontoinhaber.y, size: 11, font: helveticaFont }) + lastPage.drawText(data.iban || '', { x: coords.iban.x, y: coords.iban.y, size: 11, font: helveticaFont }) + lastPage.drawText(data.bic || '', { x: coords.bic.x, y: coords.bic.y, size: 11, font: helveticaFont }) + lastPage.drawText(data.bank || '', { x: coords.bank.x, y: coords.bank.y, size: 11, font: helveticaFont }) + // Zeichne X in die passende Mitgliedschafts-Checkbox + try { + if (data.mitgliedschaftsart === 'aktiv') { + firstPage.drawText('X', { x: coords.mitglied_checkbox_aktiv.x, y: coords.mitglied_checkbox_aktiv.y, size: 12, font: helveticaFont }) + } else if (data.mitgliedschaftsart === 'passiv') { + firstPage.drawText('X', { x: coords.mitglied_checkbox_passiv.x, y: coords.mitglied_checkbox_passiv.y, size: 12, font: helveticaFont }) + } + } catch (e) { + console.warn('Fehler beim Zeichnen der Checkbox:', e.message) + } + // Debug overlay: zeichne Marker an allen Koordinaten, wenn data.debug === true + if (data && data.debug) { + try { + // Auffälliges Debug-Tag oben links + const tagW = 100 + const tagH = 22 + firstPage.drawRectangle({ x: 40, y: height - 60, width: tagW, height: tagH, color: rgb(1, 0, 0), opacity: 0.25 }) + firstPage.drawText('DEBUG', { x: 48, y: height - 56, size: 12, color: rgb(1, 0, 0), font: helveticaFont }) + + // Markiere alle Koordinaten mit einem gefüllten roten Quadrat und Label + const allCoords = Object.entries(coords) + for (const [key, c] of allCoords) { + // small filled square + firstPage.drawRectangle({ x: c.x - 3, y: c.y - 3, width: 8, height: 8, color: rgb(1, 0, 0), opacity: 1 }) + // small label a bit to the right + firstPage.drawText(key, { x: c.x + 8, y: c.y - 1, size: 7, color: rgb(0.6, 0, 0), font: helveticaFont }) + } + } catch (e) { + console.warn('Debug overlay fehlgeschlagen:', e.message) + } + } + } catch (e) { + console.warn('Fehler beim positional drawing:', e.message) + } + + const pdfBytes = await pdfDoc.save() + return Buffer.from(pdfBytes) + } + + // Wenn Formularfelder existieren: befülle sie per AcroForm + const fields = form.getFields() + if (fields && fields.length > 0) { + try { + const byName = {} + for (const f of fields) byName[f.getName().toLowerCase()] = f + const setIf = (name, value) => { + const f = byName[name] + if (!f) return + try { + if (typeof f.setText === 'function') f.setText(String(value || '')) + else if (typeof f.check === 'function' && (value === true || String(value).toLowerCase() === 'true')) f.check() + } catch (e) { + console.warn('Fehler beim Setzen Feld', name, e.message) + } + } + setIf('nachname', data.nachname) + setIf('vorname', data.vorname) + setIf('strasse', data.strasse) + setIf('plz_ort', `${data.plz || ''} ${data.ort || ''}`.trim()) + setIf('geburtsdatum', new Date(data.geburtsdatum).toLocaleDateString('de-DE')) + setIf('telefon', data.telefon_privat || data.telefon_mobil) + setIf('email', data.email) + setIf('telefon_mobil', data.telefon_mobil) + // Checkboxes + if (byName['mitglied_aktiv'] && data.mitgliedschaftsart === 'aktiv') byName['mitglied_aktiv'].check && byName['mitglied_aktiv'].check() + if (byName['mitglied_passiv'] && data.mitgliedschaftsart === 'passiv') byName['mitglied_passiv'].check && byName['mitglied_passiv'].check() + const pdfBytes = await pdfDoc.save() + return Buffer.from(pdfBytes) + } catch (e) { + console.warn('AcroForm-Füllung fehlgeschlagen, fallback auf positional:', e.message) + } + } + const mapValue = (name) => { + // einfache Heuristiken für Feldnamen + name = name.toLowerCase() + if (name.includes('nachname') || name.includes('zuname') || name.includes('name')) return data.nachname || '' + if (name.includes('vorname') || name.includes('given')) return data.vorname || '' + if (name.includes('str') || name.includes('straße') || name.includes('street')) return data.strasse || '' + if (name.includes('plz')) return data.plz || '' + if (name.includes('ort') || name.includes('stadt')) return data.ort || '' + if (name.includes('geb') || name.includes('geburts')) return new Date(data.geburtsdatum).toLocaleDateString('de-DE') + if (name.includes('telefon') || name.includes('tel')) return data.telefon_privat || data.telefon_mobil || '' + if (name.includes('email')) return data.email || '' + if (name.includes('kontoinhaber') || name.includes('kontoinh')) return data.kontoinhaber || '' + if (name.includes('iban')) return data.iban || '' + if (name.includes('bic')) return data.bic || '' + if (name.includes('bank') || name.includes('kreditinstitut')) return data.bank || '' + if (name.includes('mitglied') || name.includes('mitgliedschaft') || name.includes('art')) return data.mitgliedschaftsart || '' + return '' + } + + for (const field of fields) { + const fname = field.getName() + const lower = fname.toLowerCase() + try { + // Textfelder + if (typeof field.setText === 'function') { + const val = mapValue(lower) + field.setText(val) + continue + } + + // Checkbox / Radio + if (typeof field.check === 'function' || typeof field.isChecked === 'function') { + // einfache Heuristik: bei Mitgliedschaftsart + if (lower.includes('aktiv') || lower.includes('passiv') || lower.includes('mitglied')) { + if (data.mitgliedschaftsart && lower.includes(data.mitgliedschaftsart)) { + field.check && field.check() + } else { + if (lower.includes('aktiv') && data.mitgliedschaftsart === 'aktiv') field.check && field.check() + if (lower.includes('passiv') && data.mitgliedschaftsart === 'passiv') field.check && field.check() + } + continue + } + const mapped = mapValue(lower) + if (mapped === 'true' || mapped === 'ja' || mapped === 'checked') { + field.check && field.check() + } + } + } catch (e) { + console.warn('Fehler beim Befüllen Feld', fname, e.message) + } + } + + const pdfBytes = await pdfDoc.save() + return Buffer.from(pdfBytes) + } + + let usedTemplate = false + const uploadsDir = path.join(process.cwd(), 'public', 'uploads') + await fs.mkdir(uploadsDir, { recursive: true }) + try { + const filled = await fillPdfTemplate(data) + const finalPdfPath = path.join(uploadsDir, `${filename}.pdf`) + await fs.writeFile(finalPdfPath, filled) + // Zusätzlich: Kopie ins repo-root public/uploads legen, falls Nitro cwd anders ist + try { + const repoRoot = path.resolve(process.cwd(), '..') + const repoUploads = path.join(repoRoot, 'public', 'uploads') + await fs.mkdir(repoUploads, { recursive: true }) + await fs.copyFile(finalPdfPath, path.join(repoUploads, `${filename}.pdf`)) + } catch (e) { + console.warn('Kopie in repo public/uploads fehlgeschlagen:', e.message) + } + usedTemplate = true + } catch (templateError) { + // Template konnte nicht verwendet werden -> weiter zum LaTeX-Fallback + console.warn('Template-Füllung fehlgeschlagen, fahre mit LaTeX fort:', templateError.message) + } + + let emailResult + if (usedTemplate) { + // E-Mail senden + emailResult = await sendMembershipEmail(data, filename, event) + // Antragsdaten verschlüsselt speichern + const encryptionKey = process.env.ENCRYPTION_KEY || 'default-key-change-in-production' + const encryptedData = encrypt(JSON.stringify(data), encryptionKey) + const dataPath = path.join(uploadsDir, `${filename}.data`) + await fs.writeFile(dataPath, encryptedData, 'utf8') + + // Download-Token setzen + const downloadToken = Buffer.from(`${filename}:${Date.now()}`).toString('base64') + setCookie(event, 'download_token', downloadToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 60 * 60 * 24 // 24 Stunden + }) + + return { + success: true, + message: 'Beitrittsformular erfolgreich aus Template erzeugt und E-Mail gesendet.', + downloadUrl: `/api/membership/download/${filename}.pdf`, + emailSuccess: emailResult.success, + emailMessage: emailResult.message, + usedTemplate: true + } + } + + // Falls Template nicht verwendet: weiter mit LaTeX-Fallback + // LaTeX-Inhalt generieren + const latexContent = generateLaTeXContent(data) + + // LaTeX-Datei schreiben + const texPath = path.join(tempDir, `${filename}.tex`) + await fs.writeFile(texPath, latexContent, 'utf8') + + // PDF mit pdflatex generieren + const command = `cd "${tempDir}" && pdflatex -interaction=nonstopmode "${filename}.tex"` + await execAsync(command) + + // PDF-Datei in Uploads-Verzeichnis kopieren + const pdfPath = path.join(tempDir, `${filename}.pdf`) + await fs.mkdir(uploadsDir, { recursive: true }) + + const finalPdfPath = path.join(uploadsDir, `${filename}.pdf`) + await fs.copyFile(pdfPath, finalPdfPath) + // Kopie ins repo-root public/uploads für bessere Auffindbarkeit + try { + const repoRoot = path.resolve(process.cwd(), '..') + const repoUploads = path.join(repoRoot, 'public', 'uploads') + await fs.mkdir(repoUploads, { recursive: true }) + await fs.copyFile(finalPdfPath, path.join(repoUploads, `${filename}.pdf`)) + } catch (e) { + console.warn('Kopie in repo public/uploads fehlgeschlagen:', e.message) + } + + // E-Mail senden + emailResult = await sendMembershipEmail(data, filename, event) + + // Antragsdaten verschlüsselt speichern + const encryptionKey = process.env.ENCRYPTION_KEY || 'default-key-change-in-production' + const encryptedData = encrypt(JSON.stringify(data), encryptionKey) + const dataPath = path.join(uploadsDir, `${filename}.data`) + await fs.writeFile(dataPath, encryptedData, 'utf8') + + // Download-Berechtigung für den Antragsteller setzen + const downloadToken = Buffer.from(`${filename}:${Date.now()}`).toString('base64') + setCookie(event, 'download_token', downloadToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 60 * 60 * 24 // 24 Stunden + }) + + return { + success: true, + message: 'Beitrittsformular erfolgreich erstellt und E-Mail gesendet.', + downloadUrl: `/api/membership/download/${filename}.pdf`, + emailSuccess: emailResult.success, + emailMessage: emailResult.message, + } + + } catch (latexError) { + console.error('LaTeX-Fehler:', latexError) + + // Fallback: Einfache Textdatei generieren + const fallbackFilename = await generateSimplePDF(data, filename, event) + + // E-Mail senden (Fallback) + const emailResult = await sendMembershipEmail(data, filename, event) + + console.log('LaTeX nicht verfügbar, verwende Fallback-Lösung') + console.log('E-Mail würde gesendet werden an:', emailResult.recipients || []) + console.log('Betreff:', emailResult.subject || '') + console.log('Nachricht:', emailResult.message || '') + console.log('Upload-Verzeichnis:', path.join(process.cwd(), 'public', 'uploads')) + + // Verfügbare Dateien auflisten + const uploadsDir = path.join(process.cwd(), 'public', 'uploads') + try { + const files = await fs.readdir(uploadsDir) + console.log('Verfügbare Dateien:', files) + + // Gesuchte Datei finden + const targetFile = files.find(file => file.startsWith(filename)) + console.log('Gesuchte Datei-ID:', filename) + console.log('Gefundene Datei:', targetFile || 'Nicht gefunden') + } catch (dirError) { + console.error('Fehler beim Lesen des Upload-Verzeichnisses:', dirError) + } + + return { + success: true, + message: 'Beitrittsformular erfolgreich erstellt und E-Mail gesendet (Fallback-Lösung).', + downloadUrl: `/api/membership/download/${fallbackFilename}`, + emailSuccess: emailResult.success, + emailMessage: emailResult.message, + } + } + + } catch (error) { + console.error('Fehler beim Generieren des PDFs:', error) + throw createError({ + statusCode: 500, + statusMessage: 'Fehler beim Generieren des PDFs' + }) + } +}) \ No newline at end of file diff --git a/server/api/membership/update-status.put.js b/server/api/membership/update-status.put.js new file mode 100644 index 0000000..976d71d --- /dev/null +++ b/server/api/membership/update-status.put.js @@ -0,0 +1,90 @@ +import fs from 'fs/promises' +import path from 'path' +import { decryptObject, encryptObject } from '../../utils/encryption.js' +import { saveMember } from '../../utils/members.js' + +export default defineEventHandler(async (event) => { + try { + const { id, status, notes } = await readBody(event) + + if (!id || !status) { + throw createError({ + statusCode: 400, + statusMessage: 'ID und Status sind erforderlich' + }) + } + + if (!['pending', 'approved', 'rejected'].includes(status)) { + throw createError({ + statusCode: 400, + statusMessage: 'Ungültiger Status' + }) + } + + const config = useRuntimeConfig() + const encryptionKey = config.encryptionKey + + if (!encryptionKey) { + throw createError({ + statusCode: 500, + statusMessage: 'Verschlüsselungsschlüssel nicht konfiguriert' + }) + } + + const dataDir = path.join(process.cwd(), 'server/data/membership-applications') + const filePath = path.join(dataDir, `${id}.json`) + + // Antrag laden + const fileContent = await fs.readFile(filePath, 'utf8') + const applicationData = JSON.parse(fileContent) + + // Status aktualisieren + applicationData.status = status + applicationData.updatedAt = new Date().toISOString() + + if (notes) { + applicationData.notes = notes + } + + // Wenn genehmigt: In Mitgliederliste einfügen + if (status === 'approved') { + try { + const decryptedData = decryptObject(applicationData.encryptedData, encryptionKey) + + const newMember = { + firstName: decryptedData.vorname, + lastName: decryptedData.nachname, + email: decryptedData.email, + phone: decryptedData.telefon_privat || decryptedData.telefon_mobil || '', + address: `${decryptedData.strasse}, ${decryptedData.plz} ${decryptedData.ort}`, + notes: `Mitgliedschaftsart: ${applicationData.metadata.mitgliedschaftsart} | Genehmigt: ${new Date().toLocaleDateString('de-DE')}`, + source: 'membership_application', + applicationId: id + } + + await saveMember(newMember) + applicationData.memberId = newMember.id + + console.log(`Mitgliedschaftsantrag ${id} wurde genehmigt und in Mitgliederliste eingefügt`) + } catch (error) { + console.error('Fehler beim Einfügen in Mitgliederliste:', error) + throw error + } + } + + // Speichern + await fs.writeFile(filePath, JSON.stringify(applicationData, null, 2), 'utf8') + + return { + success: true, + message: status === 'approved' ? 'Antrag genehmigt und in Mitgliederliste eingefügt' : 'Status erfolgreich aktualisiert' + } + + } catch (error) { + console.error('Fehler beim Aktualisieren des Status:', error) + throw createError({ + statusCode: 500, + statusMessage: 'Fehler beim Aktualisieren des Status' + }) + } +}) diff --git a/server/data/sessions.json b/server/data/sessions.json index b4eae7f..303df22 100644 --- a/server/data/sessions.json +++ b/server/data/sessions.json @@ -103,5 +103,12 @@ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJlbWFpbCI6ImFkbWluQGhhcmhlaW1lcnRjLmRlIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzYxMTM2NzMzLCJleHAiOjE3NjE3NDE1MzN9.rzxassQ4Uj-nXfL2y1sygzshW0YovdYR2GUjosXoPF8", "createdAt": "2025-10-22T12:38:53.918Z", "expiresAt": "2025-10-29T12:38:53.918Z" + }, + { + "id": "1761144008551", + "userId": "1", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJlbWFpbCI6ImFkbWluQGhhcmhlaW1lcnRjLmRlIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzYxMTQ0MDA4LCJleHAiOjE3NjE3NDg4MDh9.G5OtippK77mTioWV8Js-OpoJmaEPLA38VRjwRRj93oA", + "createdAt": "2025-10-22T14:40:08.551Z", + "expiresAt": "2025-10-29T14:40:08.551Z" } ] \ No newline at end of file diff --git a/server/data/users.json b/server/data/users.json index 33ae7dc..ba9c046 100644 --- a/server/data/users.json +++ b/server/data/users.json @@ -8,6 +8,6 @@ "phone": "", "active": true, "created": "2025-10-21T00:00:00.000Z", - "lastLogin": "2025-10-22T12:38:53.919Z" + "lastLogin": "2025-10-22T14:40:08.552Z" } ] \ No newline at end of file diff --git a/server/templates/Aufnahmeantrag 2025.pdf b/server/templates/Aufnahmeantrag 2025.pdf new file mode 100644 index 0000000..832b137 Binary files /dev/null and b/server/templates/Aufnahmeantrag 2025.pdf differ diff --git a/server/templates/mitgliedschaft-fillable.pdf b/server/templates/mitgliedschaft-fillable.pdf new file mode 100644 index 0000000..02a91b6 Binary files /dev/null and b/server/templates/mitgliedschaft-fillable.pdf differ diff --git a/server/utils/encryption.js b/server/utils/encryption.js new file mode 100644 index 0000000..dd0ef3d --- /dev/null +++ b/server/utils/encryption.js @@ -0,0 +1,102 @@ +import crypto from 'crypto' + +// Verschlüsselungskonfiguration +const ALGORITHM = 'aes-256-cbc' +const IV_LENGTH = 16 +const SALT_LENGTH = 32 + +/** + * Generiert einen Schlüssel aus einem Passwort und Salt + */ +function deriveKey(password, salt) { + return crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha512') +} + +/** + * Verschlüsselt einen Text + */ +export function encrypt(text, password) { + try { + // Salt generieren + const salt = crypto.randomBytes(SALT_LENGTH) + + // Schlüssel ableiten + const key = deriveKey(password, salt) + + // IV generieren + const iv = crypto.randomBytes(IV_LENGTH) + + // Cipher erstellen + const cipher = crypto.createCipheriv(ALGORITHM, key, iv) + + // Verschlüsseln + let encrypted = cipher.update(text, 'utf8', 'hex') + encrypted += cipher.final('hex') + + // Salt + IV + Verschlüsselter Text kombinieren + const combined = Buffer.concat([ + salt, + iv, + Buffer.from(encrypted, 'hex') + ]) + + return combined.toString('base64') + } catch (error) { + console.error('Verschlüsselungsfehler:', error) + throw new Error('Fehler beim Verschlüsseln der Daten') + } +} + +/** + * Entschlüsselt einen Text + */ +export function decrypt(encryptedData, password) { + try { + // Base64 dekodieren + const combined = Buffer.from(encryptedData, 'base64') + + // Komponenten extrahieren + const salt = combined.subarray(0, SALT_LENGTH) + const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH) + const encrypted = combined.subarray(SALT_LENGTH + IV_LENGTH) + + // Schlüssel ableiten + const key = deriveKey(password, salt) + + // Decipher erstellen + const decipher = crypto.createDecipheriv(ALGORITHM, key, iv) + + // Entschlüsseln + let decrypted = decipher.update(encrypted, null, 'utf8') + decrypted += decipher.final('utf8') + + return decrypted + } catch (error) { + console.error('Entschlüsselungsfehler:', error) + throw new Error('Fehler beim Entschlüsseln der Daten') + } +} + +/** + * Verschlüsselt ein Objekt (konvertiert zu JSON) + */ +export function encryptObject(obj, password) { + const jsonString = JSON.stringify(obj) + return encrypt(jsonString, password) +} + +/** + * Entschlüsselt ein Objekt (konvertiert von JSON) + */ +export function decryptObject(encryptedData, password) { + const jsonString = decrypt(encryptedData, password) + return JSON.parse(jsonString) +} + +/** + * Generiert einen sicheren Schlüssel für die Datenverschlüsselung + */ +export function generateEncryptionKey() { + return crypto.randomBytes(32).toString('hex') +} + diff --git a/temp/Beitrittserklärung_Mustermann_Max-3.pdf b/temp/Beitrittserklärung_Mustermann_Max-3.pdf new file mode 100644 index 0000000..8695781 Binary files /dev/null and b/temp/Beitrittserklärung_Mustermann_Max-3.pdf differ diff --git a/temp/latex/test.tex b/temp/latex/test.tex new file mode 100644 index 0000000..842a7e0 --- /dev/null +++ b/temp/latex/test.tex @@ -0,0 +1,26 @@ +\documentclass[12pt,a4paper]{article} +\usepackage[utf8]{inputenc} +\usepackage[ngerman]{babel} +\usepackage{geometry} +\usepackage{enumitem} +\usepackage{xcolor} +\usepackage{helvet} % Für Sans-Serif Schriftart +\renewcommand{\familydefault}{\sfdefault} % Setzt Sans-Serif als Standard +\setlength{\parindent}{0pt} % Keine Absatzeinrückung + +\geometry{margin=2cm} + +\title{\textbf{Harheimer Tischtennis-Club 1954 e.V.}\\ +\vspace{1cm} +\Large Erteilung eines SEPA-Lastschriftmandates} +\date{} + +\begin{document} +\maketitle + +\vspace{1cm} + +\textbf{Harheimer Tischtennis-Club 1954 e.V.}\\ +Unsere Gläubiger-Identifikationsnummer: DE46ZZZ00000745362 + +\end{document} \ No newline at end of file diff --git a/test-vspace.tex b/test-vspace.tex new file mode 100644 index 0000000..70de35e --- /dev/null +++ b/test-vspace.tex @@ -0,0 +1,22 @@ +\documentclass[12pt,a4paper]{article} +\usepackage[utf8]{inputenc} +\usepackage[ngerman]{babel} +\usepackage{geometry} +\geometry{margin=2cm} + +\title{\textbf{Test}\\ +\vspace{1cm} +\Large Test-Titel} +\date{} + +\begin{document} +\maketitle + +\vspace{1cm} + +Dies ist der Inhalt nach dem Header. + +\end{document} + + + diff --git a/test.aux b/test.aux new file mode 100644 index 0000000..b640121 --- /dev/null +++ b/test.aux @@ -0,0 +1,2 @@ +\relax +\gdef \@abspage@last{1} diff --git a/test.pdf b/test.pdf new file mode 100644 index 0000000..8fea39f Binary files /dev/null and b/test.pdf differ diff --git a/test.tex b/test.tex new file mode 100644 index 0000000..d6f7e9f --- /dev/null +++ b/test.tex @@ -0,0 +1 @@ +\documentclass{article}\begin{document}Test\end{document}