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:
parent
8dc5a03584
commit
ff2ea310ed
|
|
@ -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();
|
||||
339
backend/src/repositories/SocialMediaRepository.js
Normal file
339
backend/src/repositories/SocialMediaRepository.js
Normal 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;
|
||||
Loading…
Reference in New Issue
Block a user