diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js
index 41b9546..0c35896 100644
--- a/backend/src/routes/admin.js
+++ b/backend/src/routes/admin.js
@@ -417,7 +417,7 @@ router.get('/groups', async (req, res) => {
}
*/
try {
- const { workshopOnly, platform } = req.query;
+ const { workshopOnly, platform, consents } = req.query;
// Hole alle Gruppen mit vollständigen Infos (inkl. Bilder)
let allGroups = await GroupRepository.getAllGroupsWithModerationInfo();
@@ -425,18 +425,38 @@ router.get('/groups', async (req, res) => {
// Füge Consent-Daten für jede Gruppe hinzu
const groupsWithConsents = await Promise.all(
allGroups.map(async (group) => {
- const consents = await GroupRepository.getSocialMediaConsentsForGroup(group.groupId);
+ const consentData = await GroupRepository.getSocialMediaConsentsForGroup(group.groupId);
return {
...group,
- socialMediaConsents: consents
+ socialMediaConsents: consentData
};
})
);
- // Jetzt filtern wir basierend auf den Query-Parametern
+ // Jetzt filtern wir basierend auf den Query-Parametern
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
filteredGroups = groupsWithConsents.filter(group => {
// Muss Werkstatt-Consent haben
@@ -922,15 +942,32 @@ router.delete('/groups/:groupId', async (req, res) => {
try {
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({
error: 'Group not found',
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({
success: true,
message: 'Gruppe erfolgreich gelöscht',
diff --git a/backend/src/routes/consent.js b/backend/src/routes/consent.js
index 8850b8c..9445a64 100644
--- a/backend/src/routes/consent.js
+++ b/backend/src/routes/consent.js
@@ -403,7 +403,8 @@ router.get('/consents/export', async (req, res) => {
// Platform-Consents
const consentMap = {};
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 => {
diff --git a/backend/src/server.js b/backend/src/server.js
index 3a21947..3141206 100644
--- a/backend/src/server.js
+++ b/backend/src/server.js
@@ -45,6 +45,7 @@ class Server {
// Starte Express Server
initiateResources(this._app);
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
if (process.env.NODE_ENV !== 'production' && swaggerUi && swaggerDocument) {
@@ -74,6 +75,7 @@ class Server {
await dbManager.initialize();
initiateResources(this._app);
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) {
this._app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml
index 57fc2d8..3c159eb 100644
--- a/docker/dev/docker-compose.yml
+++ b/docker/dev/docker-compose.yml
@@ -26,6 +26,7 @@ services:
backend-dev:
container_name: image-uploader-backend-dev
+ user: "1000:1000"
build:
context: ../../
dockerfile: docker/dev/backend/Dockerfile
diff --git a/docker/dev/frontend/nginx.conf b/docker/dev/frontend/nginx.conf
index 358d7a2..d76f530 100644
--- a/docker/dev/frontend/nginx.conf
+++ b/docker/dev/frontend/nginx.conf
@@ -1,128 +1,34 @@
server {
listen 80;
- # Allow large uploads (50MB)
- client_max_body_size 50M;
+ # Allow large uploads (100MB for batch uploads)
+ 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_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 API too
- 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
+ # Large uploads für Batch-Upload
client_max_body_size 100M;
}
-
- # API - Download original images
- location /api/download {
- 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)
- 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;
- }
+ # ========================================
+ # Frontend Routes (React Dev Server)
+ # ========================================
- # 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
+ # Protected route - Moderation (HTTP Basic Auth)
location /moderation {
auth_basic "Restricted Area - Moderation";
auth_basic_user_file /etc/nginx/.htpasswd;
@@ -136,18 +42,16 @@ server {
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 {
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;
}
- # Frontend files - React Dev Server
+ # All other routes → React Dev Server (Client-side routing)
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
diff --git a/frontend/package.json b/frontend/package.json
index e0e4a8b..16ef67a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -33,6 +33,7 @@
"test": "react-scripts test",
"eject": "react-scripts eject"
},
+ "proxy": "http://localhost:5001",
"eslintConfig": {
"extends": [
"react-app",
diff --git a/frontend/src/Components/ComponentUtils/GroupMetadataEditor.js b/frontend/src/Components/ComponentUtils/GroupMetadataEditor.js
index db171a5..c67ca00 100644
--- a/frontend/src/Components/ComponentUtils/GroupMetadataEditor.js
+++ b/frontend/src/Components/ComponentUtils/GroupMetadataEditor.js
@@ -2,6 +2,8 @@ import React, { useState } from 'react';
import { Box, Typography, Paper } from '@mui/material';
import Swal from 'sweetalert2';
import DescriptionInput from './MultiUpload/DescriptionInput';
+import { adminRequest } from '../../services/adminApi';
+import { handleAdminError } from '../../services/adminErrorHandler';
/**
* Manages group metadata with save functionality
@@ -66,20 +68,24 @@ function GroupMetadataEditor({
// Different API endpoints for manage vs moderate
const endpoint = isModerateMode
- ? `/groups/${groupId}`
+ ? `/api/admin/groups/${groupId}`
: `/api/manage/${token}/metadata`;
const method = isModerateMode ? 'PATCH' : 'PUT';
- const res = await fetch(endpoint, {
- method: method,
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(metadata)
- });
+ if (isModerateMode) {
+ await adminRequest(endpoint, method, metadata);
+ } else {
+ const res = await fetch(endpoint, {
+ method: method,
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(metadata)
+ });
- if (!res.ok) {
- const body = await res.json().catch(() => ({}));
- throw new Error(body.error || 'Fehler beim Speichern der Metadaten');
+ if (!res.ok) {
+ const body = await res.json().catch(() => ({}));
+ throw new Error(body.error || 'Fehler beim Speichern der Metadaten');
+ }
}
await Swal.fire({
@@ -100,11 +106,16 @@ function GroupMetadataEditor({
} catch (error) {
console.error('Error saving metadata:', error);
- Swal.fire({
- icon: 'error',
- title: 'Fehler',
- text: error.message || 'Metadaten konnten nicht gespeichert werden'
- });
+
+ if (isModerateMode) {
+ handleAdminError(error, 'Metadaten speichern');
+ } else {
+ Swal.fire({
+ icon: 'error',
+ title: 'Fehler',
+ text: error.message || 'Metadaten konnten nicht gespeichert werden'
+ });
+ }
} finally {
setSaving(false);
}
diff --git a/frontend/src/Components/ComponentUtils/ImageDescriptionManager.js b/frontend/src/Components/ComponentUtils/ImageDescriptionManager.js
index 32962c1..4b3b9ad 100644
--- a/frontend/src/Components/ComponentUtils/ImageDescriptionManager.js
+++ b/frontend/src/Components/ComponentUtils/ImageDescriptionManager.js
@@ -2,6 +2,8 @@ import React, { useState } from 'react';
import { Box, Typography, Paper } from '@mui/material';
import Swal from 'sweetalert2';
import ImageGallery from './ImageGallery';
+import { adminRequest } from '../../services/adminApi';
+import { handleAdminError } from '../../services/adminErrorHandler';
/**
* Manages image descriptions with save functionality
@@ -41,16 +43,20 @@ function ImageDescriptionManager({
try {
// Different API endpoints for manage vs moderate
const endpoint = mode === 'moderate'
- ? `/groups/${groupId}/images/${imageId}`
+ ? `/api/admin/groups/${groupId}/images/${imageId}`
: `/api/manage/${token}/images/${imageId}`;
- const res = await fetch(endpoint, {
- method: 'DELETE'
- });
+ if (mode === 'moderate') {
+ await adminRequest(endpoint, 'DELETE');
+ } else {
+ const res = await fetch(endpoint, {
+ method: 'DELETE'
+ });
- if (!res.ok) {
- const body = await res.json().catch(() => ({}));
- throw new Error(body.error || 'Fehler beim Löschen des Bildes');
+ if (!res.ok) {
+ const body = await res.json().catch(() => ({}));
+ throw new Error(body.error || 'Fehler beim Löschen des Bildes');
+ }
}
await Swal.fire({
@@ -67,12 +73,16 @@ function ImageDescriptionManager({
}
} catch (error) {
- console.error('Error deleting image:', error);
- Swal.fire({
- icon: 'error',
- title: 'Fehler',
- text: error.message || 'Bild konnte nicht gelöscht werden'
- });
+ if (mode === 'moderate') {
+ await handleAdminError(error, 'Bild löschen');
+ } else {
+ console.error('Error deleting image:', error);
+ Swal.fire({
+ icon: 'error',
+ title: 'Fehler',
+ text: error.message || 'Bild konnte nicht gelöscht werden'
+ });
+ }
}
};
@@ -120,20 +130,24 @@ function ImageDescriptionManager({
// Different API endpoints for manage vs moderate
const endpoint = mode === 'moderate'
- ? `/groups/${groupId}/images/batch-description`
+ ? `/api/admin/groups/${groupId}/images/batch-description`
: `/api/manage/${token}/images/descriptions`;
const method = mode === 'moderate' ? 'PATCH' : 'PUT';
- const res = await fetch(endpoint, {
- method: method,
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ descriptions })
- });
+ if (mode === 'moderate') {
+ await adminRequest(endpoint, method, { descriptions });
+ } else {
+ const res = await fetch(endpoint, {
+ method: method,
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ descriptions })
+ });
- if (!res.ok) {
- const body = await res.json().catch(() => ({}));
- throw new Error(body.error || 'Fehler beim Speichern der Beschreibungen');
+ if (!res.ok) {
+ const body = await res.json().catch(() => ({}));
+ throw new Error(body.error || 'Fehler beim Speichern');
+ }
}
await Swal.fire({
@@ -154,11 +168,16 @@ function ImageDescriptionManager({
} catch (error) {
console.error('Error saving descriptions:', error);
- Swal.fire({
- icon: 'error',
- title: 'Fehler',
- text: error.message || 'Beschreibungen konnten nicht gespeichert werden'
- });
+
+ if (mode === 'moderate') {
+ handleAdminError(error, 'Beschreibungen speichern');
+ } else {
+ Swal.fire({
+ icon: 'error',
+ title: 'Fehler',
+ text: error.message || 'Beschreibungen konnten nicht gespeichert werden'
+ });
+ }
} finally {
setSaving(false);
}
diff --git a/frontend/src/Components/Pages/ModerationGroupsPage.js b/frontend/src/Components/Pages/ModerationGroupsPage.js
index 4778520..cbed756 100644
--- a/frontend/src/Components/Pages/ModerationGroupsPage.js
+++ b/frontend/src/Components/Pages/ModerationGroupsPage.js
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet';
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 Swal from 'sweetalert2/dist/sweetalert2.js';
@@ -22,7 +22,12 @@ const ModerationGroupsPage = () => {
const [error, setError] = useState(null);
const [selectedGroup, setSelectedGroup] = useState(null);
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 navigate = useNavigate();
@@ -33,7 +38,7 @@ const ModerationGroupsPage = () => {
useEffect(() => {
loadModerationGroups();
- }, [consentFilter]);
+ }, [consentFilters]);
const loadPlatforms = async () => {
try {
@@ -52,13 +57,10 @@ const ModerationGroupsPage = () => {
let url = '/api/admin/groups';
const params = new URLSearchParams();
- if (consentFilter !== 'all') {
- if (consentFilter === 'workshop-only') {
- params.append('workshopOnly', 'true');
- } else {
- // Platform filter (facebook, instagram, tiktok)
- params.append('platform', consentFilter);
- }
+ // Sende alle aktivierten Filter
+ const activeFilters = Object.keys(consentFilters).filter(key => consentFilters[key]);
+ if (activeFilters.length > 0) {
+ params.append('consents', activeFilters.join(','));
}
if (params.toString()) {
@@ -238,25 +240,53 @@ const ModerationGroupsPage = () => {
alignItems: 'center',
flexWrap: 'wrap'
}}>
-
-
-
+
+
+
Consent-Filter
-
-
+
+
+ setConsentFilters({...consentFilters, workshop: e.target.checked})}
+ size="small"
+ />
+ }
+ label="Werkstatt"
+ />
+ setConsentFilters({...consentFilters, facebook: e.target.checked})}
+ size="small"
+ />
+ }
+ label="Facebook"
+ />
+ setConsentFilters({...consentFilters, instagram: e.target.checked})}
+ size="small"
+ />
+ }
+ label="Instagram"
+ />
+ setConsentFilters({...consentFilters, tiktok: e.target.checked})}
+ size="small"
+ />
+ }
+ label="TikTok"
+ />
+