medienkompetenz-lernplattform/frontend/src/components/lessons/InteractiveContent/ForumScriptDemo.jsx
Marius Rometsch a439873394 Add lessons
2026-02-08 19:47:21 +01:00

596 lines
21 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { participantAPI } from '../../../services/api.service';
const ForumScriptDemo = ({ lessonData, eventLessonId }) => {
const interactiveData = lessonData?.interactiveData || {};
const forumPost = interactiveData.forumPost || {};
const initialComments = interactiveData.initialComments || [];
const freeHints = interactiveData.freeHints || [];
const timeLimit = interactiveData.timeLimit || 900000; // 15 min default
const [comments, setComments] = useState(initialComments);
const [authorName, setAuthorName] = useState('');
const [commentText, setCommentText] = useState('');
const [loading, setLoading] = useState(false);
const [remainingTime, setRemainingTime] = useState(null);
const [timerStarted, setTimerStarted] = useState(false);
const [currentHint, setCurrentHint] = useState(null);
const [progress, setProgress] = useState({ discovered: 0, total: 9, remaining: 9 });
// Start timer on mount
useEffect(() => {
const startTimer = async () => {
try {
const response = await participantAPI.executeLessonAction(
eventLessonId,
'start-timer',
{ stepId: 'forum-demo' }
);
setTimerStarted(true);
setRemainingTime(timeLimit);
} catch (error) {
console.error('Failed to start timer:', error);
}
};
startTimer();
}, [eventLessonId, timeLimit]);
// Timer countdown
useEffect(() => {
if (remainingTime === null || remainingTime <= 0) return;
const interval = setInterval(() => {
setRemainingTime(prev => {
if (prev <= 1000) {
clearInterval(interval);
return 0;
}
return prev - 1000;
});
}, 1000);
return () => clearInterval(interval);
}, [remainingTime]);
const formatTime = (ms) => {
if (ms === null) return '--:--';
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
const addComment = async () => {
if (!commentText.trim()) return;
setLoading(true);
try {
const response = await participantAPI.executeLessonAction(
eventLessonId,
'add-comment',
{ author: authorName || 'Anonym', content: commentText, stepId: 'forum-demo' }
);
const newComment = response.data.data;
setComments([...comments, newComment]);
setCommentText('');
// Update progress
if (newComment.progress) {
setProgress(newComment.progress);
}
// Update remaining time from server
if (newComment.remainingTime !== undefined) {
setRemainingTime(newComment.remainingTime);
}
// Scroll to bottom to show new comment
setTimeout(() => {
const commentList = document.getElementById('comment-list');
if (commentList) {
commentList.scrollTop = commentList.scrollHeight;
}
}, 100);
} catch (error) {
console.error('Failed to add comment:', error);
} finally {
setLoading(false);
}
};
const requestHint = async () => {
try {
const response = await participantAPI.executeLessonAction(
eventLessonId,
'get-hint',
{ stepId: 'forum-demo' }
);
setCurrentHint(response.data.data);
} catch (error) {
console.error('Failed to get hint:', error);
}
};
const reloadForum = () => {
setComments(initialComments);
setAuthorName('');
setCommentText('');
};
const formatTimestamp = (timestamp) => {
if (!timestamp) return 'gerade eben';
const date = new Date(timestamp);
return date.toLocaleString('de-DE', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const timeExpired = remainingTime === 0;
const progressPercent = (progress.discovered / progress.total) * 100;
return (
<div style={{ border: '1px solid #e5e7eb', borderRadius: '0.5rem', padding: '1.5rem', background: 'white' }}>
{/* Educational Warning */}
<div style={{
padding: '1rem',
background: '#fef3c7',
border: '2px solid #f59e0b',
borderRadius: '0.375rem',
marginBottom: '1.5rem'
}}>
<div style={{ fontWeight: '600', color: '#92400e' }}>
Nur zu Lehrzwecken
</div>
<div style={{ fontSize: '0.875rem', color: '#78350f', marginTop: '0.5rem' }}>
Dieses Forum demonstriert Stored-XSS-Schwachstellen. Es werden keine tatsächlichen Skripte ausgeführt - sie werden sicher als Text mit klaren Warnungen angezeigt.
</div>
</div>
<h4 style={{ marginBottom: '1rem', color: '#1f2937' }}>💬 Anfälliges Forum Demo</h4>
{/* Progress and Timer Bar */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '1rem',
marginBottom: '1.5rem'
}}>
{/* Progress Tracker */}
<div style={{
padding: '1rem',
background: '#f0fdf4',
border: '2px solid #10b981',
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '600', color: '#065f46', marginBottom: '0.5rem' }}>
🎯 Fortschritt
</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700', color: '#059669', marginBottom: '0.25rem' }}>
{progress.discovered} / {progress.total}
</div>
<div style={{ fontSize: '0.875rem', color: '#047857' }}>
Varianten entdeckt
</div>
{/* Progress bar */}
<div style={{
marginTop: '0.75rem',
height: '8px',
background: '#d1fae5',
borderRadius: '4px',
overflow: 'hidden'
}}>
<div style={{
width: `${progressPercent}%`,
height: '100%',
background: '#10b981',
transition: 'width 0.3s'
}}></div>
</div>
</div>
{/* Timer */}
<div style={{
padding: '1rem',
background: timeExpired ? '#fee2e2' : '#eff6ff',
border: `2px solid ${timeExpired ? '#ef4444' : '#3b82f6'}`,
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '600', color: timeExpired ? '#991b1b' : '#1e40af', marginBottom: '0.5rem' }}>
Verbleibende Zeit
</div>
<div style={{
fontSize: '1.5rem',
fontWeight: '700',
color: timeExpired ? '#dc2626' : '#2563eb',
marginBottom: '0.25rem'
}}>
{formatTime(remainingTime)}
</div>
{timeExpired && (
<div style={{ fontSize: '0.875rem', color: '#991b1b', fontWeight: '600' }}>
Keine Punkte mehr verfügbar
</div>
)}
{!timeExpired && (
<div style={{ fontSize: '0.875rem', color: '#1e40af' }}>
Punkte verdienbar
</div>
)}
</div>
</div>
{/* Free Hints */}
<div style={{
padding: '1rem',
background: '#fef3c7',
border: '1px solid #fbbf24',
borderRadius: '0.375rem',
marginBottom: '1.5rem'
}}>
<div style={{ fontWeight: '600', color: '#78350f', marginBottom: '0.5rem' }}>
💡 Hinweise (kostenlos)
</div>
<ul style={{ margin: '0.5rem 0 0 1.5rem', fontSize: '0.875rem', color: '#92400e' }}>
{freeHints.map((hint, i) => (
<li key={i} style={{ marginBottom: '0.25rem' }}>{hint}</li>
))}
</ul>
</div>
{/* Hint Request Button */}
<div style={{ marginBottom: '1.5rem' }}>
<button
onClick={requestHint}
disabled={currentHint && currentHint.noMoreHints}
style={{
padding: '0.75rem 1rem',
background: currentHint && currentHint.noMoreHints ? '#9ca3af' : '#f59e0b',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: currentHint && currentHint.noMoreHints ? 'not-allowed' : 'pointer',
fontWeight: '500',
fontSize: '0.875rem',
opacity: currentHint && currentHint.noMoreHints ? 0.6 : 1
}}
>
💡 {currentHint && currentHint.noMoreHints ? 'Alle Hinweise verwendet' : 'Gezielten Hinweis anfordern (-5 Punkte)'}
</button>
{currentHint && !currentHint.noMoreHints && (
<div style={{
marginTop: '0.75rem',
padding: '1rem',
background: '#fff7ed',
border: '2px solid #fb923c',
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '600', color: '#9a3412', marginBottom: '0.5rem' }}>
Hinweis #{currentHint.hintsUsed}
</div>
<div style={{ fontSize: '0.875rem', color: '#7c2d12', marginBottom: '0.5rem' }}>
{currentHint.hint}
</div>
<div style={{ fontSize: '0.75rem', color: '#ea580c' }}>
Abgezogene Punkte: {currentHint.totalPointsDeducted}
</div>
</div>
)}
{currentHint && currentHint.noMoreHints && (
<div style={{
marginTop: '0.75rem',
padding: '1rem',
background: '#f3f4f6',
border: '2px solid #9ca3af',
borderRadius: '0.375rem',
color: '#6b7280',
fontSize: '0.875rem'
}}>
Keine weiteren Hinweise verfügbar
</div>
)}
</div>
{/* Forum Post */}
<div style={{
background: '#f9fafb',
border: '1px solid #e5e7eb',
borderRadius: '0.375rem',
padding: '1rem',
marginBottom: '1.5rem'
}}>
<div style={{ display: 'flex', alignItems: 'flex-start', marginBottom: '0.5rem' }}>
<div style={{
width: '2.5rem',
height: '2.5rem',
borderRadius: '50%',
background: '#3b82f6',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: '0.75rem',
fontWeight: '600'
}}>
A
</div>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: '600', color: '#1f2937', fontSize: '1.125rem' }}>
{forumPost.title}
</div>
<div style={{ fontSize: '0.75rem', color: '#6b7280' }}>
Gepostet von {forumPost.author} {formatTimestamp(forumPost.timestamp)}
</div>
</div>
</div>
<div style={{ fontSize: '0.875rem', color: '#374151', marginTop: '0.5rem' }}>
{forumPost.content}
</div>
</div>
{/* Comments List */}
<div
id="comment-list"
style={{
maxHeight: '300px',
overflowY: 'auto',
marginBottom: '1rem',
border: '1px solid #e5e7eb',
borderRadius: '0.375rem',
padding: '1rem',
background: '#fafafa'
}}
>
<div style={{ fontWeight: '600', marginBottom: '1rem', color: '#374151' }}>
Kommentare ({comments.length})
</div>
{comments.map((comment, idx) => (
<div
key={comment.id || idx}
style={{
marginBottom: '1rem',
paddingBottom: '1rem',
borderBottom: idx < comments.length - 1 ? '1px solid #e5e7eb' : 'none'
}}
>
<div style={{ display: 'flex', alignItems: 'flex-start' }}>
<div style={{
width: '2rem',
height: '2rem',
borderRadius: '50%',
background: '#9ca3af',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: '0.75rem',
fontSize: '0.875rem',
fontWeight: '600'
}}>
{comment.author?.charAt(0).toUpperCase() || '?'}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '0.875rem', fontWeight: '600', color: '#1f2937' }}>
{comment.author}
</div>
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginBottom: '0.5rem' }}>
{formatTimestamp(comment.timestamp)}
</div>
{/* Comment Content - safely displayed */}
<div style={{ fontSize: '0.875rem', color: '#374151' }}>
{comment.content}
</div>
{/* Injection Warning */}
{comment.hasInjection && (
<div style={{
marginTop: '0.5rem',
padding: '0.75rem',
background: '#fee2e2',
border: '2px solid #ef4444',
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '600', color: '#991b1b', fontSize: '0.75rem', marginBottom: '0.25rem' }}>
XSS ERKANNT: {comment.injectionType}
</div>
<div style={{ fontSize: '0.75rem', color: '#7f1d1d', marginBottom: '0.25rem' }}>
{comment.injectionDescription}
</div>
<div style={{ fontSize: '0.75rem', fontFamily: 'monospace', color: '#7f1d1d', background: '#fef2f2', padding: '0.25rem', borderRadius: '0.125rem' }}>
Bereinigt: {comment.sanitizedContent}
</div>
{comment.isNewDiscovery && (
<div style={{
marginTop: '0.5rem',
padding: '0.5rem',
background: '#ecfdf5',
border: '1px solid #10b981',
borderRadius: '0.25rem',
fontSize: '0.875rem',
color: '#065f46',
fontWeight: '600'
}}>
🎉 Neue Variante entdeckt! +{timeExpired ? '0' : '10'} Punkte
</div>
)}
</div>
)}
</div>
</div>
</div>
))}
</div>
{/* Add Comment Form */}
<div style={{
border: '1px solid #e5e7eb',
borderRadius: '0.375rem',
padding: '1rem',
background: '#f9fafb'
}}>
<div style={{ fontWeight: '600', marginBottom: '0.75rem', color: '#374151' }}>
Kommentar hinzufügen
</div>
<div style={{ marginBottom: '0.75rem' }}>
<input
type="text"
value={authorName}
onChange={(e) => setAuthorName(e.target.value)}
placeholder="Ihr Name (optional)"
style={{
width: '100%',
padding: '0.5rem',
border: '1px solid #d1d5db',
borderRadius: '0.375rem',
fontSize: '0.875rem'
}}
/>
</div>
<div style={{ marginBottom: '0.75rem' }}>
<textarea
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder="Schreiben Sie hier Ihren Kommentar..."
rows={3}
style={{
width: '100%',
padding: '0.5rem',
border: '1px solid #d1d5db',
borderRadius: '0.375rem',
fontSize: '0.875rem',
fontFamily: 'inherit',
resize: 'vertical'
}}
/>
</div>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<button
onClick={addComment}
disabled={loading || !commentText.trim()}
style={{
padding: '0.5rem 1rem',
background: loading || !commentText.trim() ? '#9ca3af' : '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: loading || !commentText.trim() ? 'not-allowed' : 'pointer',
fontWeight: '500',
fontSize: '0.875rem'
}}
>
{loading ? 'Poste...' : 'Kommentar hinzufügen'}
</button>
<button
onClick={reloadForum}
style={{
padding: '0.5rem 1rem',
background: '#6b7280',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: 'pointer',
fontWeight: '500',
fontSize: '0.875rem'
}}
>
🔄 Forum neu laden
</button>
</div>
</div>
{/* Learning Tip */}
<div style={{
marginTop: '1rem',
padding: '1rem',
background: '#eff6ff',
border: '1px solid #3b82f6',
borderRadius: '0.375rem',
fontSize: '0.875rem'
}}>
<div style={{ fontWeight: '600', color: '#1e40af', marginBottom: '0.5rem' }}>
💡 Lerntipp
</div>
<div style={{ color: '#1e3a8a' }}>
Beachten Sie, wie eingeschleuste Skripte erkannt und sicher angezeigt werden. In einem echten anfälligen Forum würden diese Skripte für jeden Benutzer ausgeführt, der den Kommentar ansieht!
</div>
</div>
{/* Learning Resources */}
<div style={{
marginTop: '1rem',
padding: '1rem',
background: '#f0f9ff',
border: '1px solid #0ea5e9',
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '600', color: '#0c4a6e', marginBottom: '0.75rem' }}>
📚 Lernressourcen
</div>
<div style={{ fontSize: '0.875rem', color: '#075985', lineHeight: '1.8' }}>
<div style={{ marginBottom: '0.5rem' }}>
<strong>HTML-Elemente:</strong>
</div>
<ul style={{ margin: '0 0 1rem 1.5rem' }}>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Element/script" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;script&gt; Tag
</a> - Führt JavaScript-Code aus
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Element/img" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;img&gt; Tag
</a> - Kann mit onerror Event-Handler missbraucht werden
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/SVG/Element/svg" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;svg&gt; Tag
</a> - Kann onload Event-Handler enthalten
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Element/iframe" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;iframe&gt; Tag
</a> - Lädt externe Inhalte
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Element/object" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;object&gt; Tag
</a> - Bettet externe Ressourcen ein
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Element/embed" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;embed&gt; Tag
</a> - Bettet Plugins ein
</li>
</ul>
<div style={{ marginBottom: '0.5rem' }}>
<strong>JavaScript & Event-Handler:</strong>
</div>
<ul style={{ margin: '0 0 0 1.5rem' }}>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/API/Window/alert" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
alert() Funktion
</a> - Zeigt Dialogbox an (oft für XSS-Tests genutzt)
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Attributes#event_handler_attributes" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
Event-Handler
</a> - onclick, onerror, onload, etc.
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/URI/Schemes/javascript" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
javascript: Protokoll
</a> - Führt JS-Code in URLs aus
</li>
</ul>
</div>
</div>
</div>
);
};
export default ForumScriptDemo;