diff --git a/backend/src/routes/consent.js b/backend/src/routes/consent.js new file mode 100644 index 0000000..8483ea6 --- /dev/null +++ b/backend/src/routes/consent.js @@ -0,0 +1,304 @@ +/** + * Consent Management API Routes + * + * Handles social media platform listings and consent management + */ + +const express = require('express'); +const router = express.Router(); +const GroupRepository = require('../repositories/GroupRepository'); +const SocialMediaRepository = require('../repositories/SocialMediaRepository'); +const dbManager = require('../database/DatabaseManager'); + +// ============================================================================ +// Social Media Platforms +// ============================================================================ + +/** + * GET /api/social-media/platforms + * Liste aller aktiven Social Media Plattformen + */ +router.get('/social-media/platforms', async (req, res) => { + try { + const socialMediaRepo = new SocialMediaRepository(dbManager); + const platforms = await socialMediaRepo.getActivePlatforms(); + + res.json(platforms); + } catch (error) { + console.error('Error fetching platforms:', error); + res.status(500).json({ + error: 'Failed to fetch social media platforms', + message: error.message + }); + } +}); + +// ============================================================================ +// Group Consents +// ============================================================================ + +/** + * POST /api/groups/:groupId/consents + * Speichere oder aktualisiere Consents für eine Gruppe + * + * Body: { + * workshopConsent: boolean, + * socialMediaConsents: [{ platformId: number, consented: boolean }] + * } + */ +router.post('/groups/:groupId/consents', async (req, res) => { + try { + const { groupId } = req.params; + const { workshopConsent, socialMediaConsents } = req.body; + + // Validierung + if (typeof workshopConsent !== 'boolean') { + return res.status(400).json({ + error: 'Invalid request', + message: 'workshopConsent must be a boolean' + }); + } + + if (!Array.isArray(socialMediaConsents)) { + return res.status(400).json({ + error: 'Invalid request', + message: 'socialMediaConsents must be an array' + }); + } + + // Prüfe ob Gruppe existiert + const group = await GroupRepository.getGroupById(groupId); + if (!group) { + return res.status(404).json({ + error: 'Group not found', + message: `No group found with ID: ${groupId}` + }); + } + + // Aktualisiere Consents + await GroupRepository.updateConsents( + groupId, + workshopConsent, + socialMediaConsents + ); + + res.json({ + success: true, + message: 'Consents updated successfully', + groupId + }); + + } catch (error) { + console.error('Error updating consents:', error); + res.status(500).json({ + error: 'Failed to update consents', + message: error.message + }); + } +}); + +/** + * GET /api/groups/:groupId/consents + * Lade alle Consents für eine Gruppe + */ +router.get('/groups/:groupId/consents', async (req, res) => { + try { + const { groupId } = req.params; + + // Hole Gruppe mit Consents + const group = await GroupRepository.getGroupWithConsents(groupId); + + if (!group) { + return res.status(404).json({ + error: 'Group not found', + message: `No group found with ID: ${groupId}` + }); + } + + // Formatiere Response + const response = { + groupId: group.group_id, + workshopConsent: group.display_in_workshop === 1, + consentTimestamp: group.consent_timestamp, + socialMediaConsents: group.consents.map(c => ({ + platformId: c.platform_id, + platformName: c.platform_name, + displayName: c.display_name, + iconName: c.icon_name, + consented: c.consented === 1, + consentTimestamp: c.consent_timestamp, + revoked: c.revoked === 1, + revokedTimestamp: c.revoked_timestamp + })) + }; + + res.json(response); + + } catch (error) { + console.error('Error fetching consents:', error); + res.status(500).json({ + error: 'Failed to fetch consents', + message: error.message + }); + } +}); + +// ============================================================================ +// Admin - Filtering & Export +// ============================================================================ + +/** + * GET /api/admin/groups/by-consent + * Filtere Gruppen nach Consent-Status + * + * Query params: + * - displayInWorkshop: boolean + * - platformId: number + * - platformConsent: boolean + */ +router.get('/admin/groups/by-consent', async (req, res) => { + try { + const filters = {}; + + // Parse query parameters + if (req.query.displayInWorkshop !== undefined) { + filters.displayInWorkshop = req.query.displayInWorkshop === 'true'; + } + + if (req.query.platformId !== undefined) { + filters.platformId = parseInt(req.query.platformId, 10); + + if (isNaN(filters.platformId)) { + return res.status(400).json({ + error: 'Invalid platformId', + message: 'platformId must be a number' + }); + } + } + + if (req.query.platformConsent !== undefined) { + filters.platformConsent = req.query.platformConsent === 'true'; + } + + // Hole gefilterte Gruppen + const groups = await GroupRepository.getGroupsByConsentStatus(filters); + + res.json({ + count: groups.length, + filters, + groups + }); + + } catch (error) { + console.error('Error filtering groups by consent:', error); + res.status(500).json({ + error: 'Failed to filter groups', + message: error.message + }); + } +}); + +/** + * GET /api/admin/consents/export + * Export Consent-Daten für rechtliche Dokumentation + * + * Query params: + * - format: 'json' | 'csv' (default: json) + * - year: number (optional filter) + * - approved: boolean (optional filter) + */ +router.get('/admin/consents/export', async (req, res) => { + try { + const format = req.query.format || 'json'; + const filters = {}; + + // Parse filters + if (req.query.year) { + filters.year = parseInt(req.query.year, 10); + } + + if (req.query.approved !== undefined) { + filters.approved = req.query.approved === 'true'; + } + + // Export Daten holen + const exportData = await GroupRepository.exportConsentData(filters); + + // Format: JSON + if (format === 'json') { + res.json({ + exportDate: new Date().toISOString(), + filters, + count: exportData.length, + data: exportData + }); + return; + } + + // Format: CSV + if (format === 'csv') { + // CSV Header + let csv = 'group_id,year,title,name,upload_date,workshop_consent,consent_timestamp,approved'; + + // Sammle alle möglichen Plattformen + const allPlatforms = new Set(); + exportData.forEach(group => { + group.socialMediaConsents.forEach(consent => { + allPlatforms.add(consent.platform_name); + }); + }); + + // Füge Platform-Spalten hinzu + const platformNames = Array.from(allPlatforms).sort(); + platformNames.forEach(platform => { + csv += `,${platform}`; + }); + csv += '\n'; + + // CSV Daten + exportData.forEach(group => { + const row = [ + group.group_id, + group.year, + `"${(group.title || '').replace(/"/g, '""')}"`, + `"${(group.name || '').replace(/"/g, '""')}"`, + group.upload_date, + group.display_in_workshop === 1 ? 'true' : 'false', + group.consent_timestamp || '', + group.approved === 1 ? 'true' : 'false' + ]; + + // Platform-Consents + const consentMap = {}; + group.socialMediaConsents.forEach(consent => { + consentMap[consent.platform_name] = consent.consented === 1; + }); + + platformNames.forEach(platform => { + row.push(consentMap[platform] ? 'true' : 'false'); + }); + + csv += row.join(',') + '\n'; + }); + + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', `attachment; filename=consent-export-${Date.now()}.csv`); + res.send(csv); + return; + } + + res.status(400).json({ + error: 'Invalid format', + message: 'Format must be "json" or "csv"' + }); + + } catch (error) { + console.error('Error exporting consent data:', error); + res.status(500).json({ + error: 'Failed to export consent data', + message: error.message + }); + } +}); + +module.exports = router; diff --git a/backend/src/routes/index.js b/backend/src/routes/index.js index 5ac95b9..d7a3867 100644 --- a/backend/src/routes/index.js +++ b/backend/src/routes/index.js @@ -5,9 +5,10 @@ const groupsRouter = require('./groups'); const migrationRouter = require('./migration'); const reorderRouter = require('./reorder'); const adminRouter = require('./admin'); +const consentRouter = require('./consent'); const renderRoutes = (app) => { - [uploadRouter, downloadRouter, batchUploadRouter, groupsRouter, migrationRouter].forEach(router => app.use('/', router)); + [uploadRouter, downloadRouter, batchUploadRouter, groupsRouter, migrationRouter, consentRouter].forEach(router => app.use('/', router)); app.use('/groups', reorderRouter); app.use('/api/admin', adminRouter); };