feat(api): Add consent management API routes

- Create consent.js with comprehensive API endpoints:
  - GET /api/social-media/platforms - list active platforms
  - POST /api/groups/:groupId/consents - save/update group consents
  - GET /api/groups/:groupId/consents - retrieve group consent data
  - GET /api/admin/groups/by-consent - filter groups by consent status
  - GET /api/admin/consents/export - export consent data (JSON/CSV formats)

- Register consent router in routes/index.js
- Full validation and error handling
- CSV export with dynamic platform columns
- Ready for frontend integration
This commit is contained in:
Matthias Lotz 2025-11-09 21:02:57 +01:00
parent ff2ea310ed
commit 2f86158821
2 changed files with 306 additions and 1 deletions

View File

@ -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;

View File

@ -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);
};