Fix: Admin deletion log, CSV export revoked consents, consent filter UI

Backend Fixes:
- Admin deletions now create deletion_log entries (admin_moderation_deletion)
- Static mount for /previews added to serve preview images
- Admin groups endpoint supports consent filter parameter

Frontend Improvements:
- Replaced consent dropdown with checkbox UI (Workshop, Facebook, Instagram, TikTok)
- Checkboxes use OR logic for filtering
- Revoked consents excluded from filter counts
- Updated ModerationGroupsPage to send consents array to backend

Infrastructure:
- Simplified nginx.conf (proxy /api/* to backend, all else to frontend)
- Fixed docker-compose port mapping (5001:5000)

Tests: 11/11 passed 
This commit is contained in:
Matthias Lotz 2025-11-22 11:13:10 +01:00
parent 7af14a162d
commit 98b3616dc4
9 changed files with 198 additions and 192 deletions

View File

@ -417,7 +417,7 @@ router.get('/groups', async (req, res) => {
} }
*/ */
try { try {
const { workshopOnly, platform } = req.query; const { workshopOnly, platform, consents } = req.query;
// Hole alle Gruppen mit vollständigen Infos (inkl. Bilder) // Hole alle Gruppen mit vollständigen Infos (inkl. Bilder)
let allGroups = await GroupRepository.getAllGroupsWithModerationInfo(); let allGroups = await GroupRepository.getAllGroupsWithModerationInfo();
@ -425,10 +425,10 @@ router.get('/groups', async (req, res) => {
// Füge Consent-Daten für jede Gruppe hinzu // Füge Consent-Daten für jede Gruppe hinzu
const groupsWithConsents = await Promise.all( const groupsWithConsents = await Promise.all(
allGroups.map(async (group) => { allGroups.map(async (group) => {
const consents = await GroupRepository.getSocialMediaConsentsForGroup(group.groupId); const consentData = await GroupRepository.getSocialMediaConsentsForGroup(group.groupId);
return { return {
...group, ...group,
socialMediaConsents: consents socialMediaConsents: consentData
}; };
}) })
); );
@ -436,7 +436,27 @@ router.get('/groups', async (req, res) => {
// Jetzt filtern wir basierend auf den Query-Parametern // Jetzt filtern wir basierend auf den Query-Parametern
let filteredGroups = groupsWithConsents; let filteredGroups = groupsWithConsents;
if (workshopOnly === 'true') { // Neuer Multi-Checkbox Filter
if (consents) {
const selectedConsents = consents.split(','); // z.B. ['workshop', 'facebook', 'instagram']
filteredGroups = groupsWithConsents.filter(group => {
// Gruppe muss mindestens einen der ausgewählten Consents haben
return selectedConsents.some(consentType => {
if (consentType === 'workshop') {
return group.display_in_workshop === 1 || group.display_in_workshop === true;
} else {
// Social Media Platform (facebook, instagram, tiktok)
return group.socialMediaConsents &&
group.socialMediaConsents.some(consent =>
consent.platform_name === consentType &&
(consent.consented === 1 || consent.consented === true) &&
(consent.revoked !== 1 && consent.revoked !== true)
);
}
});
});
} else if (workshopOnly === 'true') {
// Filter: Nur Gruppen MIT Werkstatt-Consent aber OHNE zugestimmte Social Media Consents // Filter: Nur Gruppen MIT Werkstatt-Consent aber OHNE zugestimmte Social Media Consents
filteredGroups = groupsWithConsents.filter(group => { filteredGroups = groupsWithConsents.filter(group => {
// Muss Werkstatt-Consent haben // Muss Werkstatt-Consent haben
@ -922,15 +942,32 @@ router.delete('/groups/:groupId', async (req, res) => {
try { try {
const { groupId } = req.params; const { groupId } = req.params;
const deleted = await GroupRepository.deleteGroup(groupId); // Get group data before deletion for logging
const groupData = await GroupRepository.getGroupById(groupId);
if (!deleted) { if (!groupData) {
return res.status(404).json({ return res.status(404).json({
error: 'Group not found', error: 'Group not found',
message: `Gruppe mit ID ${groupId} wurde nicht gefunden` message: `Gruppe mit ID ${groupId} wurde nicht gefunden`
}); });
} }
const imageCount = groupData.images ? groupData.images.length : 0;
const totalFileSize = groupData.images ? groupData.images.reduce((sum, img) => sum + (img.fileSize || 0), 0) : 0;
// Create deletion_log entry BEFORE deleting
await DeletionLogRepository.createDeletionEntry({
groupId: groupId,
year: groupData.year,
imageCount: imageCount,
uploadDate: groupData.uploadDate,
deletionReason: 'admin_moderation_deletion',
totalFileSize: totalFileSize
});
// Delete the group
await GroupRepository.deleteGroup(groupId);
res.json({ res.json({
success: true, success: true,
message: 'Gruppe erfolgreich gelöscht', message: 'Gruppe erfolgreich gelöscht',

View File

@ -403,7 +403,8 @@ router.get('/consents/export', async (req, res) => {
// Platform-Consents // Platform-Consents
const consentMap = {}; const consentMap = {};
group.socialMediaConsents.forEach(consent => { group.socialMediaConsents.forEach(consent => {
consentMap[consent.platform_name] = consent.consented === 1; // Consent ist nur dann aktiv wenn consented=1 UND nicht revoked
consentMap[consent.platform_name] = consent.consented === 1 && consent.revoked !== 1;
}); });
platformNames.forEach(platform => { platformNames.forEach(platform => {

View File

@ -45,6 +45,7 @@ class Server {
// Starte Express Server // Starte Express Server
initiateResources(this._app); initiateResources(this._app);
this._app.use('/upload', express.static( __dirname + '/upload')); this._app.use('/upload', express.static( __dirname + '/upload'));
this._app.use('/api/previews', express.static( __dirname + '/data/previews'));
// Mount Swagger UI in dev only when available // Mount Swagger UI in dev only when available
if (process.env.NODE_ENV !== 'production' && swaggerUi && swaggerDocument) { if (process.env.NODE_ENV !== 'production' && swaggerUi && swaggerDocument) {
@ -74,6 +75,7 @@ class Server {
await dbManager.initialize(); await dbManager.initialize();
initiateResources(this._app); initiateResources(this._app);
this._app.use('/upload', express.static( __dirname + '/upload')); this._app.use('/upload', express.static( __dirname + '/upload'));
this._app.use('/api/previews', express.static( __dirname + '/data/previews'));
if (process.env.NODE_ENV !== 'production' && swaggerUi && swaggerDocument) { if (process.env.NODE_ENV !== 'production' && swaggerUi && swaggerDocument) {
this._app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); this._app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));

View File

@ -26,6 +26,7 @@ services:
backend-dev: backend-dev:
container_name: image-uploader-backend-dev container_name: image-uploader-backend-dev
user: "1000:1000"
build: build:
context: ../../ context: ../../
dockerfile: docker/dev/backend/Dockerfile dockerfile: docker/dev/backend/Dockerfile

View File

@ -1,128 +1,34 @@
server { server {
listen 80; listen 80;
# Allow large uploads (50MB) # Allow large uploads (100MB for batch uploads)
client_max_body_size 50M; client_max_body_size 100M;
# API proxy to backend-dev service # ========================================
location /upload { # Backend API Routes (all under /api/)
# ========================================
# Basierend auf routeMappings.js:
# - /api/upload, /api/download, /api/groups (Public API)
# - /api/manage/* (Management Portal, Token-basiert)
# - /api/admin/* (Admin/Moderation, Bearer Token)
# - /api/system/migration/* (System API)
location /api/ {
proxy_pass http://backend-dev:5000; proxy_pass http://backend-dev:5000;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
# Allow large uploads for API too # Large uploads für Batch-Upload
client_max_body_size 50M;
}
# API routes for new multi-upload features
location /api/upload {
proxy_pass http://backend-dev:5000/upload;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Allow large uploads for batch upload
client_max_body_size 100M; client_max_body_size 100M;
} }
# API - Download original images # ========================================
location /api/download { # Frontend Routes (React Dev Server)
proxy_pass http://backend-dev:5000/download; # ========================================
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API - Preview/thumbnail images (optimized for gallery views) # Protected route - Moderation (HTTP Basic Auth)
location /api/previews {
proxy_pass http://backend-dev:5000/previews;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API - Groups (NO PASSWORD PROTECTION)
location /api/groups {
proxy_pass http://backend-dev:5000/groups;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API - Social Media Consent Management (NO PASSWORD PROTECTION)
location /api/social-media {
proxy_pass http://backend-dev:5000/api/social-media;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API - Management Portal (NO PASSWORD PROTECTION - Token-based auth)
location /api/manage {
proxy_pass http://backend-dev:5000/api/manage;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Admin API routes (NO password protection - protected by /moderation page access)
location /api/admin {
proxy_pass http://backend-dev:5000/api/admin;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Protected API - Moderation API routes (password protected) - must come before /groups
location /moderation/groups {
auth_basic "Restricted Area - Moderation API";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://backend-dev:5000/moderation/groups;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API - Groups API routes (NO PASSWORD PROTECTION)
location ~ ^/groups/[a-zA-Z0-9_-]+(/.*)?$ {
proxy_pass http://backend-dev:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /download {
proxy_pass http://backend-dev:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Frontend page - Groups overview (NO PASSWORD PROTECTION) - React Dev Server
location /groups {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Protected routes - Moderation (password protected) - React Dev Server
location /moderation { location /moderation {
auth_basic "Restricted Area - Moderation"; auth_basic "Restricted Area - Moderation";
auth_basic_user_file /etc/nginx/.htpasswd; auth_basic_user_file /etc/nginx/.htpasswd;
@ -136,18 +42,16 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
} }
# WebSocket support for hot reloading (React Dev Server) # WebSocket support for React Hot Reloading
location /ws { location /ws {
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade"; proxy_set_header Connection "Upgrade";
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
} }
# Frontend files - React Dev Server # All other routes React Dev Server (Client-side routing)
location / { location / {
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1; proxy_http_version 1.1;

View File

@ -33,6 +33,7 @@
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"proxy": "http://localhost:5001",
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"react-app", "react-app",

View File

@ -2,6 +2,8 @@ import React, { useState } from 'react';
import { Box, Typography, Paper } from '@mui/material'; import { Box, Typography, Paper } from '@mui/material';
import Swal from 'sweetalert2'; import Swal from 'sweetalert2';
import DescriptionInput from './MultiUpload/DescriptionInput'; import DescriptionInput from './MultiUpload/DescriptionInput';
import { adminRequest } from '../../services/adminApi';
import { handleAdminError } from '../../services/adminErrorHandler';
/** /**
* Manages group metadata with save functionality * Manages group metadata with save functionality
@ -66,11 +68,14 @@ function GroupMetadataEditor({
// Different API endpoints for manage vs moderate // Different API endpoints for manage vs moderate
const endpoint = isModerateMode const endpoint = isModerateMode
? `/groups/${groupId}` ? `/api/admin/groups/${groupId}`
: `/api/manage/${token}/metadata`; : `/api/manage/${token}/metadata`;
const method = isModerateMode ? 'PATCH' : 'PUT'; const method = isModerateMode ? 'PATCH' : 'PUT';
if (isModerateMode) {
await adminRequest(endpoint, method, metadata);
} else {
const res = await fetch(endpoint, { const res = await fetch(endpoint, {
method: method, method: method,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -81,6 +86,7 @@ function GroupMetadataEditor({
const body = await res.json().catch(() => ({})); const body = await res.json().catch(() => ({}));
throw new Error(body.error || 'Fehler beim Speichern der Metadaten'); throw new Error(body.error || 'Fehler beim Speichern der Metadaten');
} }
}
await Swal.fire({ await Swal.fire({
icon: 'success', icon: 'success',
@ -100,11 +106,16 @@ function GroupMetadataEditor({
} catch (error) { } catch (error) {
console.error('Error saving metadata:', error); console.error('Error saving metadata:', error);
if (isModerateMode) {
handleAdminError(error, 'Metadaten speichern');
} else {
Swal.fire({ Swal.fire({
icon: 'error', icon: 'error',
title: 'Fehler', title: 'Fehler',
text: error.message || 'Metadaten konnten nicht gespeichert werden' text: error.message || 'Metadaten konnten nicht gespeichert werden'
}); });
}
} finally { } finally {
setSaving(false); setSaving(false);
} }

View File

@ -2,6 +2,8 @@ import React, { useState } from 'react';
import { Box, Typography, Paper } from '@mui/material'; import { Box, Typography, Paper } from '@mui/material';
import Swal from 'sweetalert2'; import Swal from 'sweetalert2';
import ImageGallery from './ImageGallery'; import ImageGallery from './ImageGallery';
import { adminRequest } from '../../services/adminApi';
import { handleAdminError } from '../../services/adminErrorHandler';
/** /**
* Manages image descriptions with save functionality * Manages image descriptions with save functionality
@ -41,9 +43,12 @@ function ImageDescriptionManager({
try { try {
// Different API endpoints for manage vs moderate // Different API endpoints for manage vs moderate
const endpoint = mode === 'moderate' const endpoint = mode === 'moderate'
? `/groups/${groupId}/images/${imageId}` ? `/api/admin/groups/${groupId}/images/${imageId}`
: `/api/manage/${token}/images/${imageId}`; : `/api/manage/${token}/images/${imageId}`;
if (mode === 'moderate') {
await adminRequest(endpoint, 'DELETE');
} else {
const res = await fetch(endpoint, { const res = await fetch(endpoint, {
method: 'DELETE' method: 'DELETE'
}); });
@ -52,6 +57,7 @@ function ImageDescriptionManager({
const body = await res.json().catch(() => ({})); const body = await res.json().catch(() => ({}));
throw new Error(body.error || 'Fehler beim Löschen des Bildes'); throw new Error(body.error || 'Fehler beim Löschen des Bildes');
} }
}
await Swal.fire({ await Swal.fire({
icon: 'success', icon: 'success',
@ -67,6 +73,9 @@ function ImageDescriptionManager({
} }
} catch (error) { } catch (error) {
if (mode === 'moderate') {
await handleAdminError(error, 'Bild löschen');
} else {
console.error('Error deleting image:', error); console.error('Error deleting image:', error);
Swal.fire({ Swal.fire({
icon: 'error', icon: 'error',
@ -74,6 +83,7 @@ function ImageDescriptionManager({
text: error.message || 'Bild konnte nicht gelöscht werden' text: error.message || 'Bild konnte nicht gelöscht werden'
}); });
} }
}
}; };
// Initialize descriptions from images // Initialize descriptions from images
@ -120,11 +130,14 @@ function ImageDescriptionManager({
// Different API endpoints for manage vs moderate // Different API endpoints for manage vs moderate
const endpoint = mode === 'moderate' const endpoint = mode === 'moderate'
? `/groups/${groupId}/images/batch-description` ? `/api/admin/groups/${groupId}/images/batch-description`
: `/api/manage/${token}/images/descriptions`; : `/api/manage/${token}/images/descriptions`;
const method = mode === 'moderate' ? 'PATCH' : 'PUT'; const method = mode === 'moderate' ? 'PATCH' : 'PUT';
if (mode === 'moderate') {
await adminRequest(endpoint, method, { descriptions });
} else {
const res = await fetch(endpoint, { const res = await fetch(endpoint, {
method: method, method: method,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -133,7 +146,8 @@ function ImageDescriptionManager({
if (!res.ok) { if (!res.ok) {
const body = await res.json().catch(() => ({})); const body = await res.json().catch(() => ({}));
throw new Error(body.error || 'Fehler beim Speichern der Beschreibungen'); throw new Error(body.error || 'Fehler beim Speichern');
}
} }
await Swal.fire({ await Swal.fire({
@ -154,11 +168,16 @@ function ImageDescriptionManager({
} catch (error) { } catch (error) {
console.error('Error saving descriptions:', error); console.error('Error saving descriptions:', error);
if (mode === 'moderate') {
handleAdminError(error, 'Beschreibungen speichern');
} else {
Swal.fire({ Swal.fire({
icon: 'error', icon: 'error',
title: 'Fehler', title: 'Fehler',
text: error.message || 'Beschreibungen konnten nicht gespeichert werden' text: error.message || 'Beschreibungen konnten nicht gespeichert werden'
}); });
}
} finally { } finally {
setSaving(false); setSaving(false);
} }

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Container, Box, FormControl, InputLabel, Select, MenuItem } from '@mui/material'; import { Container, Box, FormControl, FormLabel, FormGroup, FormControlLabel, Checkbox } from '@mui/material';
import FilterListIcon from '@mui/icons-material/FilterList'; import FilterListIcon from '@mui/icons-material/FilterList';
import Swal from 'sweetalert2/dist/sweetalert2.js'; import Swal from 'sweetalert2/dist/sweetalert2.js';
@ -22,7 +22,12 @@ const ModerationGroupsPage = () => {
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [selectedGroup, setSelectedGroup] = useState(null); const [selectedGroup, setSelectedGroup] = useState(null);
const [showImages, setShowImages] = useState(false); const [showImages, setShowImages] = useState(false);
const [consentFilter, setConsentFilter] = useState('all'); const [consentFilters, setConsentFilters] = useState({
workshop: false,
facebook: false,
instagram: false,
tiktok: false
});
const [platforms, setPlatforms] = useState([]); const [platforms, setPlatforms] = useState([]);
const navigate = useNavigate(); const navigate = useNavigate();
@ -33,7 +38,7 @@ const ModerationGroupsPage = () => {
useEffect(() => { useEffect(() => {
loadModerationGroups(); loadModerationGroups();
}, [consentFilter]); }, [consentFilters]);
const loadPlatforms = async () => { const loadPlatforms = async () => {
try { try {
@ -52,13 +57,10 @@ const ModerationGroupsPage = () => {
let url = '/api/admin/groups'; let url = '/api/admin/groups';
const params = new URLSearchParams(); const params = new URLSearchParams();
if (consentFilter !== 'all') { // Sende alle aktivierten Filter
if (consentFilter === 'workshop-only') { const activeFilters = Object.keys(consentFilters).filter(key => consentFilters[key]);
params.append('workshopOnly', 'true'); if (activeFilters.length > 0) {
} else { params.append('consents', activeFilters.join(','));
// Platform filter (facebook, instagram, tiktok)
params.append('platform', consentFilter);
}
} }
if (params.toString()) { if (params.toString()) {
@ -238,25 +240,53 @@ const ModerationGroupsPage = () => {
alignItems: 'center', alignItems: 'center',
flexWrap: 'wrap' flexWrap: 'wrap'
}}> }}>
<FormControl sx={{ minWidth: 250 }} size="small"> <FormControl component="fieldset" sx={{ minWidth: 250 }}>
<InputLabel id="consent-filter-label"> <FormLabel component="legend" sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<FilterListIcon sx={{ mr: 0.5, fontSize: 18, verticalAlign: 'middle' }} /> <FilterListIcon sx={{ mr: 0.5, fontSize: 18 }} />
Consent-Filter Consent-Filter
</InputLabel> </FormLabel>
<Select <FormGroup>
labelId="consent-filter-label" <FormControlLabel
value={consentFilter} control={
label="Consent-Filter" <Checkbox
onChange={(e) => setConsentFilter(e.target.value)} checked={consentFilters.workshop}
> onChange={(e) => setConsentFilters({...consentFilters, workshop: e.target.checked})}
<MenuItem value="all">Alle Gruppen</MenuItem> size="small"
<MenuItem value="workshop-only">Nur Werkstatt-Consent</MenuItem> />
{platforms.map(platform => ( }
<MenuItem key={platform.id} value={platform.platform_name}> label="Werkstatt"
{platform.display_name} />
</MenuItem> <FormControlLabel
))} control={
</Select> <Checkbox
checked={consentFilters.facebook}
onChange={(e) => setConsentFilters({...consentFilters, facebook: e.target.checked})}
size="small"
/>
}
label="Facebook"
/>
<FormControlLabel
control={
<Checkbox
checked={consentFilters.instagram}
onChange={(e) => setConsentFilters({...consentFilters, instagram: e.target.checked})}
size="small"
/>
}
label="Instagram"
/>
<FormControlLabel
control={
<Checkbox
checked={consentFilters.tiktok}
onChange={(e) => setConsentFilters({...consentFilters, tiktok: e.target.checked})}
size="small"
/>
}
label="TikTok"
/>
</FormGroup>
</FormControl> </FormControl>
<button <button