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
This commit is contained in:
Matthias Lotz 2025-11-09 21:01:16 +01:00
parent 8dc5a03584
commit ff2ea310ed
2 changed files with 621 additions and 0 deletions

View File

@ -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<string>} 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<Object>} 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<void>}
*/
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<Array>} 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<Array>} 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<string>} 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<Object|null>} 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<Array>} 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<Array>} Consents
*/
async getSocialMediaConsentsForGroup(groupId) {
const SocialMediaRepository = require('./SocialMediaRepository');
const socialMediaRepo = new SocialMediaRepository(dbManager);
return await socialMediaRepo.getConsentsForGroup(groupId);
}
} }
module.exports = new GroupRepository(); module.exports = new GroupRepository();

View File

@ -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>} 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>} 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<number>} 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<void>}
*/
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<void>}
*/
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<void>}
*/
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>} 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>} 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<void>}
*/
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<void>}
*/
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>} 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<boolean>} 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<void>}
*/
async deleteConsentsForGroup(groupId) {
const query = `
DELETE FROM group_social_media_consents
WHERE group_id = ?
`;
await this.db.run(query, [groupId]);
}
}
module.exports = SocialMediaRepository;