Verbessere Codequalität und Sicherheit durch Refactoring, Hinzufügen von ESLint-Regeln und Aktualisierung der OSV-Scanner-Konfiguration
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 6m45s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m57s

This commit is contained in:
Torsten Schulz (local)
2026-07-17 09:08:42 +02:00
parent d260f00756
commit 30465c7833
25 changed files with 40 additions and 53 deletions

View File

@@ -116,7 +116,7 @@ jobs:
chmod +x osv-scanner chmod +x osv-scanner
./osv-scanner --version ./osv-scanner --version
test -f ./package-lock.json test -f ./package-lock.json
./osv-scanner --lockfile ./package-lock.json ./osv-scanner scan -L ./package-lock.json --config ./.osv-scanner.toml
deploy-production: deploy-production:
runs-on: ubuntu-latest runs-on: ubuntu-latest

4
.osv-scanner.toml Normal file
View File

@@ -0,0 +1,4 @@
[[IgnoredVulns]]
id = "GHSA-v3m3-f69x-jf25"
ignoreUntil = 2026-12-31
reason = "Temporary exception: Quill 2.0.3 is required by the current RichTextEditor implementation, and OSV currently reports no fixed version. Track upstream fix and remove this ignore once a patched release is available."

View File

@@ -151,11 +151,6 @@ const showConfirmModal = (title, message, action) => {
showConfirm.value = true showConfirm.value = true
} }
const closeSuccess = () => {
showSuccessToast.value = false
if (toastTimeout) { clearTimeout(toastTimeout); toastTimeout = null }
}
const closeError = () => { const closeError = () => {
showError.value = false showError.value = false
} }

View File

@@ -25,14 +25,19 @@ export default [
'useHead': 'readonly', 'useHead': 'readonly',
'useFetch': 'readonly', 'useFetch': 'readonly',
'definePageMeta': 'readonly', 'definePageMeta': 'readonly',
'defineNuxtPlugin': 'readonly',
'defineNitroPlugin': 'readonly',
'defineNuxtRouteMiddleware': 'readonly', 'defineNuxtRouteMiddleware': 'readonly',
'defineEventHandler': 'readonly', 'defineEventHandler': 'readonly',
'readBody': 'readonly', 'readBody': 'readonly',
'getMethod': 'readonly',
'getCookie': 'readonly', 'getCookie': 'readonly',
'setCookie': 'readonly', 'setCookie': 'readonly',
'deleteCookie': 'readonly', 'deleteCookie': 'readonly',
'getHeader': 'readonly', 'getHeader': 'readonly',
'getRequestURL': 'readonly',
'setHeader': 'readonly', 'setHeader': 'readonly',
'setResponseStatus': 'readonly',
'getRouterParam': 'readonly', 'getRouterParam': 'readonly',
'getQuery': 'readonly', 'getQuery': 'readonly',
'sendStream': 'readonly', 'sendStream': 'readonly',
@@ -66,8 +71,9 @@ export default [
'vue/multi-word-component-names': 'off', 'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'warn', 'vue/no-v-html': 'warn',
'no-unused-vars': ['warn', { 'no-unused-vars': ['warn', {
argsIgnorePattern: '^_', argsIgnorePattern: '^_|^event$',
varsIgnorePattern: '^_' varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_|^e$|^err$|^error$'
}], }],
'vue/no-unused-vars': ['warn', { 'vue/no-unused-vars': ['warn', {
ignorePattern: '^_' ignorePattern: '^_'
@@ -97,6 +103,18 @@ export default [
'tests/**', 'tests/**',
'scripts/**' 'scripts/**'
] ]
},
{
files: [
'pages/cms/newsletter.vue',
'pages/verein/geschichte.vue',
'pages/verein/satzung.vue',
'pages/verein/tt-regeln.vue',
'pages/verein/ueber-uns.vue'
],
rules: {
'vue/no-v-html': 'off'
}
} }
] ]

View File

@@ -167,6 +167,7 @@
</span> </span>
</div> </div>
<!-- nosemgrep: javascript.vue.security.audit.xss.templates.avoid-v-html --> <!-- nosemgrep: javascript.vue.security.audit.xss.templates.avoid-v-html -->
<!-- eslint-disable-next-line vue/no-v-html -->
<div <div
class="text-sm text-gray-600 prose prose-sm max-w-none mb-3" class="text-sm text-gray-600 prose prose-sm max-w-none mb-3"
v-html="useSanitizeHtml(post.content.substring(0, 200) + (post.content.length > 200 ? '...' : ''))" v-html="useSanitizeHtml(post.content.substring(0, 200) + (post.content.length > 200 ? '...' : ''))"

View File

@@ -362,7 +362,6 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { Users } from 'lucide-vue-next'
const route = useRoute() const route = useRoute()

View File

@@ -124,11 +124,9 @@
</template> </template>
<script setup> <script setup>
import { User, Users, Newspaper, Check, Calendar } from 'lucide-vue-next' import { User, Users, Newspaper, Calendar } from 'lucide-vue-next'
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
const authStore = useAuthStore()
const birthdays = ref([]) const birthdays = ref([])
const loadingBirthdays = ref(true) const loadingBirthdays = ref(true)

View File

@@ -1000,11 +1000,6 @@ const canEdit = computed(() => {
return authStore.hasAnyRole('admin', 'vorstand') return authStore.hasAnyRole('admin', 'vorstand')
}) })
const canViewContactData = computed(() => {
// Explicitly check for 'vorstand' role only
return authStore.hasRole('vorstand')
})
const isBirthdateRequired = computed(() => { const isBirthdateRequired = computed(() => {
return !editingMember.value || Boolean(editingMember.value?.geburtsdatum) return !editingMember.value || Boolean(editingMember.value?.geburtsdatum)
}) })

View File

@@ -162,8 +162,6 @@ const selectedGroup = computed(() => {
}) })
const isLoggedIn = computed(() => authStore.isLoggedIn) const isLoggedIn = computed(() => authStore.isLoggedIn)
const userEmail = computed(() => authStore.user?.email || '')
const userName = computed(() => authStore.user?.name || '')
async function loadGroups() { async function loadGroups() {
try { try {

View File

@@ -394,7 +394,6 @@ const formData = ref({
const isLoading = ref(false) const isLoading = ref(false)
const errorMessage = ref('') const errorMessage = ref('')
const successMessage = ref('') const successMessage = ref('')
const usePasskey = ref(false)
const isPasskeySupported = ref(false) const isPasskeySupported = ref(false)
const passkeySupportReason = ref('') const passkeySupportReason = ref('')
const setPasswordForPasskey = ref(true) const setPasswordForPasskey = ref(true)
@@ -424,7 +423,6 @@ const handleFormSubmit = (event) => {
// console.log('[DEBUG] Calling handleRegister...') // console.log('[DEBUG] Calling handleRegister...')
handleRegister() handleRegister()
} }
const showDebugInfo = ref(false)
const debugChallenge = ref('') const debugChallenge = ref('')
const debugRpId = ref('') const debugRpId = ref('')
const debugRegistrationId = ref('') const debugRegistrationId = ref('')
@@ -697,8 +695,6 @@ const handleRegisterWithPasskey = async () => {
debugSmartphoneUrl.value = `${window.location.origin}/passkey-register-cross-device?registrationId=${pre.registrationId}` debugSmartphoneUrl.value = `${window.location.origin}/passkey-register-cross-device?registrationId=${pre.registrationId}`
} }
showDebugInfo.value = true
console.log('[DEBUG] QR-Code Info (for Cross-Device):', { console.log('[DEBUG] QR-Code Info (for Cross-Device):', {
challenge: pre.options?.challenge, challenge: pre.options?.challenge,
challengeLength: pre.options?.challenge?.length, challengeLength: pre.options?.challenge?.length,

View File

@@ -5,6 +5,7 @@
Geschichte Geschichte
</h1> </h1>
<!-- nosemgrep: javascript.vue.security.audit.xss.templates.avoid-v-html --> <!-- nosemgrep: javascript.vue.security.audit.xss.templates.avoid-v-html -->
<!-- eslint-disable-next-line vue/no-v-html -->
<div <div
class="prose prose-lg max-w-none" class="prose prose-lg max-w-none"
v-html="content" v-html="content"

View File

@@ -6,6 +6,7 @@
</h1> </h1>
<!-- nosemgrep: javascript.vue.security.audit.xss.templates.avoid-v-html --> <!-- nosemgrep: javascript.vue.security.audit.xss.templates.avoid-v-html -->
<!-- eslint-disable-next-line vue/no-v-html -->
<div <div
class="prose prose-lg max-w-none mb-8" class="prose prose-lg max-w-none mb-8"
v-html="content" v-html="content"

View File

@@ -5,6 +5,7 @@
TT-Regeln TT-Regeln
</h1> </h1>
<!-- nosemgrep: javascript.vue.security.audit.xss.templates.avoid-v-html --> <!-- nosemgrep: javascript.vue.security.audit.xss.templates.avoid-v-html -->
<!-- eslint-disable-next-line vue/no-v-html -->
<div <div
class="prose prose-lg max-w-none" class="prose prose-lg max-w-none"
v-html="content" v-html="content"

View File

@@ -5,6 +5,7 @@
Über uns Über uns
</h1> </h1>
<!-- nosemgrep: javascript.vue.security.audit.xss.templates.avoid-v-html --> <!-- nosemgrep: javascript.vue.security.audit.xss.templates.avoid-v-html -->
<!-- eslint-disable-next-line vue/no-v-html -->
<div <div
class="prose prose-lg max-w-none" class="prose prose-lg max-w-none"
v-html="content" v-html="content"

View File

@@ -9,9 +9,6 @@ import { getClientIp } from '../../../utils/rate-limit.js'
// Local fallback for Nitro globals when lint/run env doesn't provide them // Local fallback for Nitro globals when lint/run env doesn't provide them
const getMethod = globalThis.getMethod ?? ((e) => (e?.req?.method || e?.method || 'GET')) const getMethod = globalThis.getMethod ?? ((e) => (e?.req?.method || e?.method || 'GET'))
const getRequestURL = globalThis.getRequestURL ?? ((e) => {
try { return new URL(e?.req?.url, 'http://localhost') } catch { return { href: String(e?.req?.url || ''), pathname: String(e?.req?.url || '').split('?')[0] || '' } }
})
function findUserByCredentialId(users, credentialId) { function findUserByCredentialId(users, credentialId) {
const cid = String(credentialId || '') const cid = String(credentialId || '')

View File

@@ -613,7 +613,6 @@ export default defineEventHandler(async (event) => {
// E-Mail senden via zentralen Service (pass full path) // E-Mail senden via zentralen Service (pass full path)
emailResult = await sendMembershipEmailUtil(data, finalPdfPath) emailResult = await sendMembershipEmailUtil(data, finalPdfPath)
// Antragsdaten verschlüsselt speichern // Antragsdaten verschlüsselt speichern
const encryptionKey = process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production'
const encryptedData = JSON.stringify(data) const encryptedData = JSON.stringify(data)
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
// filename is generated from timestamp, not user input, path traversal prevented // filename is generated from timestamp, not user input, path traversal prevented
@@ -674,7 +673,6 @@ export default defineEventHandler(async (event) => {
emailResult = await sendMembershipEmailUtil(data, finalPdfPath) emailResult = await sendMembershipEmailUtil(data, finalPdfPath)
// Antragsdaten verschlüsselt speichern // Antragsdaten verschlüsselt speichern
const encryptionKey = process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production'
const encryptedData = JSON.stringify(data) const encryptedData = JSON.stringify(data)
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
// filename is generated from timestamp, not user input, path traversal prevented // filename is generated from timestamp, not user input, path traversal prevented

View File

@@ -1,6 +1,7 @@
import fs from 'fs/promises' import fs from 'fs/promises'
import path from 'path' import path from 'path'
import { getUserFromToken, hasAnyRole } from '../../../../../utils/auth.js' import { getUserFromToken, hasAnyRole } from '../../../../../utils/auth.js'
import { decryptObject } from '../../../../../utils/encryption.js'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter-posts.json'), never user input // filename is always a hardcoded constant (e.g., 'newsletter-posts.json'), never user input

View File

@@ -1,5 +1,4 @@
import { promises as fs } from 'fs' import { promises as fs } from 'fs'
import path from 'path'
import { getCurrentSeasonSlug, validateSeasonSlug } from '../../utils/spielplan-data.js' import { getCurrentSeasonSlug, validateSeasonSlug } from '../../utils/spielplan-data.js'
import { getServerDataPath } from '../../utils/paths.js' import { getServerDataPath } from '../../utils/paths.js'
import { error as loggerError } from '../../utils/logger.js' import { error as loggerError } from '../../utils/logger.js'

View File

@@ -1,7 +1,7 @@
import { promises as fs } from 'fs' import { promises as fs } from 'fs'
import path from 'path' import path from 'path'
import { randomUUID } from 'crypto' import { randomUUID } from 'crypto'
import { encrypt, decrypt, encryptObject, decryptObject } from './encryption.js' import { encryptObject, decryptObject } from './encryption.js'
import { writeDataFileWithRotation } from './data-file-rotation.js' import { writeDataFileWithRotation } from './data-file-rotation.js'
// Handle both dev and production paths // Handle both dev and production paths

View File

@@ -106,7 +106,7 @@ export async function fillFormFields(pdfDoc, form, data) {
try { try {
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica) const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica)
form.updateFieldAppearances(helveticaFont) form.updateFieldAppearances(helveticaFont)
} catch (_error) { } catch (error) {
console.warn('Could not update field appearances:', error.message) console.warn('Could not update field appearances:', error.message)
} }
} }
@@ -123,7 +123,7 @@ export async function fillPdfForm(pdfDoc, form, data) {
// Check if PLZ/Ort field on page 1 is empty and fix it // Check if PLZ/Ort field on page 1 is empty and fix it
await fixPLZOrtField(pdfDoc, data) await fixPLZOrtField(pdfDoc, data)
} catch (_error) { } catch (error) {
console.warn('Form filling failed, using fallback:', error.message) console.warn('Form filling failed, using fallback:', error.message)
await fillFormFieldsPositionally(pdfDoc, data) await fillFormFieldsPositionally(pdfDoc, data)
} }
@@ -136,7 +136,6 @@ export async function fillPdfForm(pdfDoc, form, data) {
*/ */
async function fixPLZOrtField(pdfDoc, data) { async function fixPLZOrtField(pdfDoc, data) {
try { try {
const pages = pdfDoc.getPages()
await pdfDoc.embedFont(StandardFonts.Helvetica) await pdfDoc.embedFont(StandardFonts.Helvetica)
// Draw PLZ/Ort at the correct position on page 1 // Draw PLZ/Ort at the correct position on page 1
@@ -160,7 +159,7 @@ async function fixPLZOrtField(pdfDoc, data) {
} }
} }
} catch (_error) { } catch (error) {
console.warn('Could not fix PLZ/Ort field:', error.message) console.warn('Could not fix PLZ/Ort field:', error.message)
} }
} }
@@ -221,7 +220,7 @@ async function fillFormFieldsPositionally(pdfDoc, data) {
firstPage.drawText('X', { x: 116, y: -8, size: 12, font: helveticaFont }) firstPage.drawText('X', { x: 116, y: -8, size: 12, font: helveticaFont })
} }
} catch (_error) { } catch (error) {
console.error('Positional filling failed:', error.message) console.error('Positional filling failed:', error.message)
} }
} }

View File

@@ -54,7 +54,7 @@ export class PDFGeneratorService {
const pdfBytes = await pdfDoc.save() const pdfBytes = await pdfDoc.save()
return new PDFGenerationResult(true, Buffer.from(pdfBytes), filename) return new PDFGenerationResult(true, Buffer.from(pdfBytes), filename)
} catch (_error) { } catch (error) {
console.error('Template PDF generation failed:', error.message) console.error('Template PDF generation failed:', error.message)
return new PDFGenerationResult(false, null, null, error.message) return new PDFGenerationResult(false, null, null, error.message)
} }
@@ -84,7 +84,7 @@ export class PDFGeneratorService {
* @param {Object} data - Form data * @param {Object} data - Form data
* @returns {string} Filename * @returns {string} Filename
*/ */
generateFilename(data) { generateFilename(_data) {
const timestamp = Date.now() const timestamp = Date.now()
return `beitrittserklärung_${timestamp}.pdf` return `beitrittserklärung_${timestamp}.pdf`
} }

View File

@@ -57,16 +57,6 @@ function toNumberOrNull(value) {
return Number.isNaN(numberValue) ? null : numberValue return Number.isNaN(numberValue) ? null : numberValue
} }
function normalizeName(value) {
return String(value || '')
.trim()
.toLowerCase()
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/\s+/g, ' ')
.replace(/['`]/g, '')
}
function normalizeGender(value) { function normalizeGender(value) {
const normalized = String(value || '').trim().toLowerCase() const normalized = String(value || '').trim().toLowerCase()
if (normalized === 'm' || normalized === 'männlich') return 'männlich' if (normalized === 'm' || normalized === 'männlich') return 'männlich'

View File

@@ -1,7 +1,7 @@
import { promises as fs } from 'fs' import { promises as fs } from 'fs'
import path from 'path' import path from 'path'
import { getProjectPath, getServerDataPath } from './paths.js' import { getProjectPath, getServerDataPath } from './paths.js'
import { error as loggerError, info as loggerInfo } from './logger.js' import { error as loggerError } from './logger.js'
const SPIELPLAN_HEADERS = [ const SPIELPLAN_HEADERS = [
'Termin', 'Termin',

View File

@@ -12,10 +12,6 @@ const OUTPUT_DIR = getServerDataPath('spielplan-import')
const JSON_FILE = path.join(OUTPUT_DIR, 'harheimer_tc_spielplan.json') const JSON_FILE = path.join(OUTPUT_DIR, 'harheimer_tc_spielplan.json')
const HTML_FILE = path.join(OUTPUT_DIR, 'harheimer_tc_spielplan.html') const HTML_FILE = path.join(OUTPUT_DIR, 'harheimer_tc_spielplan.html')
function pad2(value) {
return String(value).padStart(2, '0')
}
export function getSpieljahrForDate(date = new Date()) { export function getSpieljahrForDate(date = new Date()) {
const year = date.getFullYear() const year = date.getFullYear()
const startYear = date.getMonth() >= 6 ? year : year - 1 const startYear = date.getMonth() >= 6 ? year : year - 1

View File

@@ -1,5 +1,4 @@
import { promises as fs } from 'fs' import { promises as fs } from 'fs'
import path from 'path'
import { randomUUID } from 'crypto' import { randomUUID } from 'crypto'
import { writeDataFileWithRotation } from './data-file-rotation.js' import { writeDataFileWithRotation } from './data-file-rotation.js'