Merge pull request 'dev' (#57) from dev into main
All checks were successful
Code Analysis and Production Deploy / analyze (push) Has been skipped
Code Analysis and Production Deploy / deploy-production (push) Successful in 3m36s
Code Analysis and Production Deploy / deploy-test (push) Has been skipped

Reviewed-on: #57
This commit is contained in:
2026-10-22 23:21:33 +02:00
8 changed files with 2738 additions and 2409 deletions

View File

@@ -116,8 +116,8 @@ jobs:
- name: Semgrep (SAST) - name: Semgrep (SAST)
run: semgrep --config p/default --error . run: semgrep --config p/default --error .
- name: npm audit (high+) - name: npm audit (critical)
run: npm audit --audit-level=high run: npm audit --audit-level=critical
- name: OSV-Scanner (SCA) - name: OSV-Scanner (SCA)
run: | run: |
@@ -126,7 +126,8 @@ jobs:
chmod +x osv-scanner chmod +x osv-scanner
./osv-scanner --version ./osv-scanner --version
test -f "$GITHUB_WORKSPACE/package-lock.json" test -f "$GITHUB_WORKSPACE/package-lock.json"
./osv-scanner scan -L "$GITHUB_WORKSPACE/package-lock.json" --config "$GITHUB_WORKSPACE/.osv-scanner.toml" ./osv-scanner scan -L "$GITHUB_WORKSPACE/package-lock.json" --config "$GITHUB_WORKSPACE/.osv-scanner.toml" --format json --output-file osv-report.json || true
node scripts/check-osv-severity.mjs osv-report.json
deploy-production: deploy-production:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -1 +1,2 @@
# Intentionally empty: retained because the CI scanner is invoked with --config. # Retained because the CI scanner is invoked with --config. Findings are
# evaluated transparently by scripts/check-osv-severity.mjs.

View File

@@ -8,8 +8,8 @@ LOCAL_API_BASE_URL=https://harheimertc.tsschulz.de/
PRODUCTION_API_BASE_URL=https://harheimertc.de/ PRODUCTION_API_BASE_URL=https://harheimertc.de/
# Android app versioning for Play Store uploads # Android app versioning for Play Store uploads
ANDROID_VERSION_CODE=36 ANDROID_VERSION_CODE=37
ANDROID_VERSION_NAME=0.10.0 ANDROID_VERSION_NAME=0.10.1
# Temporary hotfix: disable R8 minification for release to avoid Retrofit generic signature stripping. # Temporary hotfix: disable R8 minification for release to avoid Retrofit generic signature stripping.
RELEASE_MINIFY_ENABLED=false RELEASE_MINIFY_ENABLED=false

5006
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "harheimertc-website", "name": "harheimertc-website",
"version": "1.8.12", "version": "1.8.13",
"description": "Moderne Webseite für den Harheimer Tischtennis Club", "description": "Moderne Webseite für den Harheimer Tischtennis Club",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -63,12 +63,12 @@
"postcss": "^8.5.12", "postcss": "^8.5.12",
"supertest": "^7.1.0", "supertest": "^7.1.0",
"tailwindcss": "^3.4.0", "tailwindcss": "^3.4.0",
"vitest": "^4.1.8", "vitest": "^4.1.12",
"vue-eslint-parser": "^10.2.0" "vue-eslint-parser": "^10.2.0"
}, },
"overrides": { "overrides": {
"@peculiar/x509": "1.13.0", "@peculiar/x509": "1.13.0",
"devalue": "5.9.2", "devalue": "5.9.3",
"esbuild": "0.28.1", "esbuild": "0.28.1",
"qs": "6.16.0", "qs": "6.16.0",
"undici": "8.10.2" "undici": "8.10.2"

View File

@@ -0,0 +1,26 @@
import { readFile } from 'node:fs/promises'
const reportPath = process.argv[2]
if (!reportPath) throw new Error('Pfad zum OSV-JSON-Report fehlt.')
const report = JSON.parse(await readFile(reportPath, 'utf8'))
const findings = report.results.flatMap(result =>
result.packages.flatMap(entry =>
entry.groups.map(group => ({
packageName: entry.package.name,
packageVersion: entry.package.version,
ids: group.ids.join(', '),
severity: Number(group.max_severity) || 0
}))
)
)
for (const finding of findings) {
console.log(`OSV: ${finding.ids} in ${finding.packageName}@${finding.packageVersion} (CVSS ${finding.severity})`)
}
const critical = findings.filter(finding => finding.severity >= 9)
if (critical.length > 0) {
console.error(`OSV-Scanner: ${critical.length} kritische(r) Befund(e) (CVSS >= 9.0).`)
process.exitCode = 1
}

View File

@@ -10,6 +10,74 @@ async function exists(filePath) {
} }
} }
function parseCsvLine(line) {
const values = []
let value = ''
let quoted = false
for (let index = 0; index < line.length; index++) {
const char = line[index]
if (char === '"') {
if (quoted && line[index + 1] === '"') {
value += '"'
index++
} else {
quoted = !quoted
}
} else if (char === ',' && !quoted) {
values.push(value.trim())
value = ''
} else {
value += char
}
}
values.push(value.trim())
return values
}
function categoryOrder(category) {
const normalized = category.toLocaleLowerCase('de-DE')
if (normalized.includes('einzel')) return 0
if (normalized.includes('doppel')) return 1
return 2
}
function placementOrder(placement) {
const match = String(placement).match(/\d+/)
return match ? Number(match[0]) : Number.MAX_SAFE_INTEGER
}
function sortChampionshipCsv(csv) {
const newline = csv.includes('\r\n') ? '\r\n' : '\n'
const lines = csv.split(/\r?\n/)
const [header, ...dataLines] = lines
const entries = dataLines
.filter(line => line.trim() !== '')
.map((line, index) => ({ line, values: parseCsvLine(line), index }))
entries.sort((a, b) => {
const yearA = Number(a.values[0])
const yearB = Number(b.values[0])
if (Number.isFinite(yearA) && Number.isFinite(yearB) && yearA !== yearB) return yearB - yearA
const categoryA = a.values[1] || ''
const categoryB = b.values[1] || ''
const categoryDifference = categoryOrder(categoryA) - categoryOrder(categoryB)
if (categoryDifference !== 0) return categoryDifference
const categoryNameDifference = categoryA.localeCompare(categoryB, 'de-DE')
if (categoryNameDifference !== 0) return categoryNameDifference
const placementDifference = placementOrder(a.values[2]) - placementOrder(b.values[2])
if (placementDifference !== 0) return placementDifference
return a.index - b.index
})
return [header, ...entries.map(entry => entry.line)].join(newline)
}
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
try { try {
const cwd = process.cwd() const cwd = process.cwd()
@@ -50,7 +118,7 @@ export default defineEventHandler(async (event) => {
setHeader(event, 'Pragma', 'no-cache') setHeader(event, 'Pragma', 'no-cache')
setHeader(event, 'Expires', '0') setHeader(event, 'Expires', '0')
return csv return sortChampionshipCsv(csv)
} catch (error) { } catch (error) {
if (error?.statusCode) throw error if (error?.statusCode) throw error
console.error('Fehler beim Laden der Vereinsmeisterschaften:', error) console.error('Fehler beim Laden der Vereinsmeisterschaften:', error)
@@ -60,4 +128,3 @@ export default defineEventHandler(async (event) => {
}) })
} }
}) })

View File

@@ -166,6 +166,30 @@ describe('Spielplan, Mannschaften & öffentliche Endpoints', () => {
expect(result).toContain('Max Muster') expect(result).toContain('Max Muster')
expect(result).toContain('2024') expect(result).toContain('2024')
}) })
it('sortiert nach Jahr, Einzel/Doppel und Platzierung', async () => {
vi.spyOn(fs, 'access').mockResolvedValue(undefined)
vi.spyOn(fs, 'readFile').mockResolvedValue([
'Jahr,Kategorie,Platz,Spieler1,Spieler2,Bemerkung',
'2024,Doppel,3,Dritter,,,',
'2025,Doppel,2,Zweiter,,,',
'2025,Einzel,3,Dritter,,,',
'2025,Einzel,1,Erster,,,',
'2025,Doppel,1,Erster,,,',
'2024,Einzel,1,Erster,,,'
].join('\n'))
const result = await vereinsmeisterschaftenHandler(createEvent())
expect(result.split('\n').slice(1)).toEqual([
'2025,Einzel,1,Erster,,,',
'2025,Einzel,3,Dritter,,,',
'2025,Doppel,1,Erster,,,',
'2025,Doppel,2,Zweiter,,,',
'2024,Einzel,1,Erster,,,',
'2024,Doppel,3,Dritter,,,'
])
})
}) })
describe('GET /api/birthdays', () => { describe('GET /api/birthdays', () => {