596 lines
21 KiB
JavaScript
596 lines
21 KiB
JavaScript
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' }}>
|
||
<script> 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' }}>
|
||
<img> 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' }}>
|
||
<svg> 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' }}>
|
||
<iframe> 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' }}>
|
||
<object> 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' }}>
|
||
<embed> 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;
|