From 3d6646cf314b8023ebb75b2831799e91e209e4f3 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 10 Nov 2025 13:18:29 +0100 Subject: [PATCH] =?UTF-8?q?Enhance=20authentication=20checks=20in=20CMS=20?= =?UTF-8?q?API=20endpoints;=20implement=20user=20role=20validation=20for?= =?UTF-8?q?=20admin=20and=20board=20access.=20Refactor=20Spielpl=C3=A4ne?= =?UTF-8?q?=20API=20to=20remove=20unnecessary=20logging=20and=20improve=20?= =?UTF-8?q?error=20handling.=20Update=20tests=20to=20mock=20user=20authent?= =?UTF-8?q?ication=20and=20ensure=20proper=20validation=20of=20file=20uplo?= =?UTF-8?q?ads.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/api/cms/satzung-upload.post.js | 18 ++++++++++++ server/api/cms/save-csv.post.js | 18 ++++++++++++ server/api/cms/upload-spielplan-pdf.post.js | 32 ++++++++++++++++++--- server/api/spielplaene.get.js | 12 ++------ tests/cms-files-endpoints.spec.ts | 19 +++++++++--- 5 files changed, 81 insertions(+), 18 deletions(-) diff --git a/server/api/cms/satzung-upload.post.js b/server/api/cms/satzung-upload.post.js index 7d4ea75..d20733b 100644 --- a/server/api/cms/satzung-upload.post.js +++ b/server/api/cms/satzung-upload.post.js @@ -3,6 +3,7 @@ import fs from 'fs/promises' import path from 'path' import { exec } from 'child_process' import { promisify } from 'util' +import { getUserFromToken } from '../../utils/auth.js' const execAsync = promisify(exec) @@ -51,6 +52,23 @@ export default defineEventHandler(async (event) => { }) } + let token = getCookie(event, 'auth_token') + const currentUser = token ? await getUserFromToken(token) : null + + if (!currentUser) { + throw createError({ + statusCode: 401, + statusMessage: 'Nicht authentifiziert' + }) + } + + if (currentUser.role !== 'admin' && currentUser.role !== 'vorstand') { + throw createError({ + statusCode: 403, + statusMessage: 'Keine Berechtigung' + }) + } + try { // Multer-Middleware für File-Upload await new Promise((resolve, reject) => { diff --git a/server/api/cms/save-csv.post.js b/server/api/cms/save-csv.post.js index d1c5236..ed3aff3 100644 --- a/server/api/cms/save-csv.post.js +++ b/server/api/cms/save-csv.post.js @@ -1,8 +1,26 @@ import fs from 'fs/promises' import path from 'path' +import { getUserFromToken } from '../../utils/auth.js' export default defineEventHandler(async (event) => { try { + const token = getCookie(event, 'auth_token') + const currentUser = token ? await getUserFromToken(token) : null + + if (!currentUser) { + throw createError({ + statusCode: 401, + statusMessage: 'Nicht authentifiziert' + }) + } + + if (currentUser.role !== 'admin' && currentUser.role !== 'vorstand') { + throw createError({ + statusCode: 403, + statusMessage: 'Keine Berechtigung' + }) + } + const { filename, content } = await readBody(event) if (!filename || !content) { diff --git a/server/api/cms/upload-spielplan-pdf.post.js b/server/api/cms/upload-spielplan-pdf.post.js index 5b797bb..97a0ec2 100644 --- a/server/api/cms/upload-spielplan-pdf.post.js +++ b/server/api/cms/upload-spielplan-pdf.post.js @@ -1,6 +1,7 @@ import multer from 'multer' import fs from 'fs/promises' import path from 'path' +import { getUserFromToken } from '../../utils/auth.js' // Multer-Konfiguration für PDF-Uploads const storage = multer.diskStorage({ @@ -31,12 +32,35 @@ const upload = multer({ export default defineEventHandler(async (event) => { try { - // Prüfe Authentifizierung - const authHeader = getHeader(event, 'authorization') - if (!authHeader || !authHeader.startsWith('Bearer ')) { + let token = getCookie(event, 'auth_token') + + if (!token) { + const authHeader = getHeader(event, 'authorization') + if (authHeader && authHeader.startsWith('Bearer ')) { + token = authHeader.substring(7).trim() + } + } + + if (!token) { throw createError({ statusCode: 401, - statusMessage: 'Nicht autorisiert' + statusMessage: 'Nicht authentifiziert' + }) + } + + const currentUser = await getUserFromToken(token) + + if (!currentUser) { + throw createError({ + statusCode: 401, + statusMessage: 'Nicht authentifiziert' + }) + } + + if (currentUser.role !== 'admin' && currentUser.role !== 'vorstand') { + throw createError({ + statusCode: 403, + statusMessage: 'Keine Berechtigung' }) } diff --git a/server/api/spielplaene.get.js b/server/api/spielplaene.get.js index d3069cd..c677e61 100644 --- a/server/api/spielplaene.get.js +++ b/server/api/spielplaene.get.js @@ -4,21 +4,16 @@ import path from 'path' export default defineEventHandler(async (event) => { try { const spielplaeneDir = path.join(process.cwd(), 'public', 'spielplaene') - - console.log('=== SPIELPLÄNE API ===') - console.log('Verzeichnis:', spielplaeneDir) - + // Prüfe, ob das Verzeichnis existiert try { await fs.access(spielplaeneDir) } catch { - console.log('Verzeichnis nicht gefunden') return [] } // Lese alle Dateien im Verzeichnis const dateien = await fs.readdir(spielplaeneDir) - console.log('Alle Dateien:', dateien) // Filtere nur relevante Dateitypen const erlaubteExtensions = ['.pdf', '.xlsx', '.xls', '.doc', '.docx'] @@ -26,10 +21,7 @@ export default defineEventHandler(async (event) => { const ext = path.extname(datei).toLowerCase() return erlaubteExtensions.includes(ext) }) - - console.log('Gefilterte Dateien:', gefiltert) - console.log('Anzahl:', gefiltert.length) - + return gefiltert } catch (error) { console.error('Fehler beim Lesen der Spielpläne:', error) diff --git a/tests/cms-files-endpoints.spec.ts b/tests/cms-files-endpoints.spec.ts index a1fc1b3..ce03943 100644 --- a/tests/cms-files-endpoints.spec.ts +++ b/tests/cms-files-endpoints.spec.ts @@ -2,6 +2,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createEvent, mockSuccessReadBody } from './setup' import fs from 'fs/promises' +vi.mock('../server/utils/auth.js', () => ({ + getUserFromToken: vi.fn() +})) + vi.mock('multer', () => { const single = vi.fn((field) => (req, _res, cb) => { if (req.__mockMulterError) { @@ -35,22 +39,26 @@ import saveCsvHandler from '../server/api/cms/save-csv.post.js' import uploadSpielplanHandler from '../server/api/cms/upload-spielplan-pdf.post.js' import satzungUploadHandler from '../server/api/cms/satzung-upload.post.js' +const { getUserFromToken } = await import('../server/utils/auth.js') + describe('CMS File Endpoints', () => { beforeEach(() => { vi.restoreAllMocks() vi.clearAllMocks() + getUserFromToken.mockReset() + getUserFromToken.mockResolvedValue({ id: 'admin', role: 'admin' }) }) describe('POST /api/cms/save-csv', () => { it('validiert Eingaben', async () => { - const event = createEvent() + const event = createEvent({ cookies: { auth_token: 'token' } }) mockSuccessReadBody({}) await expect(saveCsvHandler(event)).rejects.toMatchObject({ statusCode: 400 }) }) it('speichert erlaubte Datei', async () => { - const event = createEvent() + const event = createEvent({ cookies: { auth_token: 'token' } }) mockSuccessReadBody({ filename: 'mannschaften.csv', content: 'data' }) vi.spyOn(fs, 'mkdir').mockResolvedValue(undefined) vi.spyOn(fs, 'writeFile').mockResolvedValue(undefined) @@ -66,14 +74,16 @@ describe('CMS File Endpoints', () => { const event = createEvent({ method: 'POST' }) event.node.req.__mockFile = { filename: 'file.pdf', originalname: 'orig.pdf', path: 'tmp', mimetype: 'application/pdf' } event.node.req.body = { type: 'gesamt' } + getUserFromToken.mockResolvedValue(null) await expect(uploadSpielplanHandler(event)).rejects.toMatchObject({ statusCode: 401 }) }) it('lädt PDF hoch und gibt Erfolg zurück', async () => { - const event = createEvent({ method: 'POST', headers: { authorization: 'Bearer token' } }) + const event = createEvent({ method: 'POST', headers: { authorization: 'Bearer valid-token' } }) event.node.req.__mockFile = { filename: 'spielplan_gesamt.pdf', originalname: 'orig.pdf', path: 'tmp', mimetype: 'application/pdf' } event.node.req.body = { type: 'gesamt' } + getUserFromToken.mockResolvedValue({ id: 'admin', role: 'admin' }) const response = await uploadSpielplanHandler(event) expect(response.success).toBe(true) @@ -83,10 +93,11 @@ describe('CMS File Endpoints', () => { describe('POST /api/cms/satzung-upload', () => { it('verarbeitet hochgeladene Satzung', async () => { - const event = createEvent({ method: 'POST' }) + const event = createEvent({ method: 'POST', cookies: { auth_token: 'token' } }) event.node.req.__mockFile = { path: 'public/documents/satzung.pdf', filename: 'satzung.pdf', originalname: 'satzung.pdf', mimetype: 'application/pdf' } vi.spyOn(fs, 'readFile').mockResolvedValueOnce(JSON.stringify({ seiten: {}, vorstand: { vorsitzender: { email: '' }, schriftfuehrer: { email: '' } } })) vi.spyOn(fs, 'writeFile').mockResolvedValue(undefined) + getUserFromToken.mockResolvedValue({ id: 'admin', role: 'admin' }) const response = await satzungUploadHandler(event) expect(response.success).toBe(true)