Project-Image-Uploader/frontend/src/Components/Pages/ManagementPortalPage.js
matthias.lotz cdb2aa95e6 feat: Add comprehensive test suite and admin API authentication
🧪 Testing Infrastructure (45 tests, 100% passing)
- Implemented Jest + Supertest framework for automated testing
- Unit tests: 5 tests for auth middleware (100% coverage)
- Integration tests: 40 tests covering admin, consent, migration, upload APIs
- Test execution time: ~10 seconds for full suite
- Coverage: 26% statements, 15% branches (realistic start)
- In-memory SQLite database for isolated testing
- Singleton server pattern for fast test execution
- Automatic cleanup and teardown

🔒 Admin API Authentication
- Bearer token authentication for all admin endpoints
- requireAdminAuth middleware with ADMIN_API_KEY validation
- Protected routes: /api/admin/*, /api/system/migration/migrate|rollback
- Complete authentication guide in AUTHENTICATION.md
- HTTP 403 for missing/invalid tokens, 500 if not configured
- Ready for production with token rotation support

📋 API Route Documentation
- Single Source of Truth: backend/src/routes/routeMappings.js
- Comprehensive route overview in backend/src/routes/README.md
- Express routing order documented (specific before generic)
- Frontend integration guide with authentication examples
- OpenAPI auto-generation integrated

🐛 Bug Fixes
- Fixed SQLite connection not properly awaited (caused test hangs)
- Fixed upload validation checking req.files.file before req.files
- Fixed Express route order (consent before admin router)
- Fixed test environment using /tmp for uploads (permission issues)

📚 Documentation Updates
- Updated README.md with testing and authentication features
- Updated README.dev.md with testing section and API development guide
- Updated CHANGELOG.md with complete feature documentation
- Updated FEATURE_PLAN-autogen-openapi.md (status: 100% complete)
- Added frontend/MIGRATION-GUIDE.md for frontend team

🚀 Frontend Impact
Frontend needs to add Bearer token to all /api/admin/* calls.
See frontend/MIGRATION-GUIDE.md for detailed instructions.

Test Status:  45/45 passing (100%)
Backend:  Production ready
Frontend: ⚠️ Migration required (see MIGRATION-GUIDE.md)
2025-11-16 18:08:48 +01:00

315 lines
9.8 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Container, Card, CardContent, Typography, Box, Button } from '@mui/material';
import Swal from 'sweetalert2';
import Navbar from '../ComponentUtils/Headers/Navbar';
import Footer from '../ComponentUtils/Footer';
import Loading from '../ComponentUtils/LoadingAnimation/Loading';
import ImageGalleryCard from '../ComponentUtils/ImageGalleryCard';
import ConsentBadges from '../ComponentUtils/ConsentBadges';
import MultiImageDropzone from '../ComponentUtils/MultiUpload/MultiImageDropzone';
import ImageDescriptionManager from '../ComponentUtils/ImageDescriptionManager';
import GroupMetadataEditor from '../ComponentUtils/GroupMetadataEditor';
import ConsentManager from '../ComponentUtils/ConsentManager';
import DeleteGroupButton from '../ComponentUtils/DeleteGroupButton';
/**
* ManagementPortalPage - Self-service management for uploaded groups
*
* Modulare Struktur mit individuellen Komponenten:
* - ImageDescriptionManager: Bildbeschreibungen bearbeiten
* - GroupMetadataEditor: Gruppenmetadaten bearbeiten
* - ConsentManager: Einwilligungen verwalten
* - DeleteGroupButton: Gruppe löschen
*/
function ManagementPortalPage() {
const { token } = useParams();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [group, setGroup] = useState(null);
// Load group data
const loadGroup = async () => {
try {
setLoading(true);
setError(null);
const res = await fetch(`/api/manage/${token}`);
if (res.status === 404) {
setError('Ungültiger oder abgelaufener Verwaltungslink');
return;
}
if (res.status === 429) {
setError('Zu viele Anfragen. Bitte versuchen Sie es später erneut.');
return;
}
if (!res.ok) {
throw new Error('Fehler beim Laden der Gruppe');
}
const response = await res.json();
const data = response.data || response;
// Transform data
const transformedData = {
...data,
displayInWorkshop: data.displayInWorkshop || data.display_in_workshop,
consentTimestamp: data.consentTimestamp || data.consent_timestamp,
consents: {
workshopConsent: (data.displayInWorkshop === 1 || data.display_in_workshop === 1),
socialMediaConsents: (data.socialMediaConsents || [])
.filter(c => c.consented === 1 && c.revoked === 0)
.map(c => ({ platformId: c.platform_id, consented: true }))
},
metadata: {
year: data.year || new Date().getFullYear(),
title: data.title || '',
description: data.description || '',
name: data.name || ''
},
images: (data.images || []).map(img => ({
...img,
remoteUrl: `/download/${img.fileName}`,
originalName: img.originalName || img.fileName,
id: img.id,
imageDescription: img.imageDescription || ''
}))
};
setGroup(transformedData);
} catch (e) {
console.error('Error loading group:', e);
setError('Fehler beim Laden der Gruppe');
} finally {
setLoading(false);
}
};
useEffect(() => {
if (token) {
loadGroup();
}
}, [token]); // eslint-disable-line react-hooks/exhaustive-deps
// Handle adding new images
const handleImagesSelected = async (newImages) => {
try {
const formData = new FormData();
newImages.forEach(file => {
formData.append('images', file);
});
const res = await fetch(`/api/manage/${token}/images`, {
method: 'POST',
body: formData
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error || 'Fehler beim Hochladen');
}
await Swal.fire({
icon: 'success',
title: 'Bilder hinzugefügt',
text: `${newImages.length} Bild(er) wurden erfolgreich hinzugefügt.`,
timer: 2000,
showConfirmButton: false
});
// Reload group data
await loadGroup();
} catch (error) {
console.error('Error adding images:', error);
Swal.fire({
icon: 'error',
title: 'Fehler',
text: error.message || 'Bilder konnten nicht hinzugefügt werden'
});
}
};
const handleReorder = async (newOrder) => {
if (!group || !group.groupId) {
console.error('No groupId available for reordering');
return;
}
try {
const imageIds = newOrder.map(img => img.id);
// Use token-based management API
const response = await fetch(`/api/manage/${token}/reorder`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ imageIds: imageIds })
});
if (!response.ok) {
throw new Error('Reihenfolge konnte nicht gespeichert werden');
}
await Swal.fire({
icon: 'success',
title: 'Gespeichert',
text: 'Die neue Reihenfolge wurde gespeichert.',
timer: 1500,
showConfirmButton: false
});
await loadGroup();
} catch (error) {
console.error('Error reordering images:', error);
Swal.fire({
icon: 'error',
title: 'Fehler',
text: error.message || 'Reihenfolge konnte nicht gespeichert werden'
});
}
};
if (loading) {
return (
<div className="allContainer">
<Navbar />
<Container maxWidth="lg" sx={{ pt: '20px', pb: '40px', minHeight: '80vh', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Loading />
</Container>
<Footer />
</div>
);
}
if (error) {
return (
<div className="allContainer">
<Navbar />
<Container maxWidth="lg" sx={{ pt: '20px', pb: '40px', minHeight: '80vh' }}>
<Card sx={{ borderRadius: '12px', boxShadow: '0 4px 12px rgba(0,0,0,0.1)', p: '20px', textAlign: 'center' }}>
<Typography variant="h5" color="error" gutterBottom>
{error}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
{error}
</Typography>
<Button variant="contained" onClick={() => navigate('/')}>
Zur Startseite
</Button>
</Card>
</Container>
<Footer />
</div>
);
}
return (
<div className="allContainer">
<Navbar />
<Container maxWidth="lg" sx={{ pt: '20px', pb: '40px', minHeight: '80vh' }}>
<Card sx={{ borderRadius: '12px', boxShadow: '0 4px 12px rgba(0,0,0,0.1)', p: '20px', mb: '20px' }}>
<CardContent>
<Typography sx={{ fontFamily: 'roboto', fontWeight: 400, fontSize: '28px', textAlign: 'center', mb: '10px', color: '#333333' }}>
Mein Upload verwalten
</Typography>
<Typography sx={{ fontFamily: 'roboto', fontWeight: 300, fontSize: '16px', color: '#666666', textAlign: 'center', mb: '30px' }}>
Hier können Sie Ihre hochgeladenen Bilder verwalten, Metadaten bearbeiten und Einwilligungen ändern.
</Typography>
{/* Group Overview */}
{group && (
<Box sx={{ mb: 3 }}>
<ImageGalleryCard
item={group}
showActions={false}
isPending={!group.approved}
mode="group"
hidePreview={true}
/>
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle2" gutterBottom sx={{ fontWeight: 600 }}>
Erteilte Einwilligungen:
</Typography>
<ConsentBadges group={group} />
</Box>
</Box>
)}
{/* Add Images Dropzone */}
<Box sx={{ mb: 3 }}>
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600 }}>
Weitere Bilder hinzufügen
</Typography>
<MultiImageDropzone
onImagesSelected={handleImagesSelected}
selectedImages={[]}
/>
</Box>
{/* Image Descriptions Manager */}
{group && group.images && group.images.length > 0 && (
<Box sx={{ mb: 3 }}>
<ImageDescriptionManager
images={group.images}
token={token}
enableReordering={true}
onReorder={handleReorder}
onRefresh={loadGroup}
/>
</Box>
)}
{/* Group Metadata Editor */}
{group && (
<Box sx={{ mb: 3 }}>
<GroupMetadataEditor
initialMetadata={group.metadata}
token={token}
onRefresh={loadGroup}
/>
</Box>
)}
{/* Consent Manager */}
{group && (
<Box sx={{ mb: 3 }}>
<ConsentManager
initialConsents={group.consents}
token={token}
groupId={group.groupId}
onRefresh={loadGroup}
/>
</Box>
)}
{/* Delete Group Button */}
{group && (
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'center' }}>
<DeleteGroupButton
token={token}
groupName={group.title || group.name || 'diese Gruppe'}
/>
</Box>
)}
</CardContent>
</Card>
</Container>
<div className="footerContainer">
<Footer />
</div>
</div>
);
}
export default ManagementPortalPage;