wip(phase2): Task 17 - Management-Link in Upload-Erfolg & Rate-Limiter Anpassung

- Task 17: Management-Link im Upload-Erfolg angezeigt mit Copy-Button
- Widerruf-Dialoge überarbeitet: Klarstellung zu Scope & Kontakt für Social Media Posts
- Rate-Limiter für Dev-Umgebung erhöht (100/h statt 10/h)
- Mailto-Link Verhalten noch nicht final getestet (Browser vs. Mail-Client)

ACHTUNG: Noch nicht vollständig getestet! Mailto-Funktionalität muss in verschiedenen Browsern validiert werden.
This commit is contained in:
Matthias Lotz 2025-11-13 22:03:50 +01:00
parent cedc1380dd
commit e065f2bbc4
3 changed files with 138 additions and 25 deletions

View File

@ -13,7 +13,7 @@ const blockedIPs = new Map(); // IP -> { reason, blockedUntil, failedAttempts
// Konfiguration
const RATE_LIMIT = {
MAX_REQUESTS_PER_HOUR: 10,
MAX_REQUESTS_PER_HOUR: process.env.NODE_ENV === 'production' ? 10 : 100, // 100 für Dev, 10 für Production
WINDOW_MS: 60 * 60 * 1000, // 1 Stunde
BRUTE_FORCE_THRESHOLD: 20,
BLOCK_DURATION_MS: 24 * 60 * 60 * 1000 // 24 Stunden

View File

@ -227,24 +227,43 @@ const ManagementPortalPage = () => {
? 'Werkstatt-Anzeige'
: group.consents.socialMediaConsents.find(c => c.platformId === platformId)?.platformDisplayName || 'Social Media';
const result = await Swal.fire({
title: `Einwilligung widerrufen?`,
html: `Möchten Sie Ihre Einwilligung für <strong>${consentName}</strong> widerrufen?<br><br>
<small>Ihre Bilder werden dann nicht mehr für diesen Zweck verwendet.</small>`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Ja, widerrufen',
cancelButtonText: 'Abbrechen'
});
if (consentType === 'workshop') {
const result = await Swal.fire({
title: `Einwilligung widerrufen?`,
html: `Möchten Sie Ihre Einwilligung für <strong>${consentName}</strong> widerrufen?<br><br>
<small>Ihre Bilder werden aus der Werkstatt-Anzeige entfernt.</small>`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Ja, widerrufen',
cancelButtonText: 'Abbrechen'
});
if (!result.isConfirmed) return;
if (!result.isConfirmed) return;
} else {
// Social Media Widerruf
const result = await Swal.fire({
title: `Einwilligung widerrufen?`,
html: `Möchten Sie Ihre Einwilligung für <strong>${consentName}</strong> widerrufen?<br><br>
<small>Ihre Bilder werden nicht mehr auf ${consentName} veröffentlicht.<br>
Bereits veröffentlichte Beiträge bleiben bestehen, aber es werden keine neuen Posts mit Ihren Bildern erstellt.</small>`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Ja, widerrufen',
cancelButtonText: 'Abbrechen',
footer: `<div style="font-size: 13px; color: #666;">Wenn Sie die Löschung bereits veröffentlichter Beiträge wünschen, kontaktieren Sie uns nach dem Widerruf.</div>`
});
if (!result.isConfirmed) return;
}
try {
const payload = consentType === 'workshop'
? { workshopConsent: false }
: { socialMediaConsents: [{ platformId, consented: false }] };
? { consentType: 'workshop', action: 'revoke' }
: { consentType: 'social_media', action: 'revoke', platformId };
const res = await fetch(`/api/manage/${token}/consents`, {
method: 'PUT',
@ -257,13 +276,33 @@ const ManagementPortalPage = () => {
throw new Error(body.error || 'Fehler beim Widerrufen');
}
await Swal.fire({
icon: 'success',
title: 'Einwilligung widerrufen',
text: `Ihre Einwilligung für ${consentName} wurde widerrufen.`,
timer: 2000,
showConfirmButton: false
});
// Erfolg - zeige Bestätigung mit Kontaktinfo für Social Media
if (consentType === 'social-media') {
const mailtoLink = `mailto:it@hobbyhimmel.de?subject=${encodeURIComponent(`Löschung Social Media Post - Gruppe ${group.groupId}`)}&body=${encodeURIComponent(`Hallo,\n\nBitte löschen Sie die bereits veröffentlichten Beiträge meiner Gruppe ${group.groupId} von ${consentName}.\n\nVielen Dank`)}`;
await Swal.fire({
icon: 'success',
title: 'Einwilligung widerrufen',
html: `Ihre Einwilligung für ${consentName} wurde widerrufen.<br><br>
<div style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin-top: 15px;">
<strong>Bereits veröffentlichte Beiträge löschen?</strong><br>
<small>Kontaktieren Sie uns mit Ihrer Gruppen-ID:</small><br>
<div style="margin-top: 10px;">
<strong>Gruppen-ID:</strong> ${group.groupId}<br>
<strong>E-Mail:</strong> <span style="color: #1976d2; cursor: pointer;" onclick="window.open='${mailtoLink}'">it@hobbyhimmel.de</span>
</div>
</div>`,
confirmButtonText: 'Verstanden'
});
} else {
await Swal.fire({
icon: 'success',
title: 'Einwilligung widerrufen',
text: `Ihre Einwilligung für ${consentName} wurde widerrufen.`,
timer: 2000,
showConfirmButton: false
});
}
// Reload group to get updated consent status
await loadGroup();
@ -299,8 +338,8 @@ const ManagementPortalPage = () => {
try {
const payload = consentType === 'workshop'
? { workshopConsent: true }
: { socialMediaConsents: [{ platformId, consented: true }] };
? { consentType: 'workshop', action: 'restore' }
: { consentType: 'social_media', action: 'restore', platformId };
const res = await fetch(`/api/manage/${token}/consents`, {
method: 'PUT',

View File

@ -362,6 +362,80 @@ function MultiUploadPage() {
</Typography>
</Box>
{uploadResult?.managementToken && (
<Box sx={{
bgcolor: 'rgba(255,255,255,0.95)',
borderRadius: '8px',
p: 2.5,
mb: 2,
border: '2px solid rgba(255,255,255,0.3)'
}}>
<Typography sx={{ fontSize: '16px', fontWeight: 'bold', mb: 1.5, color: '#2e7d32' }}>
🔗 Verwaltungslink für Ihren Upload
</Typography>
<Typography sx={{ fontSize: '13px', mb: 1.5, color: '#333' }}>
Mit diesem Link können Sie später Ihre Bilder verwalten, Einwilligungen widerrufen oder die Gruppe löschen:
</Typography>
<Box sx={{
bgcolor: '#f5f5f5',
p: 1.5,
borderRadius: '6px',
mb: 1.5,
display: 'flex',
alignItems: 'center',
gap: 1,
flexWrap: 'wrap'
}}>
<Typography sx={{
fontSize: '13px',
fontFamily: 'monospace',
color: '#1976d2',
wordBreak: 'break-all',
flex: 1,
minWidth: '200px'
}}>
{window.location.origin}/manage/{uploadResult.managementToken}
</Typography>
<Button
size="small"
sx={{
minWidth: 'auto',
px: 2,
py: 0.5,
fontSize: '12px',
textTransform: 'none',
bgcolor: '#1976d2',
color: 'white',
'&:hover': {
bgcolor: '#1565c0'
}
}}
onClick={() => {
const link = `${window.location.origin}/manage/${uploadResult.managementToken}`;
navigator.clipboard.writeText(link);
Swal.fire({
icon: 'success',
title: 'Link kopiert!',
text: 'Der Verwaltungslink wurde in die Zwischenablage kopiert.',
timer: 2000,
showConfirmButton: false
});
}}
>
📋 Kopieren
</Button>
</Box>
<Typography sx={{ fontSize: '11px', color: '#666', mb: 0.5 }}>
<strong>Wichtig:</strong> Bewahren Sie diesen Link sicher auf! Jeder mit diesem Link kann Ihren Upload verwalten.
</Typography>
<Typography sx={{ fontSize: '11px', color: '#666', fontStyle: 'italic' }}>
<strong>Hinweis:</strong> Über diesen Link können Sie nur die Bilder in der Werkstatt verwalten. Bereits auf Social Media Plattformen veröffentlichte Bilder müssen separat dort gelöscht werden.
</Typography>
</Box>
)}
<Typography sx={{ fontSize: '13px', mb: 2, opacity: 0.95 }}>
Die Bilder werden geprüft und nach Freigabe auf dem Werkstatt-Monitor angezeigt.
{' '}Bei Social Media Einwilligung werden sie entsprechend veröffentlicht.