From ff2ea310edd266ba6d16ae249abb1592df256317 Mon Sep 17 00:00:00 2001 From: "matthias.lotz" Date: Sun, 9 Nov 2025 21:01:16 +0100 Subject: [PATCH] feat(repositories): Add SocialMediaRepository and extend GroupRepository - Create new SocialMediaRepository for platform and consent management - getAllPlatforms(), getActivePlatforms() - createPlatform(), updatePlatform(), togglePlatformStatus() - saveConsents(), getConsentsForGroup(), getGroupIdsByConsentStatus() - revokeConsent(), restoreConsent(), hasActiveConsent() - Extend GroupRepository with consent management methods - createGroupWithConsent() - create group with workshop & social media consents - getGroupWithConsents() - retrieve group with all consent data - updateConsents() - update consent preferences - getGroupsByConsentStatus() - filter groups by consent status - exportConsentData() - export for legal documentation - generateManagementToken(), getGroupByManagementToken() (Phase 2) - Both repositories work together seamlessly via transactions --- backend/src/repositories/GroupRepository.js | 282 +++++++++++++++ .../src/repositories/SocialMediaRepository.js | 339 ++++++++++++++++++ 2 files changed, 621 insertions(+) create mode 100644 backend/src/repositories/SocialMediaRepository.js diff --git a/backend/src/repositories/GroupRepository.js b/backend/src/repositories/GroupRepository.js index bfecd5b..ce65161 100644 --- a/backend/src/repositories/GroupRepository.js +++ b/backend/src/repositories/GroupRepository.js @@ -505,6 +505,288 @@ class GroupRepository { }; }); } + + // ============================================================================ + // Consent Management Methods + // ============================================================================ + + /** + * Erstelle neue Gruppe mit Consent-Daten + * @param {Object} groupData - Standard Gruppendaten + * @param {boolean} workshopConsent - Werkstatt-Anzeige Zustimmung + * @param {Array} socialMediaConsents - Array von {platformId, consented} + * @returns {Promise} groupId der erstellten Gruppe + */ + async createGroupWithConsent(groupData, workshopConsent, socialMediaConsents = []) { + const SocialMediaRepository = require('./SocialMediaRepository'); + const socialMediaRepo = new SocialMediaRepository(dbManager); + + return await dbManager.transaction(async (db) => { + const consentTimestamp = new Date().toISOString(); + + // Füge Gruppe mit Consent-Feldern hinzu + await db.run(` + INSERT INTO groups ( + group_id, year, title, description, name, upload_date, approved, + display_in_workshop, consent_timestamp + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `, [ + groupData.groupId, + groupData.year, + groupData.title, + groupData.description || null, + groupData.name || null, + groupData.uploadDate, + groupData.approved || false, + workshopConsent ? 1 : 0, + consentTimestamp + ]); + + // Füge Bilder hinzu + if (groupData.images && groupData.images.length > 0) { + for (const image of groupData.images) { + await db.run(` + INSERT INTO images ( + group_id, file_name, original_name, file_path, upload_order, + file_size, mime_type, preview_path, image_description + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `, [ + groupData.groupId, + image.fileName, + image.originalName, + image.filePath, + image.uploadOrder, + image.fileSize || null, + image.mimeType || null, + image.previewPath || null, + image.imageDescription || null + ]); + } + } + + // Speichere Social Media Consents + if (socialMediaConsents && socialMediaConsents.length > 0) { + await socialMediaRepo.saveConsents( + groupData.groupId, + socialMediaConsents, + consentTimestamp + ); + } + + return groupData.groupId; + }); + } + + /** + * Hole Gruppe mit allen Consent-Informationen + * @param {string} groupId - ID der Gruppe + * @returns {Promise} Gruppe mit Bildern und Consents + */ + async getGroupWithConsents(groupId) { + const SocialMediaRepository = require('./SocialMediaRepository'); + const socialMediaRepo = new SocialMediaRepository(dbManager); + + // Hole Standard-Gruppendaten + const group = await this.getGroupById(groupId); + + if (!group) { + return null; + } + + // Füge Consent-Daten hinzu + group.consents = await socialMediaRepo.getConsentsForGroup(groupId); + + return group; + } + + /** + * Aktualisiere Consents für eine bestehende Gruppe + * @param {string} groupId - ID der Gruppe + * @param {boolean} workshopConsent - Neue Werkstatt-Consent + * @param {Array} socialMediaConsents - Neue Social Media Consents + * @returns {Promise} + */ + async updateConsents(groupId, workshopConsent, socialMediaConsents = []) { + const SocialMediaRepository = require('./SocialMediaRepository'); + const socialMediaRepo = new SocialMediaRepository(dbManager); + + return await dbManager.transaction(async (db) => { + const consentTimestamp = new Date().toISOString(); + + // Aktualisiere Werkstatt-Consent + await db.run(` + UPDATE groups + SET display_in_workshop = ?, + consent_timestamp = ? + WHERE group_id = ? + `, [workshopConsent ? 1 : 0, consentTimestamp, groupId]); + + // Lösche alte Social Media Consents + await socialMediaRepo.deleteConsentsForGroup(groupId); + + // Speichere neue Consents + if (socialMediaConsents && socialMediaConsents.length > 0) { + await socialMediaRepo.saveConsents( + groupId, + socialMediaConsents, + consentTimestamp + ); + } + }); + } + + /** + * Filtere Gruppen nach Consent-Status + * @param {Object} filters - Filter-Optionen + * @param {boolean} filters.displayInWorkshop - Filter nach Werkstatt-Consent + * @param {number} filters.platformId - Filter nach Plattform-ID + * @param {boolean} filters.platformConsent - Filter nach Platform-Consent-Status + * @returns {Promise} Gefilterte Gruppen + */ + async getGroupsByConsentStatus(filters = {}) { + let query = ` + SELECT DISTINCT g.* + FROM groups g + `; + const params = []; + const conditions = []; + + // Filter nach Werkstatt-Consent + if (filters.displayInWorkshop !== undefined) { + conditions.push('g.display_in_workshop = ?'); + params.push(filters.displayInWorkshop ? 1 : 0); + } + + // Filter nach Social Media Platform + if (filters.platformId !== undefined) { + query += ` + LEFT JOIN group_social_media_consents c + ON g.group_id = c.group_id AND c.platform_id = ? + `; + params.push(filters.platformId); + + if (filters.platformConsent !== undefined) { + conditions.push('c.consented = ?'); + params.push(filters.platformConsent ? 1 : 0); + conditions.push('(c.revoked IS NULL OR c.revoked = 0)'); + } + } + + if (conditions.length > 0) { + query += ' WHERE ' + conditions.join(' AND '); + } + + query += ' ORDER BY g.upload_date DESC'; + + return await dbManager.all(query, params); + } + + /** + * Exportiere Consent-Daten für rechtliche Dokumentation + * @param {Object} filters - Optional: Filter-Kriterien + * @returns {Promise} Export-Daten mit allen Consent-Informationen + */ + async exportConsentData(filters = {}) { + let query = ` + SELECT + g.group_id, + g.year, + g.title, + g.name, + g.upload_date, + g.display_in_workshop, + g.consent_timestamp, + g.approved + FROM groups g + WHERE 1=1 + `; + const params = []; + + if (filters.year) { + query += ' AND g.year = ?'; + params.push(filters.year); + } + + if (filters.approved !== undefined) { + query += ' AND g.approved = ?'; + params.push(filters.approved ? 1 : 0); + } + + query += ' ORDER BY g.upload_date DESC'; + + const groups = await dbManager.all(query, params); + + // Lade Social Media Consents für jede Gruppe + const SocialMediaRepository = require('./SocialMediaRepository'); + const socialMediaRepo = new SocialMediaRepository(dbManager); + + for (const group of groups) { + group.socialMediaConsents = await socialMediaRepo.getConsentsForGroup(group.group_id); + } + + return groups; + } + + /** + * Generiere Management-Token für Gruppe (Phase 2) + * @param {string} groupId - ID der Gruppe + * @returns {Promise} Generierter UUID Token + */ + async generateManagementToken(groupId) { + const crypto = require('crypto'); + const token = crypto.randomUUID(); + + await dbManager.run(` + UPDATE groups + SET management_token = ? + WHERE group_id = ? + `, [token, groupId]); + + return token; + } + + /** + * Hole Gruppe über Management-Token (Phase 2) + * @param {string} token - Management Token + * @returns {Promise} Gruppe mit allen Daten oder null + */ + async getGroupByManagementToken(token) { + const group = await dbManager.get(` + SELECT * FROM groups WHERE management_token = ? + `, [token]); + + if (!group) { + return null; + } + + // Lade Bilder und Consents + return await this.getGroupWithConsents(group.group_id); + } + + /** + * Hole aktive Social Media Plattformen + * Convenience-Methode für Frontend + * @returns {Promise} Aktive Plattformen + */ + async getActiveSocialMediaPlatforms() { + const SocialMediaRepository = require('./SocialMediaRepository'); + const socialMediaRepo = new SocialMediaRepository(dbManager); + return await socialMediaRepo.getActivePlatforms(); + } + + /** + * Hole Social Media Consents für Gruppe + * Convenience-Methode + * @param {string} groupId - ID der Gruppe + * @returns {Promise} Consents + */ + async getSocialMediaConsentsForGroup(groupId) { + const SocialMediaRepository = require('./SocialMediaRepository'); + const socialMediaRepo = new SocialMediaRepository(dbManager); + return await socialMediaRepo.getConsentsForGroup(groupId); + } } module.exports = new GroupRepository(); \ No newline at end of file diff --git a/backend/src/repositories/SocialMediaRepository.js b/backend/src/repositories/SocialMediaRepository.js new file mode 100644 index 0000000..762f0c5 --- /dev/null +++ b/backend/src/repositories/SocialMediaRepository.js @@ -0,0 +1,339 @@ +/** + * SocialMediaRepository + * + * Repository für Social Media Platform und Consent Management + * Verwaltet social_media_platforms und group_social_media_consents Tabellen + */ + +class SocialMediaRepository { + constructor(dbManager) { + this.db = dbManager; + } + + // ============================================================================ + // Platform Management + // ============================================================================ + + /** + * Lade alle Social Media Plattformen (aktiv und inaktiv) + * @returns {Promise} Array von Platform-Objekten + */ + async getAllPlatforms() { + const query = ` + SELECT + id, + platform_name, + display_name, + icon_name, + is_active, + sort_order, + created_at + FROM social_media_platforms + ORDER BY sort_order ASC, display_name ASC + `; + + return await this.db.all(query); + } + + /** + * Lade nur aktive Social Media Plattformen + * @returns {Promise} Array von aktiven Platform-Objekten + */ + async getActivePlatforms() { + const query = ` + SELECT + id, + platform_name, + display_name, + icon_name, + sort_order + FROM social_media_platforms + WHERE is_active = 1 + ORDER BY sort_order ASC, display_name ASC + `; + + return await this.db.all(query); + } + + /** + * Erstelle eine neue Social Media Plattform + * @param {Object} platformData - Platform-Daten + * @param {string} platformData.platform_name - Interner Name (z.B. 'facebook') + * @param {string} platformData.display_name - Anzeigename (z.B. 'Facebook') + * @param {string} platformData.icon_name - Material-UI Icon Name + * @param {number} platformData.sort_order - Sortierreihenfolge + * @returns {Promise} ID der neu erstellten Plattform + */ + async createPlatform(platformData) { + const query = ` + INSERT INTO social_media_platforms + (platform_name, display_name, icon_name, sort_order, is_active) + VALUES (?, ?, ?, ?, 1) + `; + + const result = await this.db.run( + query, + [ + platformData.platform_name, + platformData.display_name, + platformData.icon_name || null, + platformData.sort_order || 0 + ] + ); + + return result.lastID; + } + + /** + * Aktualisiere eine bestehende Plattform + * @param {number} platformId - ID der Plattform + * @param {Object} platformData - Zu aktualisierende Daten + * @returns {Promise} + */ + async updatePlatform(platformId, platformData) { + const updates = []; + const values = []; + + if (platformData.display_name !== undefined) { + updates.push('display_name = ?'); + values.push(platformData.display_name); + } + if (platformData.icon_name !== undefined) { + updates.push('icon_name = ?'); + values.push(platformData.icon_name); + } + if (platformData.sort_order !== undefined) { + updates.push('sort_order = ?'); + values.push(platformData.sort_order); + } + + if (updates.length === 0) { + return; // Nichts zu aktualisieren + } + + values.push(platformId); + + const query = ` + UPDATE social_media_platforms + SET ${updates.join(', ')} + WHERE id = ? + `; + + await this.db.run(query, values); + } + + /** + * Aktiviere oder deaktiviere eine Plattform + * @param {number} platformId - ID der Plattform + * @param {boolean} isActive - Aktiv-Status + * @returns {Promise} + */ + async togglePlatformStatus(platformId, isActive) { + const query = ` + UPDATE social_media_platforms + SET is_active = ? + WHERE id = ? + `; + + await this.db.run(query, [isActive ? 1 : 0, platformId]); + } + + // ============================================================================ + // Consent Management + // ============================================================================ + + /** + * Speichere Consents für eine Gruppe + * @param {string} groupId - ID der Gruppe + * @param {Array} consentsArray - Array von {platformId, consented} Objekten + * @param {string} consentTimestamp - ISO-Timestamp der Zustimmung + * @returns {Promise} + */ + async saveConsents(groupId, consentsArray, consentTimestamp) { + if (!Array.isArray(consentsArray) || consentsArray.length === 0) { + return; // Keine Consents zu speichern + } + + const query = ` + INSERT INTO group_social_media_consents + (group_id, platform_id, consented, consent_timestamp) + VALUES (?, ?, ?, ?) + `; + + // Speichere jeden Consent einzeln + for (const consent of consentsArray) { + await this.db.run( + query, + [ + groupId, + consent.platformId, + consent.consented ? 1 : 0, + consentTimestamp + ] + ); + } + } + + /** + * Lade alle Consents für eine Gruppe + * @param {string} groupId - ID der Gruppe + * @returns {Promise} Array von Consent-Objekten mit Platform-Info + */ + async getConsentsForGroup(groupId) { + const query = ` + SELECT + c.id, + c.group_id, + c.platform_id, + c.consented, + c.consent_timestamp, + c.revoked, + c.revoked_timestamp, + p.platform_name, + p.display_name, + p.icon_name + FROM group_social_media_consents c + JOIN social_media_platforms p ON c.platform_id = p.id + WHERE c.group_id = ? + ORDER BY p.sort_order ASC + `; + + return await this.db.all(query, [groupId]); + } + + /** + * Lade Gruppen-IDs nach Consent-Status filtern + * @param {Object} filters - Filter-Optionen + * @param {number} filters.platformId - Optional: Filter nach Plattform-ID + * @param {boolean} filters.consented - Optional: Filter nach Consent-Status + * @returns {Promise} Array von Gruppen-IDs + */ + async getGroupIdsByConsentStatus(filters = {}) { + let query = ` + SELECT DISTINCT c.group_id + FROM group_social_media_consents c + WHERE 1=1 + `; + const params = []; + + if (filters.platformId !== undefined) { + query += ' AND c.platform_id = ?'; + params.push(filters.platformId); + } + + if (filters.consented !== undefined) { + query += ' AND c.consented = ?'; + params.push(filters.consented ? 1 : 0); + } + + if (filters.revoked !== undefined) { + query += ' AND c.revoked = ?'; + params.push(filters.revoked ? 1 : 0); + } + + const results = await this.db.all(query, params); + return results.map(row => row.group_id); + } + + /** + * Widerrufe einen Consent (Phase 2) + * @param {string} groupId - ID der Gruppe + * @param {number} platformId - ID der Plattform + * @returns {Promise} + */ + async revokeConsent(groupId, platformId) { + const query = ` + UPDATE group_social_media_consents + SET + revoked = 1, + revoked_timestamp = CURRENT_TIMESTAMP + WHERE group_id = ? AND platform_id = ? + `; + + await this.db.run(query, [groupId, platformId]); + } + + /** + * Stelle einen widerrufenen Consent wieder her (Phase 2) + * @param {string} groupId - ID der Gruppe + * @param {number} platformId - ID der Plattform + * @returns {Promise} + */ + async restoreConsent(groupId, platformId) { + const query = ` + UPDATE group_social_media_consents + SET + revoked = 0, + revoked_timestamp = NULL + WHERE group_id = ? AND platform_id = ? + `; + + await this.db.run(query, [groupId, platformId]); + } + + /** + * Lade Consent-Historie für eine Gruppe (Phase 2) + * @param {string} groupId - ID der Gruppe + * @returns {Promise} Array von Consent-Änderungen + */ + async getConsentHistory(groupId) { + const query = ` + SELECT + c.id, + c.group_id, + c.platform_id, + c.consented, + c.consent_timestamp, + c.revoked, + c.revoked_timestamp, + c.created_at, + c.updated_at, + p.platform_name, + p.display_name + FROM group_social_media_consents c + JOIN social_media_platforms p ON c.platform_id = p.id + WHERE c.group_id = ? + ORDER BY c.updated_at DESC + `; + + return await this.db.all(query, [groupId]); + } + + /** + * Prüfe ob eine Gruppe Consent für eine bestimmte Plattform hat + * @param {string} groupId - ID der Gruppe + * @param {number} platformId - ID der Plattform + * @returns {Promise} true wenn Consent erteilt und nicht widerrufen + */ + async hasActiveConsent(groupId, platformId) { + const query = ` + SELECT consented, revoked + FROM group_social_media_consents + WHERE group_id = ? AND platform_id = ? + `; + + const result = await this.db.get(query, [groupId, platformId]); + + if (!result) { + return false; + } + + return result.consented === 1 && result.revoked === 0; + } + + /** + * Lösche alle Consents für eine Gruppe (CASCADE durch DB) + * @param {string} groupId - ID der Gruppe + * @returns {Promise} + */ + async deleteConsentsForGroup(groupId) { + const query = ` + DELETE FROM group_social_media_consents + WHERE group_id = ? + `; + + await this.db.run(query, [groupId]); + } +} + +module.exports = SocialMediaRepository;