diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 8c7237f..f988b00 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,25 +1,20 @@ ## Beschreibung -Erstelle eine neue Issue, die das Rollen- und Rechtekonzept für das Projekt `helpyourneighbour` dokumentiert und implementiert. +Implementiere eine neue API-Endpunkt für die Verwaltung von Benutzerrollen im System. -## Aufgaben +## Anforderungen -- [ ] Dokumentation des Rollen- und Rechtekonzepts in `docs/roles-and-permissions.md` -- [ ] Implementierung der Middleware zur Prüfung der Benutzerrolle (`backend/middleware/requireRole.js`) -- [ ] Implementierung der Middleware zur Protokollierung sensibler Aktionen (`backend/middleware/auditLogger.js`) -- [ ] Integration der Middleware in die Auth-Routen (`backend/routes/auth.js`) -- [ ] Test der Funktionalität +- Erstelle einen neuen Endpunkt `/api/users/:userId/roles` +- Unterstütze folgende Methoden: + - `GET` - Liefert die Rollen eines Benutzers + - `PUT` - Ändert die Rollen eines Benutzers + - `DELETE` - Entfernt alle Rollen eines Benutzers +- Implementiere eine Middleware zur Überprüfung der Berechtigungen (nur Admins dürfen Rollen ändern) +- Füge Tests für den neuen Endpunkt hinzu ## Akzeptanzkriterien -- Die Dokumentation des Rollen- und Rechtekonzepts ist vollständig -- Die Middleware zur Prüfung der Benutzerrolle funktioniert korrekt -- Die Middleware zur Protokollierung sensibler Aktionen funktioniert korrekt -- Die Auth-Routen verwenden die neuen Middlewares -- Alle Tests bestehen - -## Weitere Informationen - -- Die Implementierung basiert auf JWTs mit `role` Claim -- Sensible Aktionen werden protokolliert -- Es gibt drei Rollen: `user`, `moderator`, `admin` \ No newline at end of file +- [ ] Endpunkt ist implementiert und dokumentiert +- [ ] Berechtigungsprüfung funktioniert korrekt +- [ ] Tests sind erfolgreich +- [ ] Code wurde reviewed und merged \ No newline at end of file diff --git a/backend/app.js b/backend/app.js index dc5c76f..74b2050 100644 --- a/backend/app.js +++ b/backend/app.js @@ -16,7 +16,7 @@ app.use(auditLogger); // Routes app.use('/auth', authRoutes); -app.use('/roles', rolesRoutes); +app.use('/api/users', rolesRoutes); // Health check endpoint app.get('/health', (req, res) => { diff --git a/backend/controllers/roles.controller.js b/backend/controllers/roles.controller.js new file mode 100644 index 0000000..7df0b8c --- /dev/null +++ b/backend/controllers/roles.controller.js @@ -0,0 +1,105 @@ +const { getUserById, updateUser } = require('../services/user.service'); +const { logAudit } = require('../services/audit.service'); + +/** + * Liefert die Rollen eines Benutzers + * @param {Object} req - Express Request Objekt + * @param {Object} res - Express Response Objekt + */ +exports.getUserRoles = async (req, res) => { + try { + const { userId } = req.params; + + const user = await getUserById(userId); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + res.json(user.roles || []); + } catch (error) { + console.error('Error getting user roles:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +/** + * Ändert die Rollen eines Benutzers + * @param {Object} req - Express Request Objekt + * @param {Object} res - Express Response Objekt + */ +exports.updateUserRoles = async (req, res) => { + try { + const { userId } = req.params; + const { roles } = req.body; + + // Validierung der Rollen + if (!Array.isArray(roles)) { + return res.status(400).json({ error: 'Roles must be an array' }); + } + + // Überprüfe, ob alle Rollen gültig sind + const validRoles = ['user', 'moderator', 'admin']; + for (const role of roles) { + if (!validRoles.includes(role)) { + return res.status(400).json({ error: `Invalid role: ${role}` }); + } + } + + const user = await getUserById(userId); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + // Aktualisiere die Rollen + user.roles = roles; + await updateUser(userId, { roles }); + + // Audit-Eintrag + await logAudit({ + actorUserId: req.user?.id || 'system', + action: 'USER_ROLES_UPDATE', + targetType: 'user', + targetId: userId, + details: { oldRoles: user.roles, newRoles: roles } + }); + + res.json({ message: 'Roles updated successfully' }); + } catch (error) { + console.error('Error updating user roles:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +/** + * Entfernt alle Rollen eines Benutzers + * @param {Object} req - Express Request Objekt + * @param {Object} res - Express Response Objekt + */ +exports.deleteUserRoles = async (req, res) => { + try { + const { userId } = req.params; + + const user = await getUserById(userId); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + // Entferne alle Rollen + user.roles = []; + await updateUser(userId, { roles: [] }); + + // Audit-Eintrag + await logAudit({ + actorUserId: req.user?.id || 'system', + action: 'USER_ROLES_DELETE', + targetType: 'user', + targetId: userId, + details: { oldRoles: user.roles, newRoles: [] } + }); + + res.json({ message: 'Roles deleted successfully' }); + } catch (error) { + console.error('Error deleting user roles:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; \ No newline at end of file diff --git a/backend/middleware/role.middleware.js b/backend/middleware/role.middleware.js index b21750a..9343775 100644 --- a/backend/middleware/role.middleware.js +++ b/backend/middleware/role.middleware.js @@ -1,23 +1,42 @@ /** - * Middleware to check if the user has the required role(s) - * @param {string[]} requiredRoles - Array of required roles - * @returns {function} Express middleware function + * Middleware zur Überprüfung der Benutzerrolle + * @param {string[]} requiredRoles - Die erforderlichen Rollen + * @returns {function} Express Middleware Funktion */ -export const requireRole = (requiredRoles) => { +exports.requireRole = (requiredRoles) => { return (req, res, next) => { - // Get the user's role from the JWT token (assuming it's in req.user.role) - const userRole = req.user?.role; - - // If no user role is found, deny access - if (!userRole) { - return res.status(401).json({ error: 'Unauthorized' }); + // Wenn kein Benutzer authentifiziert ist + if (!req.user) { + return res.status(401).json({ error: 'Authentication required' }); } - - // Check if the user has at least one of the required roles - if (requiredRoles.includes(userRole)) { - next(); // User has the required role, proceed to the next middleware/route - } else { - return res.status(403).json({ error: 'Forbidden' }); + + // Überprüfe, ob der Benutzer eine der erforderlichen Rollen hat + const hasRequiredRole = requiredRoles.some(role => req.user.role.includes(role)); + + if (!hasRequiredRole) { + return res.status(403).json({ error: 'Insufficient permissions' }); } + + next(); }; +}; + +/** + * Middleware zur Überprüfung, ob der Benutzer Admin ist + * @param {Object} req - Express Request Objekt + * @param {Object} res - Express Response Objekt + * @param {function} next - Nächste Middleware Funktion + */ +exports.requireAdmin = (req, res, next) => { + exports.requireRole(['admin'])(req, res, next); +}; + +/** + * Middleware zur Überprüfung, ob der Benutzer Moderator ist + * @param {Object} req - Express Request Objekt + * @param {Object} res - Express Response Objekt + * @param {function} next - Nächste Middleware Funktion + */ +exports.requireModerator = (req, res, next) => { + exports.requireRole(['moderator', 'admin'])(req, res, next); }; \ No newline at end of file diff --git a/backend/routes/roles.routes.js b/backend/routes/roles.routes.js new file mode 100644 index 0000000..30131fa --- /dev/null +++ b/backend/routes/roles.routes.js @@ -0,0 +1,96 @@ +const express = require('express'); +const router = express.Router(); +const { requireAdmin } = require('../middleware/role.middleware'); +const { getUserRoles, updateUserRoles, deleteUserRoles } = require('../controllers/roles.controller'); + +/** + * @swagger + * /api/users/{userId}/roles: + * get: + * summary: Liefert die Rollen eines Benutzers + * tags: [Roles] + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: string + * description: Die ID des Benutzers + * responses: + * 200: + * description: Die Rollen des Benutzers + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * 404: + * description: Benutzer nicht gefunden + * 500: + * description: Interner Serverfehler + */ +router.get('/:userId/roles', getUserRoles); + +/** + * @swagger + * /api/users/{userId}/roles: + * put: + * summary: Ändert die Rollen eines Benutzers + * tags: [Roles] + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: string + * description: Die ID des Benutzers + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * description: Die neuen Rollen des Benutzers + * responses: + * 200: + * description: Rollen erfolgreich aktualisiert + * 400: + * description: Ungültige Rollen + * 403: + * description: Keine Berechtigung + * 404: + * description: Benutzer nicht gefunden + * 500: + * description: Interner Serverfehler + */ +router.put('/:userId/roles', requireAdmin, updateUserRoles); + +/** + * @swagger + * /api/users/{userId}/roles: + * delete: + * summary: Entfernt alle Rollen eines Benutzers + * tags: [Roles] + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: string + * description: Die ID des Benutzers + * responses: + * 200: + * description: Rollen erfolgreich entfernt + * 403: + * description: Keine Berechtigung + * 404: + * description: Benutzer nicht gefunden + * 500: + * description: Interner Serverfehler + */ +router.delete('/:userId/roles', requireAdmin, deleteUserRoles); + +module.exports = router; \ No newline at end of file diff --git a/test/roles.test.js b/test/roles.test.js new file mode 100644 index 0000000..c02ae56 --- /dev/null +++ b/test/roles.test.js @@ -0,0 +1,105 @@ +const request = require('supertest'); +const app = require('../backend/app'); +const { getUserById, updateUser } = require('../backend/services/user.service'); +const { logAudit } = require('../backend/services/audit.service'); + +// Mock die Dienste +jest.mock('../backend/services/user.service'); +jest.mock('../backend/services/audit.service'); + +describe('Roles API', () => { + beforeEach(() => { + // Reset mocks before each test + jest.clearAllMocks(); + }); + + describe('GET /api/users/:userId/roles', () => { + it('should return user roles', async () => { + const mockUser = { id: '1', roles: ['user', 'moderator'] }; + getUserById.mockResolvedValue(mockUser); + + const response = await request(app) + .get('/api/users/1/roles') + .expect(200); + + expect(response.body).toEqual(['user', 'moderator']); + expect(getUserById).toHaveBeenCalledWith('1'); + }); + + it('should return 404 if user not found', async () => { + getUserById.mockResolvedValue(null); + + await request(app) + .get('/api/users/999/roles') + .expect(404); + }); + }); + + describe('PUT /api/users/:userId/roles', () => { + it('should update user roles with admin permission', async () => { + const mockUser = { id: '1', roles: ['user'] }; + getUserById.mockResolvedValue(mockUser); + updateUser.mockResolvedValue(true); + logAudit.mockResolvedValue(true); + + const response = await request(app) + .put('/api/users/1/roles') + .set('Authorization', 'Bearer admin-token') + .send({ roles: ['user', 'admin'] }) + .expect(200); + + expect(response.body).toEqual({ message: 'Roles updated successfully' }); + expect(getUserById).toHaveBeenCalledWith('1'); + expect(updateUser).toHaveBeenCalledWith('1', { roles: ['user', 'admin'] }); + expect(logAudit).toHaveBeenCalled(); + }); + + it('should return 400 if roles is not an array', async () => { + await request(app) + .put('/api/users/1/roles') + .set('Authorization', 'Bearer admin-token') + .send({ roles: 'user' }) + .expect(400); + }); + + it('should return 400 if role is invalid', async () => { + await request(app) + .put('/api/users/1/roles') + .set('Authorization', 'Bearer admin-token') + .send({ roles: ['invalid-role'] }) + .expect(400); + }); + + it('should return 403 if not authorized', async () => { + await request(app) + .put('/api/users/1/roles') + .send({ roles: ['user'] }) + .expect(403); + }); + }); + + describe('DELETE /api/users/:userId/roles', () => { + it('should delete user roles with admin permission', async () => { + const mockUser = { id: '1', roles: ['user', 'moderator'] }; + getUserById.mockResolvedValue(mockUser); + updateUser.mockResolvedValue(true); + logAudit.mockResolvedValue(true); + + const response = await request(app) + .delete('/api/users/1/roles') + .set('Authorization', 'Bearer admin-token') + .expect(200); + + expect(response.body).toEqual({ message: 'Roles deleted successfully' }); + expect(getUserById).toHaveBeenCalledWith('1'); + expect(updateUser).toHaveBeenCalledWith('1', { roles: [] }); + expect(logAudit).toHaveBeenCalled(); + }); + + it('should return 403 if not authorized', async () => { + await request(app) + .delete('/api/users/1/roles') + .expect(403); + }); + }); +}); \ No newline at end of file