From 7c9862a08a7d1bda9c78337b852e84220de750f1 Mon Sep 17 00:00:00 2001 From: BibaBot Jarvis Date: Mon, 16 Mar 2026 00:07:16 +0000 Subject: [PATCH] feat: implement role-based access control and auth routes --- backend/middleware/auditLogger.js | 49 ++--- backend/middleware/requireRole.js | 34 ++-- backend/routes/auth.js | 306 ++++++++++++++++++++++++++++-- 3 files changed, 344 insertions(+), 45 deletions(-) diff --git a/backend/middleware/auditLogger.js b/backend/middleware/auditLogger.js index 8944e0e..f4e0358 100644 --- a/backend/middleware/auditLogger.js +++ b/backend/middleware/auditLogger.js @@ -1,28 +1,33 @@ // middleware/auditLogger.js -const fs = require('fs').promises; -const path = require('path'); +const db = require('../db'); -// In a real app, this would write to a database -async function auditLogger(req, res, next) { - const logEntry = { - timestamp: new Date().toISOString(), - actorUserId: req.user?.id || 'anonymous', - action: `${req.method} ${req.path}`, - targetType: req.route?.path || 'unknown', - targetId: req.params?.id || 'unknown', - userAgent: req.get('User-Agent'), - ip: req.ip +/** + * Middleware zur Protokollierung sensibler Aktionen + * @param {string} action - Name der Aktion (z.B. 'USER_SUSPEND') + * @param {string} targetType - Typ des Zielobjekts (z.B. 'user') + * @returns {function} Express-Middleware-Funktion + */ +function auditLogger(action, targetType) { + return async (req, res, next) => { + try { + const timestamp = new Date().toISOString(); + const actorUserId = req.user?.id || null; + const targetId = req.params.id || req.body.id || null; + const reason = req.body.reason || null; + + // Audit-Eintrag in die Datenbank schreiben + await db.run( + 'INSERT INTO audit_log (timestamp, actor_user_id, action, target_type, target_id, reason) VALUES (?, ?, ?, ?, ?, ?)', + [timestamp, actorUserId, action, targetType, targetId, reason] + ); + + next(); + } catch (err) { + console.error('Audit logging failed:', err); + // Fehler bei Audit-Logging sollte nicht den Request blockieren + next(); + } }; - - // Log to file (in real app, this would be a DB insert) - try { - const logPath = path.join(__dirname, '../logs/audit.log'); - await fs.appendFile(logPath, JSON.stringify(logEntry) + '\n'); - } catch (error) { - console.error('Failed to write audit log:', error); - } - - next(); } module.exports = auditLogger; \ No newline at end of file diff --git a/backend/middleware/requireRole.js b/backend/middleware/requireRole.js index 3d51450..1c475ac 100644 --- a/backend/middleware/requireRole.js +++ b/backend/middleware/requireRole.js @@ -1,18 +1,30 @@ // middleware/requireRole.js -const requireRole = (allowedRoles) => { +const jwt = require('jsonwebtoken'); + +/** + * Middleware zur Prüfung der Benutzerrolle + * @param {string[]} allowedRoles - Erlaubte Rollen + * @returns {function} Express-Middleware-Funktion + */ +function requireRole(allowedRoles) { return (req, res, next) => { - const userRole = req.user?.role; - - if (!userRole) { - return res.status(401).json({ error: 'Authorization required' }); + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Authorization header missing or invalid' }); } - - if (!allowedRoles.includes(userRole)) { - return res.status(403).json({ error: 'Insufficient permissions' }); + + const token = authHeader.substring(7); // "Bearer " entfernen + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + if (!decoded.role || !allowedRoles.includes(decoded.role)) { + return res.status(403).json({ error: 'Insufficient permissions' }); + } + req.user = decoded; // Nutzerdaten an die Request-Objekt anhängen + next(); + } catch (err) { + return res.status(401).json({ error: 'Invalid token' }); } - - next(); }; -}; +} module.exports = requireRole; \ No newline at end of file diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 8a94b26..f78d0d8 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,27 +1,309 @@ // routes/auth.js const express = require('express'); const router = express.Router(); -const { authenticateUser } = require('../middleware/auth'); +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +const db = require('../db'); const requireRole = require('../middleware/requireRole'); -// Public route - register +/** + * @swagger + * /auth/register: + * post: + * summary: Registriert einen neuen Benutzer + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - username + * - email + * - password + * properties: + * username: + * type: string + * email: + * type: string + * password: + * type: string + * responses: + * 201: + * description: Benutzer erfolgreich registriert + * content: + * application/json: + * schema: + * type: object + * properties: + * user: + * type: object + * properties: + * id: + * type: integer + * username: + * type: string + * email: + * type: string + * role: + * type: string + * token: + * type: string + * 400: + * description: Ungültige Eingabedaten + * 409: + * description: Benutzer existiert bereits + */ router.post('/register', async (req, res) => { - // Implementation for user registration + const { username, email, password } = req.body; + + if (!username || !email || !password) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + try { + // Prüfen, ob Benutzer bereits existiert + const existingUser = await db.get('SELECT id FROM users WHERE email = ?', [email]); + if (existingUser) { + return res.status(409).json({ error: 'User already exists' }); + } + + // Passwort hashen + const hashedPassword = await bcrypt.hash(password, 10); + + // Neuen Benutzer anlegen + const result = await db.run( + 'INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, ?)', + [username, email, hashedPassword, 'user'] + ); + + // Token generieren + const token = jwt.sign( + { id: result.lastID, username, email, role: 'user' }, + process.env.JWT_SECRET, + { expiresIn: '24h' } + ); + + res.status(201).json({ + user: { + id: result.lastID, + username, + email, + role: 'user' + }, + token + }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Internal server error' }); + } }); -// Public route - login -router.post('/login', authenticateUser, (req, res) => { - // Implementation for user login +/** + * @swagger + * /auth/login: + * post: + * summary: Loggt einen Benutzer ein + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * password: + * type: string + * responses: + * 200: + * description: Login erfolgreich + * content: + * application/json: + * schema: + * type: object + * properties: + * user: + * type: object + * properties: + * id: + * type: integer + * username: + * type: string + * email: + * type: string + * role: + * type: string + * token: + * type: string + * 400: + * description: Ungültige Eingabedaten + * 401: + * description: Falsche Anmeldedaten + */ +router.post('/login', async (req, res) => { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ error: 'Missing email or password' }); + } + + try { + const user = await db.get('SELECT id, username, email, password_hash, role FROM users WHERE email = ?', [email]); + if (!user) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + const isValidPassword = await bcrypt.compare(password, user.password_hash); + if (!isValidPassword) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + // Token generieren + const token = jwt.sign( + { id: user.id, username: user.username, email: user.email, role: user.role }, + process.env.JWT_SECRET, + { expiresIn: '24h' } + ); + + res.json({ + user: { + id: user.id, + username: user.username, + email: user.email, + role: user.role + }, + token + }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Internal server error' }); + } }); -// Protected route - get user profile -router.get('/profile', requireRole(['user', 'moderator', 'admin']), (req, res) => { - // Implementation for getting user profile +/** + * @swagger + * /auth/profile: + * get: + * summary: Holt das Profil des eingeloggten Benutzers + * tags: [Auth] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Profil erfolgreich abgerufen + * content: + * application/json: + * schema: + * type: object + * properties: + * user: + * type: object + * properties: + * id: + * type: integer + * username: + * type: string + * email: + * type: string + * role: + * type: string + * 401: + * description: Nicht autorisiert + */ +router.get('/profile', requireRole(['user', 'moderator', 'admin']), async (req, res) => { + try { + const user = await db.get('SELECT id, username, email, role FROM users WHERE id = ?', [req.user.id]); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + res.json({ user }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Internal server error' }); + } }); -// Protected route - update user profile -router.put('/profile', requireRole(['user', 'moderator', 'admin']), (req, res) => { - // Implementation for updating user profile +/** + * @swagger + * /auth/profile: + * put: + * summary: Aktualisiert das Profil des eingeloggten Benutzers + * tags: [Auth] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * username: + * type: string + * email: + * type: string + * responses: + * 200: + * description: Profil erfolgreich aktualisiert + * content: + * application/json: + * schema: + * type: object + * properties: + * user: + * type: object + * properties: + * id: + * type: integer + * username: + * type: string + * email: + * type: string + * role: + * type: string + * 400: + * description: Ungültige Eingabedaten + * 401: + * description: Nicht autorisiert + */ +router.put('/profile', requireRole(['user', 'moderator', 'admin']), async (req, res) => { + const { username, email } = req.body; + + if (!username && !email) { + return res.status(400).json({ error: 'At least one field must be provided' }); + } + + try { + let updateFields = []; + let values = []; + + if (username) { + updateFields.push('username = ?'); + values.push(username); + } + if (email) { + updateFields.push('email = ?'); + values.push(email); + } + + values.push(req.user.id); + + const query = `UPDATE users SET ${updateFields.join(', ')} WHERE id = ?`; + await db.run(query, values); + + // Aktualisierte Daten abrufen + const updatedUser = await db.get('SELECT id, username, email, role FROM users WHERE id = ?', [req.user.id]); + res.json({ user: updatedUser }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Internal server error' }); + } }); module.exports = router; \ No newline at end of file