Add lessons
This commit is contained in:
Generated
+1175
-6
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,11 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.20.1",
|
||||
"axios": "^1.6.2"
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^6.20.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { participantAPI } from '../services/api.service';
|
||||
|
||||
const EventComments = ({ eventId, onScoreUpdate }) => {
|
||||
const [comments, setComments] = useState([]);
|
||||
const [commentText, setCommentText] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showCongratsModal, setShowCongratsModal] = useState(false);
|
||||
const [congratsData, setCongratsData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadComments();
|
||||
}, [eventId]);
|
||||
|
||||
const loadComments = async () => {
|
||||
try {
|
||||
const response = await participantAPI.getEventComments(eventId);
|
||||
setComments(response.data.data);
|
||||
} catch (err) {
|
||||
console.error('Failed to load comments:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!commentText.trim() || loading) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await participantAPI.addEventComment(eventId, commentText);
|
||||
|
||||
// Add comment to list
|
||||
setComments([response.data.data.comment, ...comments]);
|
||||
setCommentText('');
|
||||
|
||||
// Check if jackpot was discovered
|
||||
if (response.data.data.jackpotDiscovered) {
|
||||
setCongratsData({
|
||||
message: response.data.data.congratsMessage,
|
||||
bonusPoints: response.data.data.bonusPoints,
|
||||
xssType: response.data.data.xssType // Track if this is XSS discovery
|
||||
});
|
||||
setShowCongratsModal(true);
|
||||
}
|
||||
|
||||
// Notify parent of score update
|
||||
if (onScoreUpdate && response.data.data.totalScore !== undefined) {
|
||||
onScoreUpdate(response.data.data.totalScore);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to add comment:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeCongratsModal = () => {
|
||||
setShowCongratsModal(false);
|
||||
setCongratsData(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="card" style={{ marginBottom: '2rem' }}>
|
||||
<h3 style={{ marginBottom: '0.5rem' }}>Feedback</h3>
|
||||
<p style={{ fontSize: '0.875rem', color: '#6b7280', marginBottom: '1rem' }}>
|
||||
Share your thoughts on the event
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit} style={{ marginBottom: '1.5rem' }}>
|
||||
<textarea
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
placeholder="Your feedback on this event..."
|
||||
rows={3}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.75rem',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '0.375rem',
|
||||
fontSize: '0.875rem',
|
||||
fontFamily: 'inherit',
|
||||
marginBottom: '0.5rem',
|
||||
resize: 'vertical'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!commentText.trim() || loading}
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
background: loading || !commentText.trim() ? '#9ca3af' : '#2563eb',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.375rem',
|
||||
cursor: loading || !commentText.trim() ? 'not-allowed' : 'pointer',
|
||||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
{loading ? 'Submitting...' : 'Submit Feedback'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{comments.length > 0 && (
|
||||
<div>
|
||||
<div style={{
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
marginBottom: '0.75rem'
|
||||
}}>
|
||||
Your Feedback ({comments.length})
|
||||
</div>
|
||||
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
||||
{comments.map((comment) => (
|
||||
<div
|
||||
key={comment.id}
|
||||
style={{
|
||||
marginBottom: '0.75rem',
|
||||
paddingBottom: '0.75rem',
|
||||
borderBottom: '1px solid #e5e7eb'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
fontSize: '0.75rem',
|
||||
color: '#6b7280',
|
||||
marginBottom: '0.25rem'
|
||||
}}>
|
||||
{new Date(comment.created_at).toLocaleString()}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '0.875rem',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word'
|
||||
}}>
|
||||
{comment.content}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{comments.length === 0 && (
|
||||
<div style={{
|
||||
fontSize: '0.875rem',
|
||||
color: '#9ca3af',
|
||||
textAlign: 'center',
|
||||
padding: '1rem'
|
||||
}}>
|
||||
No feedback yet
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Congratulations Modal */}
|
||||
{showCongratsModal && congratsData && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 9999
|
||||
}}
|
||||
onClick={closeCongratsModal}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: 'white',
|
||||
padding: '2rem',
|
||||
borderRadius: '0.5rem',
|
||||
maxWidth: '500px',
|
||||
textAlign: 'center',
|
||||
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.3)'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div style={{ fontSize: '3rem', marginBottom: '1rem' }}>
|
||||
{congratsData.xssType ? '🎯' : '🎰'}
|
||||
</div>
|
||||
<h2 style={{ color: '#10b981', marginBottom: '1rem', fontSize: '1.875rem' }}>
|
||||
{congratsData.xssType ? 'XSS Discovered!' : 'Jackpot!'}
|
||||
</h2>
|
||||
<p style={{ marginBottom: '1rem', color: '#374151', lineHeight: '1.6' }}>
|
||||
{congratsData.message}
|
||||
</p>
|
||||
<div style={{
|
||||
fontSize: '2rem',
|
||||
fontWeight: 'bold',
|
||||
color: '#2563eb',
|
||||
marginBottom: '1.5rem'
|
||||
}}>
|
||||
+{congratsData.bonusPoints} Points
|
||||
</div>
|
||||
<button
|
||||
onClick={closeCongratsModal}
|
||||
style={{
|
||||
padding: '0.75rem 2rem',
|
||||
background: '#2563eb',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.375rem',
|
||||
cursor: 'pointer',
|
||||
fontWeight: '600',
|
||||
fontSize: '1rem'
|
||||
}}
|
||||
>
|
||||
Awesome!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventComments;
|
||||
@@ -0,0 +1,217 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { participantAPI } from '../services/api.service';
|
||||
|
||||
const EventLeaderboard = ({ eventId }) => {
|
||||
const [leaderboard, setLeaderboard] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const [filterText, setFilterText] = useState('');
|
||||
const [showJackpotModal, setShowJackpotModal] = useState(false);
|
||||
const [jackpotData, setJackpotData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadLeaderboard();
|
||||
}, [eventId]);
|
||||
|
||||
// Reload when filter changes (with debounce)
|
||||
useEffect(() => {
|
||||
const debounce = setTimeout(() => {
|
||||
loadLeaderboard();
|
||||
}, 300);
|
||||
return () => clearTimeout(debounce);
|
||||
}, [filterText]);
|
||||
|
||||
const loadLeaderboard = async () => {
|
||||
try {
|
||||
const response = await participantAPI.getEventLeaderboard(eventId, filterText);
|
||||
const data = response.data.data;
|
||||
|
||||
// Check for SQL injection jackpot
|
||||
if (data.jackpotDiscovered) {
|
||||
setJackpotData({
|
||||
message: data.congratsMessage,
|
||||
bonusPoints: data.bonusPoints,
|
||||
totalScore: data.totalScore
|
||||
});
|
||||
setShowJackpotModal(true);
|
||||
}
|
||||
|
||||
// DEBUG: Log query metadata (TODO: remove before production)
|
||||
if (data._metadata) {
|
||||
console.log('[Leaderboard] Query executed:', {
|
||||
source: data._metadata.query_source,
|
||||
scoreColumn: data._metadata.score_column,
|
||||
aggregation: data._metadata.aggregation_function,
|
||||
join: data._metadata.table_join,
|
||||
filter: data._metadata.filter_applied
|
||||
});
|
||||
}
|
||||
|
||||
setLeaderboard(data.rankings || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load leaderboard:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card" style={{ marginBottom: '2rem' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
>
|
||||
<h3 style={{ margin: 0 }}>🏆 Top Participants</h3>
|
||||
<span style={{ fontSize: '1.25rem', color: '#6b7280' }}>
|
||||
{collapsed ? '▼' : '▲'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{!collapsed && (
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
{/* Search/Filter Input */}
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
placeholder="Search by participant name..."
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.75rem',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '0.375rem',
|
||||
fontSize: '0.875rem',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
/>
|
||||
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginTop: '0.25rem' }}>
|
||||
💡 Tip: Try searching for different participants to filter results
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{leaderboard.length === 0 ? (
|
||||
<p style={{ color: '#6b7280', fontSize: '0.875rem' }}>
|
||||
{filterText ? `No participants found matching "${filterText}"` : 'No participants yet. Be the first to complete lessons!'}
|
||||
</p>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
{leaderboard.map((entry, index) => (
|
||||
<div
|
||||
key={entry.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0.75rem',
|
||||
background: index < 3 ? '#fef3c7' : '#f9fafb',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '0.375rem'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: '2rem',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '1.25rem',
|
||||
color: index === 0 ? '#f59e0b' : index === 1 ? '#9ca3af' : index === 2 ? '#c2410c' : '#6b7280'
|
||||
}}>
|
||||
{index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : `${index + 1}.`}
|
||||
</div>
|
||||
<div style={{ flex: 1, marginLeft: '1rem' }}>
|
||||
<div style={{ fontWeight: '600', color: '#1f2937' }}>
|
||||
{entry.pseudonym}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#6b7280' }}>
|
||||
{entry.lessons_completed} lesson{entry.lessons_completed !== 1 ? 's' : ''} completed
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 'bold', color: '#2563eb' }}>
|
||||
{entry.total_score}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#6b7280' }}>
|
||||
points
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Jackpot Modal */}
|
||||
{showJackpotModal && jackpotData && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 9999
|
||||
}}>
|
||||
<div style={{
|
||||
background: 'white',
|
||||
padding: '2rem',
|
||||
borderRadius: '0.5rem',
|
||||
maxWidth: '500px',
|
||||
textAlign: 'center',
|
||||
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)'
|
||||
}}>
|
||||
<div style={{ fontSize: '3rem', marginBottom: '1rem' }}>🎰</div>
|
||||
<h2 style={{ color: '#10b981', marginBottom: '1rem', fontSize: '1.875rem' }}>
|
||||
SQL Injection Jackpot!
|
||||
</h2>
|
||||
<p style={{ color: '#4b5563', marginBottom: '1.5rem', lineHeight: '1.6' }}>
|
||||
{jackpotData.message}
|
||||
</p>
|
||||
<div style={{
|
||||
fontSize: '2rem',
|
||||
fontWeight: 'bold',
|
||||
color: '#2563eb',
|
||||
marginBottom: '0.5rem'
|
||||
}}>
|
||||
+{jackpotData.bonusPoints} Points
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#6b7280', marginBottom: '1.5rem' }}>
|
||||
New Total: {jackpotData.totalScore} points
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowJackpotModal(false);
|
||||
// Reload to show updated leaderboard
|
||||
window.location.reload();
|
||||
}}
|
||||
style={{
|
||||
padding: '0.75rem 2rem',
|
||||
background: '#10b981',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.375rem',
|
||||
fontSize: '1rem',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Awesome!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventLeaderboard;
|
||||
@@ -109,12 +109,12 @@ const BitBDemo = ({ lessonData }) => {
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', color: '#5f6368', marginBottom: '24px' }}>
|
||||
Sign in to continue
|
||||
Anmelden, um fortzufahren
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email or phone"
|
||||
placeholder="E-Mail oder Telefon"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
@@ -127,7 +127,7 @@ const BitBDemo = ({ lessonData }) => {
|
||||
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
placeholder="Passwort"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
@@ -151,7 +151,7 @@ const BitBDemo = ({ lessonData }) => {
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Sign in
|
||||
Anmelden
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -171,7 +171,7 @@ const BitBDemo = ({ lessonData }) => {
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.2)'
|
||||
}}
|
||||
>
|
||||
{scenario.isReal ? '✅ REAL' : '⚠️ FAKE'}
|
||||
{scenario.isReal ? '✅ ECHT' : '⚠️ FAKE'}
|
||||
</div>
|
||||
|
||||
{/* Close Button */}
|
||||
@@ -192,7 +192,7 @@ const BitBDemo = ({ lessonData }) => {
|
||||
zIndex: 10000
|
||||
}}
|
||||
>
|
||||
Close Demo
|
||||
Demo schließen
|
||||
</button>
|
||||
|
||||
{/* Feedback Messages */}
|
||||
@@ -211,7 +211,7 @@ const BitBDemo = ({ lessonData }) => {
|
||||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
⚠️ Notice: This fake window cannot be dragged!
|
||||
⚠️ Hinweis: Dieses gefälschte Fenster kann nicht gezogen werden!
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -232,7 +232,7 @@ const BitBDemo = ({ lessonData }) => {
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
⚠️ Right-click works! This reveals it's just HTML, not a real browser.
|
||||
⚠️ Rechtsklick funktioniert! Das zeigt, dass es nur HTML ist, kein echter Browser.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -246,7 +246,7 @@ const BitBDemo = ({ lessonData }) => {
|
||||
{/* Test Instructions */}
|
||||
<div style={{ marginBottom: '1.5rem', padding: '1rem', background: '#eff6ff', border: '1px solid #3b82f6', borderRadius: '0.375rem' }}>
|
||||
<div style={{ fontWeight: '600', color: '#1e40af', marginBottom: '0.5rem' }}>
|
||||
🔍 How to Test:
|
||||
🔍 So testen Sie:
|
||||
</div>
|
||||
<ul style={{ margin: 0, paddingLeft: '1.5rem', fontSize: '0.875rem', color: '#1e3a8a' }}>
|
||||
{testInstructions.map((instruction, idx) => (
|
||||
@@ -291,12 +291,12 @@ const BitBDemo = ({ lessonData }) => {
|
||||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
Launch {scenario.provider} Login
|
||||
{scenario.provider}-Login starten
|
||||
</button>
|
||||
|
||||
{/* Indicators */}
|
||||
<div style={{ marginTop: '1rem', fontSize: '0.75rem', color: '#6b7280' }}>
|
||||
<div style={{ fontWeight: '600', marginBottom: '0.25rem' }}>Key Indicators:</div>
|
||||
<div style={{ fontWeight: '600', marginBottom: '0.25rem' }}>Wichtige Indikatoren:</div>
|
||||
<ul style={{ margin: 0, paddingLeft: '1.25rem' }}>
|
||||
{scenario.indicators.slice(0, 3).map((indicator, idx) => (
|
||||
<li key={idx} style={{ marginBottom: '0.125rem' }}>{indicator}</li>
|
||||
@@ -310,12 +310,12 @@ const BitBDemo = ({ lessonData }) => {
|
||||
{/* Educational Note */}
|
||||
<div style={{ padding: '1rem', background: '#fef3c7', border: '1px solid #f59e0b', borderRadius: '0.375rem', fontSize: '0.875rem' }}>
|
||||
<div style={{ fontWeight: '600', color: '#92400e', marginBottom: '0.5rem' }}>
|
||||
⚠️ Important
|
||||
⚠️ Wichtig
|
||||
</div>
|
||||
<div style={{ color: '#78350f' }}>
|
||||
In a real Browser-in-the-Browser attack, the fake popup would look identical to the real one.
|
||||
The ONLY reliable way to detect it is by testing the physical behavior: Can you drag it outside the browser window?
|
||||
Can you right-click the address bar and inspect it as HTML?
|
||||
Bei einem echten Browser-in-the-Browser-Angriff würde das gefälschte Popup identisch zum echten aussehen.
|
||||
Die EINZIGE zuverlässige Methode zur Erkennung besteht darin, das physische Verhalten zu testen: Können Sie es über das Browserfenster hinaus ziehen?
|
||||
Können Sie mit der rechten Maustaste auf die Adressleiste klicken und sie als HTML untersuchen?
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -323,7 +323,7 @@ const BitBDemo = ({ lessonData }) => {
|
||||
{interactiveData.realWorldExamples && (
|
||||
<div style={{ marginTop: '1.5rem', padding: '1rem', background: '#f9fafb', borderRadius: '0.375rem' }}>
|
||||
<div style={{ fontWeight: '600', fontSize: '0.875rem', marginBottom: '0.75rem', color: '#1f2937' }}>
|
||||
📰 Real-World BitB Attacks:
|
||||
📰 BitB-Angriffe aus der Praxis:
|
||||
</div>
|
||||
<div style={{ display: 'grid', gap: '0.5rem' }}>
|
||||
{interactiveData.realWorldExamples.map((example, idx) => (
|
||||
|
||||
@@ -0,0 +1,595 @@
|
||||
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;
|
||||
@@ -0,0 +1,582 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { participantAPI } from '../../../services/api.service';
|
||||
|
||||
const IDORDemo = ({ lessonData, eventLessonId }) => {
|
||||
const interactiveData = lessonData?.interactiveData || {};
|
||||
const baseUrl = interactiveData.baseUrl || 'https://securebank.example/profile';
|
||||
const currentUserId = interactiveData.currentUserId || 55;
|
||||
const easterEggsData = interactiveData.easterEggs || [];
|
||||
|
||||
const defaultUrl = `${baseUrl}?ref=dashboard&userId=${currentUserId}`;
|
||||
const [fullUrl, setFullUrl] = useState(defaultUrl);
|
||||
const [profile, setProfile] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [timeLeft, setTimeLeft] = useState(300); // 5 minutes = 300 seconds
|
||||
const [timerActive, setTimerActive] = useState(false);
|
||||
const [discoveries, setDiscoveries] = useState(new Set());
|
||||
|
||||
// Timer countdown
|
||||
useEffect(() => {
|
||||
if (!timerActive) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setTimeLeft(prev => {
|
||||
if (prev <= 1) {
|
||||
setTimerActive(false);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [timerActive]);
|
||||
|
||||
// Start timer on first interaction
|
||||
useEffect(() => {
|
||||
if (!timerActive && fullUrl !== defaultUrl) {
|
||||
setTimerActive(true);
|
||||
}
|
||||
}, [fullUrl, timerActive, defaultUrl]);
|
||||
|
||||
const formatTime = (seconds) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const parseUserIdFromUrl = (url) => {
|
||||
try {
|
||||
const match = url.match(/[?&]userId=(\d+)/);
|
||||
return match ? parseInt(match[1]) : null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const resetUrl = () => {
|
||||
setFullUrl(defaultUrl);
|
||||
setProfile(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const fetchProfile = async () => {
|
||||
const userId = parseUserIdFromUrl(fullUrl);
|
||||
|
||||
if (!userId) {
|
||||
setError('Ungültige URL - userId-Parameter fehlt oder ist ungültig');
|
||||
setProfile(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await participantAPI.executeLessonAction(
|
||||
eventLessonId,
|
||||
'fetch-profile',
|
||||
{ userId: userId }
|
||||
);
|
||||
|
||||
const data = response.data.data;
|
||||
|
||||
if (data.success) {
|
||||
setProfile(data);
|
||||
|
||||
// Track discoveries
|
||||
if (data.isUnauthorized) {
|
||||
setDiscoveries(prev => new Set([...prev, userId]));
|
||||
}
|
||||
} else {
|
||||
setError(data.message || 'Benutzer nicht gefunden');
|
||||
setProfile(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profile:', error);
|
||||
setError('Fehler beim Laden des Benutzerprofils');
|
||||
setProfile(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const totalEasterEggs = easterEggsData.length;
|
||||
const foundEasterEggs = easterEggsData.filter(egg => discoveries.has(egg.id)).length;
|
||||
|
||||
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 Bildungszwecken
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#78350f', marginTop: '0.5rem' }}>
|
||||
Dies demonstriert eine IDOR-Schwachstelle, bei der Sie auf private Daten anderer Benutzer zugreifen können, indem Sie den URL-Parameter ändern. Dies ist eine sichere Simulation mit Beispieldaten.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timer and Discovery Tracker */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '1rem',
|
||||
marginBottom: '1.5rem',
|
||||
flexWrap: 'wrap'
|
||||
}}>
|
||||
{/* Timer */}
|
||||
<div style={{
|
||||
flex: '1',
|
||||
minWidth: '150px',
|
||||
padding: '1rem',
|
||||
background: timeLeft < 60 ? '#fee2e2' : '#f0fdf4',
|
||||
border: `2px solid ${timeLeft < 60 ? '#ef4444' : '#22c55e'}`,
|
||||
borderRadius: '0.375rem'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: '600',
|
||||
color: timeLeft < 60 ? '#991b1b' : '#166534',
|
||||
marginBottom: '0.25rem'
|
||||
}}>
|
||||
⏱️ Verbleibende Zeit
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 'bold',
|
||||
color: timeLeft < 60 ? '#dc2626' : '#16a34a'
|
||||
}}>
|
||||
{formatTime(timeLeft)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Discovery Tracker */}
|
||||
<div style={{
|
||||
flex: '1',
|
||||
minWidth: '150px',
|
||||
padding: '1rem',
|
||||
background: '#eff6ff',
|
||||
border: '2px solid #3b82f6',
|
||||
borderRadius: '0.375rem'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: '600',
|
||||
color: '#1e40af',
|
||||
marginBottom: '0.25rem'
|
||||
}}>
|
||||
🎯 Entdeckungen
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 'bold',
|
||||
color: '#2563eb'
|
||||
}}>
|
||||
{foundEasterEggs} / {totalEasterEggs}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '0.75rem',
|
||||
color: '#1e40af',
|
||||
marginTop: '0.25rem'
|
||||
}}>
|
||||
Gefundene Benutzer: {discoveries.size}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style={{ marginBottom: '1rem', color: '#1f2937' }}>🏦 IDOR-Schwachstellen-Demo</h4>
|
||||
|
||||
{/* Browser Mockup */}
|
||||
<div style={{
|
||||
border: '2px solid #d1d5db',
|
||||
borderRadius: '0.5rem',
|
||||
overflow: 'hidden',
|
||||
background: 'white',
|
||||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{/* Browser Chrome */}
|
||||
<div style={{
|
||||
background: '#f1f3f4',
|
||||
padding: '0.75rem',
|
||||
borderBottom: '1px solid #dadce0',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
{/* Window Controls */}
|
||||
<div style={{ display: 'flex', gap: '0.5rem', marginRight: '1rem' }}>
|
||||
<div style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '50%',
|
||||
background: '#ff5f56'
|
||||
}}></div>
|
||||
<div style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '50%',
|
||||
background: '#ffbd2e'
|
||||
}}></div>
|
||||
<div style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '50%',
|
||||
background: '#27c93f'
|
||||
}}></div>
|
||||
</div>
|
||||
|
||||
{/* Address Bar */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
background: 'white',
|
||||
border: '1px solid #dadce0',
|
||||
borderRadius: '1rem',
|
||||
padding: '0.5rem 1rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: '0.875rem',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
<span style={{ marginRight: '0.5rem', color: '#5f6368' }}>🔒</span>
|
||||
<input
|
||||
type="text"
|
||||
value={fullUrl}
|
||||
onChange={(e) => setFullUrl(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && fetchProfile()}
|
||||
style={{
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
flex: 1,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.875rem',
|
||||
background: 'transparent',
|
||||
padding: '0.125rem 0.25rem',
|
||||
borderRadius: '0.125rem',
|
||||
color: '#202124'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Reset Button */}
|
||||
<button
|
||||
onClick={resetUrl}
|
||||
disabled={loading}
|
||||
style={{
|
||||
marginLeft: '0.5rem',
|
||||
padding: '0.5rem 0.75rem',
|
||||
background: 'white',
|
||||
color: '#5f6368',
|
||||
border: '1px solid #dadce0',
|
||||
borderRadius: '0.375rem',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: '500',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.25rem'
|
||||
}}
|
||||
title="URL zurücksetzen"
|
||||
>
|
||||
↺
|
||||
</button>
|
||||
|
||||
{/* Navigate Button */}
|
||||
<button
|
||||
onClick={() => fetchProfile()}
|
||||
disabled={loading}
|
||||
style={{
|
||||
marginLeft: '0.25rem',
|
||||
padding: '0.5rem 1rem',
|
||||
background: loading ? '#9ca3af' : '#3b82f6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.375rem',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
{loading ? '⏳' : '→'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* DEMO BROWSER Label */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '0.5rem',
|
||||
right: '0.5rem',
|
||||
background: '#fee2e2',
|
||||
color: '#991b1b',
|
||||
padding: '0.25rem 0.5rem',
|
||||
borderRadius: '0.25rem',
|
||||
fontSize: '0.625rem',
|
||||
fontWeight: '600',
|
||||
zIndex: 10
|
||||
}}>
|
||||
DEMO BROWSER
|
||||
</div>
|
||||
|
||||
{/* Browser Content */}
|
||||
<div style={{ padding: '2rem', minHeight: '400px', background: '#f9fafb' }}>
|
||||
{loading && (
|
||||
<div style={{ textAlign: 'center', padding: '2rem', color: '#6b7280' }}>
|
||||
<div style={{ fontSize: '2rem', marginBottom: '0.5rem' }}>⏳</div>
|
||||
Profil wird geladen...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && !loading && (
|
||||
<div style={{
|
||||
padding: '1.5rem',
|
||||
background: '#fee2e2',
|
||||
border: '2px solid #ef4444',
|
||||
borderRadius: '0.375rem',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div style={{ fontSize: '2rem', marginBottom: '0.5rem' }}>❌</div>
|
||||
<div style={{ fontWeight: '600', color: '#991b1b', marginBottom: '0.5rem' }}>
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{profile && profile.user && !loading && (
|
||||
<>
|
||||
{/* Easter Egg Message */}
|
||||
{profile.easterEgg && (
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
background: '#f0fdf4',
|
||||
border: '3px solid #22c55e',
|
||||
borderRadius: '0.375rem',
|
||||
marginBottom: '1.5rem',
|
||||
textAlign: 'center',
|
||||
animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '2rem',
|
||||
marginBottom: '0.5rem'
|
||||
}}>
|
||||
{profile.easterEgg.type === 'admin' && '🎯'}
|
||||
{profile.easterEgg.type === 'millionaire' && '💰'}
|
||||
{profile.easterEgg.type === 'shrug' && '¯\\_(ツ)_/¯'}
|
||||
</div>
|
||||
<div style={{
|
||||
fontWeight: '600',
|
||||
color: '#166534',
|
||||
fontSize: '1rem',
|
||||
marginBottom: '0.5rem'
|
||||
}}>
|
||||
{profile.easterEgg.message}
|
||||
</div>
|
||||
{profile.pointsAwarded && (
|
||||
<div style={{
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 'bold',
|
||||
color: '#16a34a'
|
||||
}}>
|
||||
+{profile.pointsAwarded} Punkte
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* IDOR Vulnerability Warning */}
|
||||
{profile.isUnauthorized && profile.vulnerability && (
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
background: profile.vulnerability.severity === 'CRITICAL' ? '#7f1d1d' : '#fee2e2',
|
||||
border: `3px solid ${profile.vulnerability.severity === 'CRITICAL' ? '#991b1b' : '#ef4444'}`,
|
||||
borderRadius: '0.375rem',
|
||||
marginBottom: '1.5rem',
|
||||
animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite'
|
||||
}}>
|
||||
<div style={{
|
||||
fontWeight: '600',
|
||||
color: profile.vulnerability.severity === 'CRITICAL' ? 'white' : '#991b1b',
|
||||
fontSize: '1rem',
|
||||
marginBottom: '0.5rem'
|
||||
}}>
|
||||
{profile.vulnerability.description}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '0.875rem',
|
||||
color: profile.vulnerability.severity === 'CRITICAL' ? '#fecaca' : '#7f1d1d',
|
||||
marginBottom: '0.5rem'
|
||||
}}>
|
||||
{profile.vulnerability.message}
|
||||
</div>
|
||||
{profile.pointsAwarded && !profile.easterEgg && (
|
||||
<div style={{
|
||||
fontSize: '0.875rem',
|
||||
color: profile.vulnerability.severity === 'CRITICAL' ? '#86efac' : '#16a34a',
|
||||
fontWeight: '600',
|
||||
marginTop: '0.5rem'
|
||||
}}>
|
||||
+{profile.pointsAwarded} Punkte für IDOR-Entdeckung
|
||||
</div>
|
||||
)}
|
||||
<div style={{
|
||||
fontSize: '0.75rem',
|
||||
color: profile.vulnerability.severity === 'CRITICAL' ? '#fca5a5' : '#991b1b',
|
||||
padding: '0.5rem',
|
||||
background: profile.vulnerability.severity === 'CRITICAL' ? 'rgba(0,0,0,0.2)' : '#fef2f2',
|
||||
borderRadius: '0.25rem',
|
||||
marginTop: '0.5rem'
|
||||
}}>
|
||||
<strong>Auswirkung:</strong> {profile.vulnerability.impact}
|
||||
</div>
|
||||
{profile.vulnerability.cve && (
|
||||
<div style={{
|
||||
fontSize: '0.75rem',
|
||||
color: 'white',
|
||||
marginTop: '0.5rem',
|
||||
padding: '0.5rem',
|
||||
background: '#dc2626',
|
||||
borderRadius: '0.25rem',
|
||||
fontWeight: '600'
|
||||
}}>
|
||||
🚨 {profile.vulnerability.cve}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* User Profile Card */}
|
||||
<div style={{
|
||||
background: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '0.5rem',
|
||||
padding: '2rem'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: '1.5rem'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '4rem',
|
||||
height: '4rem',
|
||||
borderRadius: '50%',
|
||||
background: profile.isCurrentUser ? '#3b82f6' : '#9ca3af',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: '600',
|
||||
marginRight: '1rem'
|
||||
}}>
|
||||
{profile.user.name.charAt(0)}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
|
||||
<h3 style={{ margin: 0, color: '#1f2937' }}>{profile.user.name}</h3>
|
||||
{profile.isCurrentUser && (
|
||||
<span style={{
|
||||
background: '#dbeafe',
|
||||
color: '#1e40af',
|
||||
padding: '0.125rem 0.5rem',
|
||||
borderRadius: '0.25rem',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
Sie
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ color: '#6b7280', fontSize: '0.875rem' }}>
|
||||
{profile.user.email}
|
||||
</div>
|
||||
<div style={{ color: '#9ca3af', fontSize: '0.75rem', marginTop: '0.25rem' }}>
|
||||
{profile.user.accountType}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Account Details */}
|
||||
<div style={{
|
||||
background: '#f9fafb',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '0.375rem',
|
||||
padding: '1rem'
|
||||
}}>
|
||||
<div style={{ fontSize: '0.875rem', fontWeight: '600', marginBottom: '1rem', color: '#374151' }}>
|
||||
Kontodetails
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginBottom: '0.25rem' }}>
|
||||
Kontostand
|
||||
</div>
|
||||
<div style={{ fontSize: '1.125rem', fontWeight: '600', color: '#1f2937' }}>
|
||||
{profile.user.accountBalance}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginBottom: '0.25rem' }}>
|
||||
Kontonummer
|
||||
</div>
|
||||
<div style={{ fontSize: '1.125rem', fontWeight: '600', color: '#1f2937', fontFamily: 'monospace' }}>
|
||||
{profile.user.accountNumber}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginBottom: '0.25rem' }}>
|
||||
Telefonnummer
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#374151' }}>
|
||||
{profile.user.phone}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginBottom: '0.25rem' }}>
|
||||
Letzte Anmeldung
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#374151' }}>
|
||||
{profile.user.lastLogin}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: '1rem', paddingTop: '1rem', borderTop: '1px solid #e5e7eb' }}>
|
||||
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginBottom: '0.25rem' }}>
|
||||
Adresse
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#374151' }}>
|
||||
{profile.user.address}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Learning Tip */}
|
||||
<div style={{
|
||||
marginTop: '1.5rem',
|
||||
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 Sie auf private Informationen anderer Benutzer zugreifen können, indem Sie einfach den userId-Parameter ändern. In einer sicheren Anwendung muss der Server überprüfen, ob Sie die Berechtigung haben, auf jede spezifische Ressource zuzugreifen!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IDORDemo;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { participantAPI } from '../../../services/api.service';
|
||||
|
||||
const SQLShopDemo = ({ lessonData, eventLessonId }) => {
|
||||
@@ -7,8 +7,102 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showSafeComparison, setShowSafeComparison] = useState(false);
|
||||
|
||||
// Timer state
|
||||
const [timerActive, setTimerActive] = useState(false);
|
||||
const [timeLeft, setTimeLeft] = useState(0);
|
||||
|
||||
// Discovery tracking
|
||||
const [discoveries, setDiscoveries] = useState(new Set());
|
||||
|
||||
// Hints
|
||||
const [currentHint, setCurrentHint] = useState(null);
|
||||
const [hintsUsed, setHintsUsed] = useState(0);
|
||||
|
||||
// UI state
|
||||
const [showResources, setShowResources] = useState(false);
|
||||
const [showSchema, setShowSchema] = useState(false);
|
||||
const [showChallenges, setShowChallenges] = useState(true);
|
||||
|
||||
const interactiveData = lessonData?.interactiveData || {};
|
||||
const examples = interactiveData.examples || [];
|
||||
const timerDuration = interactiveData.timerDuration || 600;
|
||||
const totalChallenges = interactiveData.totalChallenges || 5;
|
||||
const challenges = interactiveData.challenges || [];
|
||||
const externalResources = interactiveData.externalResources || [];
|
||||
const schemaInfo = interactiveData.schemaInfo || {};
|
||||
|
||||
// Auto-start timer on mount
|
||||
useEffect(() => {
|
||||
if (eventLessonId && !timerActive && timeLeft === 0) {
|
||||
startTimer();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [eventLessonId]);
|
||||
|
||||
// Timer countdown
|
||||
useEffect(() => {
|
||||
if (timerActive && timeLeft > 0) {
|
||||
const timer = setInterval(() => {
|
||||
setTimeLeft(prev => {
|
||||
if (prev <= 1) {
|
||||
setTimerActive(false);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
}
|
||||
}, [timerActive, timeLeft]);
|
||||
|
||||
const startTimer = async () => {
|
||||
try {
|
||||
const response = await participantAPI.executeLessonAction(
|
||||
eventLessonId,
|
||||
'start-timer',
|
||||
{}
|
||||
);
|
||||
const result = response.data.data;
|
||||
if (result.started) {
|
||||
setTimerActive(true);
|
||||
setTimeLeft(result.duration);
|
||||
|
||||
// Update discoveries from timer response
|
||||
if (result.discoveries && result.discoveries.types) {
|
||||
setDiscoveries(new Set(result.discoveries.types));
|
||||
}
|
||||
|
||||
// Show UNION hint if it was previously unlocked
|
||||
if (result.unionHintShown) {
|
||||
setCurrentHint({
|
||||
title: '🎯 Neue Herausforderung freigeschaltet!',
|
||||
content: 'Du kannst jetzt versuchen, Daten aus anderen Tabellen zu extrahieren! Die Datenbank hat eine "users" Tabelle mit den Spalten: id, username, password, role. Verwende UNION SELECT um diese Daten zu kombinieren. Die Anzahl der Spalten muss übereinstimmen (5 Spalten).',
|
||||
hint: "Versuche: ' UNION SELECT id, username, password, role, 'X' FROM users--"
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to start timer:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getHint = async () => {
|
||||
try {
|
||||
const response = await participantAPI.executeLessonAction(
|
||||
eventLessonId,
|
||||
'get-hint',
|
||||
{}
|
||||
);
|
||||
const hintData = response.data.data;
|
||||
if (hintData.available) {
|
||||
setCurrentHint(hintData.hint);
|
||||
setHintsUsed(hintData.totalHintsUsed);
|
||||
} else {
|
||||
setCurrentHint({ message: hintData.message });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get hint:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const executeSearch = async (term = searchTerm) => {
|
||||
if (!term.trim()) return;
|
||||
@@ -20,13 +114,24 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
|
||||
'execute-query',
|
||||
{ searchTerm: term, mode: 'vulnerable' }
|
||||
);
|
||||
setQueryResult(response.data.data);
|
||||
const result = response.data.data;
|
||||
setQueryResult(result);
|
||||
|
||||
// Update discoveries
|
||||
if (result.discoveries && result.discoveries.types) {
|
||||
setDiscoveries(new Set(result.discoveries.types));
|
||||
}
|
||||
|
||||
// Show UNION hint if unlocked
|
||||
if (result.unionHintMessage) {
|
||||
setCurrentHint(result.unionHintMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to execute query:', error);
|
||||
setQueryResult({
|
||||
query: 'Error executing query',
|
||||
query: 'Fehler beim Ausführen der Abfrage',
|
||||
results: [],
|
||||
explanation: 'Failed to execute search'
|
||||
explanation: 'Suche konnte nicht ausgeführt werden'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -52,60 +157,276 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadExample = (example) => {
|
||||
setSearchTerm(example.input);
|
||||
setShowSafeComparison(false);
|
||||
const formatTime = (seconds) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const getDifficultyColor = (difficulty) => {
|
||||
switch (difficulty) {
|
||||
case 'Anfänger': return '#10b981';
|
||||
case 'Mittel': return '#f59e0b';
|
||||
case 'Fortgeschritten': return '#ef4444';
|
||||
default: return '#6b7280';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ border: '1px solid #e5e7eb', borderRadius: '0.5rem', padding: '1.5rem', background: 'white' }}>
|
||||
<div style={{ marginBottom: '1.5rem' }}>
|
||||
<h4 style={{ marginBottom: '1rem', color: '#1f2937' }}>🛒 TechShop - Product Search</h4>
|
||||
<p style={{ fontSize: '0.875rem', color: '#6b7280', marginBottom: '1rem' }}>
|
||||
This is a vulnerable online shop. Try searching for products, then experiment with SQL injection.
|
||||
</p>
|
||||
{/* Header with Timer and Discovery Tracker */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem', flexWrap: 'wrap', gap: '1rem' }}>
|
||||
<h4 style={{ margin: 0, color: '#1f2937' }}>🛒 TechShop SQL Injection Challenge</h4>
|
||||
|
||||
{/* Example Queries */}
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<div style={{ fontSize: '0.875rem', fontWeight: '500', marginBottom: '0.5rem' }}>
|
||||
Try these examples:
|
||||
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
{/* Discovery Tracker */}
|
||||
<div style={{
|
||||
padding: '0.5rem 1rem',
|
||||
background: '#eff6ff',
|
||||
border: '2px solid #3b82f6',
|
||||
borderRadius: '0.5rem',
|
||||
fontWeight: '600',
|
||||
fontSize: '0.875rem',
|
||||
color: '#1e40af'
|
||||
}}>
|
||||
🎯 {discoveries.size}/{totalChallenges} Entdeckt
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
|
||||
{examples.map((example, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => loadExample(example)}
|
||||
style={{
|
||||
padding: '0.5rem 0.75rem',
|
||||
fontSize: '0.75rem',
|
||||
background: '#f3f4f6',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '0.375rem',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onMouseOver={(e) => {
|
||||
e.target.style.background = '#e5e7eb';
|
||||
}}
|
||||
onMouseOut={(e) => {
|
||||
e.target.style.background = '#f3f4f6';
|
||||
}}
|
||||
title={example.description}
|
||||
>
|
||||
{example.label}
|
||||
</button>
|
||||
|
||||
{/* Timer */}
|
||||
{!timerActive ? (
|
||||
<button
|
||||
onClick={startTimer}
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
background: '#10b981',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
fontWeight: '600',
|
||||
fontSize: '0.875rem'
|
||||
}}
|
||||
>
|
||||
⏱️ Timer starten (10 Min)
|
||||
</button>
|
||||
) : (
|
||||
<div style={{
|
||||
padding: '0.5rem 1rem',
|
||||
background: timeLeft < 60 ? '#fee2e2' : '#dcfce7',
|
||||
border: `2px solid ${timeLeft < 60 ? '#ef4444' : '#10b981'}`,
|
||||
borderRadius: '0.5rem',
|
||||
fontWeight: '600',
|
||||
fontSize: '0.875rem',
|
||||
color: timeLeft < 60 ? '#991b1b' : '#065f46'
|
||||
}}>
|
||||
⏱️ {formatTime(timeLeft)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Challenges Overview */}
|
||||
{showChallenges && challenges.length > 0 && (
|
||||
<div style={{ marginBottom: '1.5rem', padding: '1rem', background: '#f9fafb', borderRadius: '0.5rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
|
||||
<h5 style={{ margin: 0, fontSize: '0.875rem', fontWeight: '600' }}>🎮 Herausforderungen</h5>
|
||||
<button
|
||||
onClick={() => setShowChallenges(false)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '1.25rem' }}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '0.75rem' }}>
|
||||
{challenges.map((challenge) => {
|
||||
const isDiscovered = discoveries.has(challenge.id);
|
||||
return (
|
||||
<div
|
||||
key={challenge.id}
|
||||
style={{
|
||||
padding: '0.75rem',
|
||||
background: isDiscovered ? '#d1fae5' : 'white',
|
||||
border: `2px solid ${isDiscovered ? '#10b981' : '#e5e7eb'}`,
|
||||
borderRadius: '0.375rem',
|
||||
fontSize: '0.75rem'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.25rem' }}>
|
||||
<span style={{ fontWeight: '600' }}>
|
||||
{isDiscovered ? '✅' : '🔒'} {challenge.title}
|
||||
</span>
|
||||
{challenge.isEasterEgg && <span>⭐</span>}
|
||||
</div>
|
||||
<div style={{ color: getDifficultyColor(challenge.difficulty), fontWeight: '500', marginBottom: '0.25rem' }}>
|
||||
{challenge.difficulty} • {challenge.points} Punkte
|
||||
</div>
|
||||
{!isDiscovered && (
|
||||
<div style={{ color: '#6b7280', fontSize: '0.7rem' }}>
|
||||
{challenge.hint}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hint System */}
|
||||
<div style={{ marginBottom: '1.5rem' }}>
|
||||
<button
|
||||
onClick={getHint}
|
||||
disabled={hintsUsed >= 5}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
background: hintsUsed >= 5 ? '#9ca3af' : '#f59e0b',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: hintsUsed >= 5 ? 'not-allowed' : 'pointer',
|
||||
fontWeight: '500',
|
||||
fontSize: '0.875rem',
|
||||
marginRight: '0.5rem',
|
||||
opacity: hintsUsed >= 5 ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
💡 {hintsUsed >= 5 ? 'Alle Hinweise verwendet' : `Hinweis anfordern (${hintsUsed}/5 verwendet)`}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setShowSchema(!showSchema)}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
background: '#6366f1',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
fontWeight: '500',
|
||||
fontSize: '0.875rem',
|
||||
marginRight: '0.5rem'
|
||||
}}
|
||||
>
|
||||
📊 Datenbank-Schema {showSchema ? 'verbergen' : 'anzeigen'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setShowResources(!showResources)}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
background: '#8b5cf6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
fontWeight: '500',
|
||||
fontSize: '0.875rem'
|
||||
}}
|
||||
>
|
||||
📚 Ressourcen {showResources ? 'verbergen' : 'anzeigen'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Current Hint Display */}
|
||||
{currentHint && (
|
||||
<div style={{
|
||||
marginBottom: '1.5rem',
|
||||
padding: '1rem',
|
||||
background: '#fef3c7',
|
||||
border: '2px solid #f59e0b',
|
||||
borderRadius: '0.5rem'
|
||||
}}>
|
||||
<div style={{ fontWeight: '600', color: '#92400e', marginBottom: '0.5rem' }}>
|
||||
💡 {currentHint.title || 'Hinweis'}
|
||||
{currentHint.cost && <span style={{ float: 'right' }}>-{currentHint.cost} Punkte</span>}
|
||||
</div>
|
||||
<div style={{ color: '#78350f', fontSize: '0.875rem', marginBottom: '0.5rem' }}>
|
||||
{currentHint.content || currentHint.message}
|
||||
</div>
|
||||
{currentHint.resources && currentHint.resources.length > 0 && (
|
||||
<div style={{ marginTop: '0.5rem' }}>
|
||||
{currentHint.resources.map((resource, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
href={resource.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#0284c7', fontSize: '0.75rem', marginRight: '1rem' }}
|
||||
>
|
||||
📖 {resource.title} ↗
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Schema Information */}
|
||||
{showSchema && schemaInfo.tables && (
|
||||
<div style={{
|
||||
marginBottom: '1.5rem',
|
||||
padding: '1rem',
|
||||
background: '#f0f9ff',
|
||||
border: '2px solid #0ea5e9',
|
||||
borderRadius: '0.5rem'
|
||||
}}>
|
||||
<h5 style={{ margin: '0 0 0.75rem 0', color: '#0c4a6e' }}>📊 Datenbank-Schema</h5>
|
||||
<div style={{ fontSize: '0.875rem', fontFamily: 'monospace' }}>
|
||||
<div style={{ marginBottom: '0.5rem' }}>
|
||||
<strong>Tabellen:</strong> {schemaInfo.tables.join(', ')}
|
||||
</div>
|
||||
<div style={{ marginBottom: '0.25rem' }}>
|
||||
<strong>products:</strong> {schemaInfo.productsColumns?.join(', ')}
|
||||
</div>
|
||||
<div style={{ marginBottom: '0.25rem' }}>
|
||||
<strong>users:</strong> {schemaInfo.usersColumns?.join(', ')}
|
||||
</div>
|
||||
<div>
|
||||
<strong>orders:</strong> {schemaInfo.ordersColumns?.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* External Resources */}
|
||||
{showResources && externalResources.length > 0 && (
|
||||
<div style={{
|
||||
marginBottom: '1.5rem',
|
||||
padding: '1rem',
|
||||
background: '#faf5ff',
|
||||
border: '2px solid #a855f7',
|
||||
borderRadius: '0.5rem'
|
||||
}}>
|
||||
<h5 style={{ margin: '0 0 0.75rem 0', color: '#581c87' }}>📚 Externe Ressourcen</h5>
|
||||
<div style={{ display: 'grid', gap: '0.75rem' }}>
|
||||
{externalResources.map((resource, idx) => (
|
||||
<div key={idx}>
|
||||
<a
|
||||
href={resource.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#7c3aed', fontWeight: '600', fontSize: '0.875rem' }}
|
||||
>
|
||||
{resource.title} ↗
|
||||
</a>
|
||||
<div style={{ color: '#6b21a8', fontSize: '0.75rem' }}>
|
||||
{resource.description}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search Input */}
|
||||
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
|
||||
{/* Search Input */}
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && executeSearch()}
|
||||
placeholder="Search products... (try SQL injection!)"
|
||||
placeholder="SQL Injection payload eingeben..."
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '0.75rem',
|
||||
@@ -126,10 +447,11 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
|
||||
borderRadius: '0.375rem',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: '500'
|
||||
fontWeight: '500',
|
||||
opacity: loading || !searchTerm.trim() ? 0.5 : 1
|
||||
}}
|
||||
>
|
||||
{loading ? 'Searching...' : '🔍 Vulnerable Search'}
|
||||
{loading ? 'Läuft...' : '🔍 Verwundbar'}
|
||||
</button>
|
||||
<button
|
||||
onClick={executeSafeSearch}
|
||||
@@ -142,10 +464,11 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
|
||||
borderRadius: '0.375rem',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: '500'
|
||||
fontWeight: '500',
|
||||
opacity: loading || !searchTerm.trim() ? 0.5 : 1
|
||||
}}
|
||||
>
|
||||
{loading ? 'Searching...' : '✅ Safe Search'}
|
||||
{loading ? 'Läuft...' : '✅ Sicher'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -155,7 +478,7 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
|
||||
<div>
|
||||
{/* SQL Query Display */}
|
||||
<div style={{ marginBottom: '1rem', padding: '1rem', background: '#1f2937', color: '#f3f4f6', borderRadius: '0.375rem', fontFamily: 'monospace', fontSize: '0.875rem' }}>
|
||||
<div style={{ color: '#9ca3af', marginBottom: '0.5rem' }}>Executed SQL Query:</div>
|
||||
<div style={{ color: '#9ca3af', marginBottom: '0.5rem' }}>Ausgeführte SQL-Abfrage:</div>
|
||||
<div style={{ color: '#fbbf24' }}>{queryResult.query}</div>
|
||||
{queryResult.parameter && (
|
||||
<div style={{ marginTop: '0.5rem' }}>
|
||||
@@ -165,6 +488,24 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* New Discovery Alert */}
|
||||
{queryResult.isNewDiscovery && queryResult.pointsAwarded > 0 && (
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
background: '#d1fae5',
|
||||
border: '2px solid #10b981',
|
||||
borderRadius: '0.375rem',
|
||||
marginBottom: '1rem'
|
||||
}}>
|
||||
<div style={{ fontWeight: '600', color: '#065f46', fontSize: '1.125rem', marginBottom: '0.5rem' }}>
|
||||
🎉 Neue Entdeckung! +{queryResult.pointsAwarded} Punkte
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#047857' }}>
|
||||
Du hast eine neue SQL Injection-Technik entdeckt!
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Injection Detection */}
|
||||
{queryResult.injectionDetected && (
|
||||
<div style={{
|
||||
@@ -175,7 +516,8 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
|
||||
marginBottom: '1rem'
|
||||
}}>
|
||||
<div style={{ fontWeight: '600', color: '#991b1b', marginBottom: '0.5rem' }}>
|
||||
⚠️ SQL Injection Detected: {queryResult.injectionType?.replace(/_/g, ' ')}
|
||||
⚠️ SQL-Injection: {queryResult.injectionType?.replace(/_/g, ' ')}
|
||||
{queryResult.injectionType === 'UNION_SELECT' && ' ⭐ Easter Egg!'}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#7f1d1d' }}>
|
||||
{queryResult.explanation}
|
||||
@@ -193,7 +535,7 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
|
||||
marginBottom: '1rem'
|
||||
}}>
|
||||
<div style={{ fontWeight: '600', color: '#065f46', marginBottom: '0.5rem' }}>
|
||||
✅ Secure Query
|
||||
✅ Sichere Abfrage
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#047857' }}>
|
||||
{queryResult.explanation}
|
||||
@@ -204,7 +546,7 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
|
||||
{/* Results Table */}
|
||||
<div>
|
||||
<div style={{ fontSize: '0.875rem', fontWeight: '500', marginBottom: '0.5rem', color: '#374151' }}>
|
||||
Results: {queryResult.recordCount} {queryResult.recordCount === 1 ? 'record' : 'records'}
|
||||
Ergebnisse: {queryResult.recordCount} {queryResult.recordCount === 1 ? 'Datensatz' : 'Datensätze'}
|
||||
</div>
|
||||
|
||||
{queryResult.results.length > 0 ? (
|
||||
@@ -214,9 +556,9 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
|
||||
<tr style={{ background: '#f9fafb', borderBottom: '2px solid #e5e7eb' }}>
|
||||
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>ID</th>
|
||||
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>Name</th>
|
||||
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>Price</th>
|
||||
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>Category</th>
|
||||
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>Stock</th>
|
||||
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>Preis</th>
|
||||
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>Kategorie</th>
|
||||
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>Lagerbestand</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -245,8 +587,8 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
|
||||
) : (
|
||||
<div style={{ padding: '2rem', textAlign: 'center', color: '#6b7280', background: '#f9fafb', borderRadius: '0.375rem' }}>
|
||||
{queryResult.injectionType === 'DROP_TABLE'
|
||||
? '💥 Table would be deleted! (Simulated - no actual data was harmed)'
|
||||
: 'No products found'
|
||||
? '💥 Tabelle würde gelöscht werden! (Simuliert - keine Daten wurden tatsächlich beschädigt)'
|
||||
: 'Keine Produkte gefunden'
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
@@ -254,14 +596,14 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Educational Note */}
|
||||
{!queryResult && (
|
||||
<div style={{ padding: '1.5rem', background: '#eff6ff', border: '1px solid #3b82f6', borderRadius: '0.375rem', fontSize: '0.875rem' }}>
|
||||
<div style={{ fontWeight: '600', color: '#1e40af', marginBottom: '0.5rem' }}>
|
||||
💡 Learning Tip
|
||||
{/* Initial Instructions with Helpful Hints */}
|
||||
{!queryResult && interactiveData.initialHint && (
|
||||
<div style={{ padding: '1.5rem', background: '#fef3c7', border: '2px solid #f59e0b', borderRadius: '0.5rem' }}>
|
||||
<div style={{ fontWeight: '600', color: '#92400e', marginBottom: '0.75rem', fontSize: '1rem' }}>
|
||||
{interactiveData.initialHint.title}
|
||||
</div>
|
||||
<div style={{ color: '#1e3a8a' }}>
|
||||
Start with a normal search like "laptop" to see how the query works. Then try the SQL injection examples to understand how attackers manipulate queries.
|
||||
<div style={{ color: '#78350f', lineHeight: '1.6' }}>
|
||||
{interactiveData.initialHint.content}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
import React, { useState } from 'react';
|
||||
import { participantAPI } from '../../../services/api.service';
|
||||
|
||||
const SocialMediaPasswordDemo = ({ lessonData, eventLessonId }) => {
|
||||
const interactiveData = lessonData?.interactiveData || {};
|
||||
const profile = interactiveData.profile || {};
|
||||
const loginForm = interactiveData.loginForm || {};
|
||||
const posts = profile.posts || [];
|
||||
|
||||
const [password, setPassword] = useState('');
|
||||
const [result, setResult] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const attemptLogin = async () => {
|
||||
if (!password.trim()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await participantAPI.executeLessonAction(
|
||||
eventLessonId,
|
||||
'test-password',
|
||||
{ password }
|
||||
);
|
||||
|
||||
setResult(response.data.data);
|
||||
|
||||
if (response.data.data.success) {
|
||||
// Clear password on success
|
||||
setTimeout(() => {
|
||||
setPassword('');
|
||||
}, 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to test password:', error);
|
||||
setResult({
|
||||
success: false,
|
||||
message: 'Fehler bei der Passwortprüfung',
|
||||
attemptCount: 0
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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 Bildungszwecken
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#78350f', marginTop: '0.5rem' }}>
|
||||
Dies ist eine sichere Simulation. Teilen Sie niemals persönliche Informationen in sozialen Medien, die für Passwörter verwendet werden könnten!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style={{ marginBottom: '1.5rem', color: '#1f2937' }}>🔐 Social Engineering - Passwort-Schwachstelle</h4>
|
||||
|
||||
{/* Two-Column Layout */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.5rem' }}>
|
||||
{/* Left Column: Social Media Profile */}
|
||||
<div style={{
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '0.5rem',
|
||||
background: '#fafafa',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* Profile Header */}
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
padding: '1.5rem',
|
||||
color: 'white'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '4rem',
|
||||
height: '4rem',
|
||||
borderRadius: '50%',
|
||||
background: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '2rem',
|
||||
marginBottom: '0.75rem'
|
||||
}}>
|
||||
{profile.profileImage || '👤'}
|
||||
</div>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: '600', marginBottom: '0.25rem' }}>
|
||||
{profile.name}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', opacity: 0.9 }}>
|
||||
{profile.username}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.75rem', opacity: 0.8, marginTop: '0.5rem' }}>
|
||||
{profile.bio}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Profile Stats */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr 1fr',
|
||||
padding: '1rem',
|
||||
borderBottom: '1px solid #e5e7eb',
|
||||
background: 'white'
|
||||
}}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontWeight: '600', color: '#1f2937' }}>{posts.length}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#6b7280' }}>Beiträge</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontWeight: '600', color: '#1f2937' }}>{profile.followers || 0}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#6b7280' }}>Follower</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontWeight: '600', color: '#1f2937' }}>{profile.following || 0}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#6b7280' }}>Folge ich</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Posts Feed */}
|
||||
<div style={{
|
||||
maxHeight: '400px',
|
||||
overflowY: 'auto',
|
||||
padding: '1rem'
|
||||
}}>
|
||||
<div style={{ fontWeight: '600', marginBottom: '1rem', color: '#374151', fontSize: '0.875rem' }}>
|
||||
📝 Beiträge
|
||||
</div>
|
||||
{posts.map((post, idx) => (
|
||||
<div
|
||||
key={post.id || idx}
|
||||
style={{
|
||||
background: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '0.375rem',
|
||||
padding: '1rem',
|
||||
marginBottom: '1rem'
|
||||
}}
|
||||
>
|
||||
{post.type === 'photo' && (
|
||||
<>
|
||||
<div style={{
|
||||
background: '#e5e7eb',
|
||||
borderRadius: '0.375rem',
|
||||
padding: '2rem',
|
||||
marginBottom: '0.75rem',
|
||||
textAlign: 'center',
|
||||
fontSize: '0.875rem',
|
||||
color: '#6b7280',
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
{post.imageDescription}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#374151', marginBottom: '0.5rem' }}>
|
||||
{post.caption}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{post.type === 'text' && (
|
||||
<div style={{ fontSize: '0.875rem', color: '#374151', marginBottom: '0.5rem' }}>
|
||||
{post.content}
|
||||
</div>
|
||||
)}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '1rem',
|
||||
fontSize: '0.75rem',
|
||||
color: '#6b7280',
|
||||
marginTop: '0.75rem',
|
||||
paddingTop: '0.75rem',
|
||||
borderTop: '1px solid #f3f4f6'
|
||||
}}>
|
||||
<span>❤️ {post.likes || 0} Gefällt mir</span>
|
||||
<span>💬 {post.comments || 0} Kommentare</span>
|
||||
<span style={{ marginLeft: 'auto' }}>{post.timestamp}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Login Form */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '0.5rem',
|
||||
padding: '2rem',
|
||||
background: 'white'
|
||||
}}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
marginBottom: '1.5rem'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '2rem',
|
||||
marginBottom: '0.5rem'
|
||||
}}>🔒</div>
|
||||
<h3 style={{ margin: 0, color: '#1f2937', marginBottom: '0.5rem' }}>Anmelden</h3>
|
||||
<p style={{ fontSize: '0.875rem', color: '#6b7280', margin: 0 }}>
|
||||
{loginForm.passwordHint}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<label style={{ display: 'block', fontSize: '0.875rem', fontWeight: '500', marginBottom: '0.5rem', color: '#374151' }}>
|
||||
E-Mail-Adresse
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={loginForm.username}
|
||||
disabled
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.75rem',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '0.375rem',
|
||||
fontSize: '0.875rem',
|
||||
background: '#f3f4f6',
|
||||
color: '#6b7280'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<label style={{ display: 'block', fontSize: '0.875rem', fontWeight: '500', marginBottom: '0.5rem', color: '#374151' }}>
|
||||
Passwort
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && attemptLogin()}
|
||||
placeholder="Passwort eingeben..."
|
||||
disabled={result?.success}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.75rem',
|
||||
border: `2px solid ${result?.success ? '#10b981' : '#d1d5db'}`,
|
||||
borderRadius: '0.375rem',
|
||||
fontSize: '0.875rem'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={attemptLogin}
|
||||
disabled={loading || !password.trim() || result?.success}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.75rem',
|
||||
background: result?.success ? '#10b981' : (loading || !password.trim() ? '#9ca3af' : '#3b82f6'),
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.375rem',
|
||||
cursor: loading || !password.trim() || result?.success ? 'not-allowed' : 'pointer',
|
||||
fontWeight: '500',
|
||||
fontSize: '0.875rem',
|
||||
marginBottom: '1rem'
|
||||
}}
|
||||
>
|
||||
{loading ? 'Wird geprüft...' : (result?.success ? '✓ Angemeldet' : 'Anmelden')}
|
||||
</button>
|
||||
|
||||
{/* Result Message */}
|
||||
{result && (
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
background: result.success ? '#d1fae5' : '#fee2e2',
|
||||
border: `2px solid ${result.success ? '#10b981' : '#ef4444'}`,
|
||||
borderRadius: '0.375rem',
|
||||
marginBottom: '1rem'
|
||||
}}>
|
||||
<div style={{
|
||||
fontWeight: '600',
|
||||
color: result.success ? '#065f46' : '#991b1b',
|
||||
marginBottom: '0.5rem'
|
||||
}}>
|
||||
{result.success ? '✅ Erfolg!' : '❌ Fehlgeschlagen'}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '0.875rem',
|
||||
color: result.success ? '#047857' : '#7f1d1d',
|
||||
marginBottom: result.explanation ? '0.5rem' : 0
|
||||
}}>
|
||||
{result.message}
|
||||
</div>
|
||||
{result.explanation && (
|
||||
<div style={{
|
||||
fontSize: '0.75rem',
|
||||
color: '#065f46',
|
||||
padding: '0.5rem',
|
||||
background: '#ecfdf5',
|
||||
borderRadius: '0.25rem',
|
||||
marginBottom: '0.5rem'
|
||||
}}>
|
||||
<strong>Erklärung:</strong> {result.explanation}
|
||||
</div>
|
||||
)}
|
||||
{result.securityTip && (
|
||||
<div style={{
|
||||
fontSize: '0.75rem',
|
||||
color: '#065f46',
|
||||
padding: '0.5rem',
|
||||
background: '#ecfdf5',
|
||||
borderRadius: '0.25rem'
|
||||
}}>
|
||||
<strong>💡 Sicherheits-Tipp:</strong> {result.securityTip}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hint Display */}
|
||||
{result && result.hint && !result.success && (
|
||||
<div style={{
|
||||
padding: '0.75rem',
|
||||
background: '#eff6ff',
|
||||
border: '1px solid #3b82f6',
|
||||
borderRadius: '0.375rem',
|
||||
fontSize: '0.875rem',
|
||||
color: '#1e40af'
|
||||
}}>
|
||||
💡 {result.hint}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Attempt Counter */}
|
||||
{result && result.attemptCount > 0 && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
fontSize: '0.75rem',
|
||||
color: '#6b7280',
|
||||
marginTop: '1rem'
|
||||
}}>
|
||||
Versuch {result.attemptCount}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Learning Tip */}
|
||||
<div style={{
|
||||
marginTop: '1.5rem',
|
||||
padding: '1rem',
|
||||
background: '#fef2f2',
|
||||
border: '1px solid #fca5a5',
|
||||
borderRadius: '0.375rem',
|
||||
fontSize: '0.875rem'
|
||||
}}>
|
||||
<div style={{ fontWeight: '600', color: '#991b1b', marginBottom: '0.5rem' }}>
|
||||
🎯 Hinweis
|
||||
</div>
|
||||
<div style={{ color: '#7f1d1d' }}>
|
||||
Schauen Sie sich die Posts genau an. Welche persönlichen Informationen werden geteilt? Namen, Jahreszahlen, besondere Details...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SocialMediaPasswordDemo;
|
||||
@@ -0,0 +1,498 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { participantAPI } from '../../../services/api.service';
|
||||
|
||||
const XSSDeeplinkDemo = ({ lessonData, eventLessonId }) => {
|
||||
const [payload, setPayload] = useState('');
|
||||
const [result, setResult] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showComparison, setShowComparison] = 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 });
|
||||
|
||||
const interactiveData = lessonData?.interactiveData || {};
|
||||
const freeHints = interactiveData.freeHints || [];
|
||||
const timeLimit = interactiveData.timeLimit || 900000; // 15 min default
|
||||
|
||||
// Start timer on mount
|
||||
useEffect(() => {
|
||||
const startTimer = async () => {
|
||||
try {
|
||||
const response = await participantAPI.executeLessonAction(
|
||||
eventLessonId,
|
||||
'start-timer',
|
||||
{ stepId: 'xss-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 testPayload = async () => {
|
||||
if (!payload.trim()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await participantAPI.executeLessonAction(
|
||||
eventLessonId,
|
||||
'test-xss',
|
||||
{ payload, stepId: 'xss-demo' }
|
||||
);
|
||||
|
||||
const data = response.data.data;
|
||||
setResult(data);
|
||||
setShowComparison(true);
|
||||
|
||||
// Update progress
|
||||
if (data.progress) {
|
||||
setProgress(data.progress);
|
||||
}
|
||||
|
||||
// Update remaining time from server
|
||||
if (data.remainingTime !== undefined) {
|
||||
setRemainingTime(data.remainingTime);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to test payload:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const requestHint = async () => {
|
||||
try {
|
||||
const response = await participantAPI.executeLessonAction(
|
||||
eventLessonId,
|
||||
'get-hint',
|
||||
{ stepId: 'xss-demo' }
|
||||
);
|
||||
setCurrentHint(response.data.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to get hint:', error);
|
||||
}
|
||||
};
|
||||
|
||||
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 Bildungszwecken
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#78350f', marginTop: '0.5rem' }}>
|
||||
Dies ist eine sichere Simulation. Echter XSS-Code wird nicht ausgeführt.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style={{ marginBottom: '1rem', color: '#1f2937' }}>🔐 XSS Deeplink Injection 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>
|
||||
|
||||
{/* URL Input */}
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: '500',
|
||||
marginBottom: '0.5rem',
|
||||
color: '#374151'
|
||||
}}>
|
||||
URL Parameter (name):
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={payload}
|
||||
onChange={(e) => setPayload(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && testPayload()}
|
||||
placeholder="Versuchen Sie XSS-Payloads einzufügen..."
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '0.75rem',
|
||||
border: '2px solid #d1d5db',
|
||||
borderRadius: '0.375rem',
|
||||
fontSize: '0.875rem',
|
||||
fontFamily: 'monospace'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={testPayload}
|
||||
disabled={loading || !payload.trim()}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
background: loading || !payload.trim() ? '#9ca3af' : '#3b82f6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.375rem',
|
||||
cursor: loading || !payload.trim() ? 'not-allowed' : 'pointer',
|
||||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
{loading ? 'Teste...' : 'Testen'}
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginTop: '0.25rem' }}>
|
||||
URL: https://example-shop.com/product?name={payload || '...'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{result && showComparison && (
|
||||
<div style={{ marginTop: '1.5rem' }}>
|
||||
{/* Detection Result */}
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
background: result.isXSS ? '#fee2e2' : '#d1fae5',
|
||||
border: `2px solid ${result.isXSS ? '#ef4444' : '#10b981'}`,
|
||||
borderRadius: '0.375rem',
|
||||
marginBottom: '1rem'
|
||||
}}>
|
||||
<div style={{
|
||||
fontWeight: '600',
|
||||
color: result.isXSS ? '#991b1b' : '#065f46',
|
||||
marginBottom: '0.5rem'
|
||||
}}>
|
||||
{result.isXSS ? '⚠️ XSS Erkannt!' : '✅ Keine XSS Erkannt'}
|
||||
</div>
|
||||
{result.isXSS && (
|
||||
<>
|
||||
<div style={{ fontSize: '0.875rem', color: '#7f1d1d', marginBottom: '0.5rem' }}>
|
||||
<strong>Typ:</strong> {result.attackTitle}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#7f1d1d', marginBottom: '0.5rem' }}>
|
||||
{result.explanation}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '0.75rem',
|
||||
color: '#991b1b',
|
||||
padding: '0.5rem',
|
||||
background: '#fef2f2',
|
||||
borderRadius: '0.25rem',
|
||||
marginTop: '0.5rem'
|
||||
}}>
|
||||
<strong>Auswirkung:</strong> {result.impact}
|
||||
</div>
|
||||
{result.isNewDiscovery && (
|
||||
<div style={{
|
||||
marginTop: '0.75rem',
|
||||
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>
|
||||
|
||||
{/* Comparison */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||
{/* Vulnerable */}
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
background: '#fef2f2',
|
||||
border: '2px solid #ef4444',
|
||||
borderRadius: '0.375rem'
|
||||
}}>
|
||||
<div style={{ fontWeight: '600', color: '#991b1b', marginBottom: '0.5rem' }}>
|
||||
❌ Anfällig (Unsicher)
|
||||
</div>
|
||||
<pre style={{
|
||||
fontSize: '0.75rem',
|
||||
fontFamily: 'monospace',
|
||||
background: '#1f2937',
|
||||
color: '#f9fafb',
|
||||
padding: '0.75rem',
|
||||
borderRadius: '0.25rem',
|
||||
overflow: 'auto',
|
||||
margin: 0
|
||||
}}>
|
||||
{result.comparisonHTML.vulnerable}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Safe */}
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
background: '#f0fdf4',
|
||||
border: '2px solid #10b981',
|
||||
borderRadius: '0.375rem'
|
||||
}}>
|
||||
<div style={{ fontWeight: '600', color: '#065f46', marginBottom: '0.5rem' }}>
|
||||
✅ Sicher (Kodiert)
|
||||
</div>
|
||||
<pre style={{
|
||||
fontSize: '0.75rem',
|
||||
fontFamily: 'monospace',
|
||||
background: '#1f2937',
|
||||
color: '#f9fafb',
|
||||
padding: '0.75rem',
|
||||
borderRadius: '0.25rem',
|
||||
overflow: 'auto',
|
||||
margin: 0
|
||||
}}>
|
||||
{result.comparisonHTML.safe}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Learning Resources */}
|
||||
<div style={{
|
||||
marginTop: '1.5rem',
|
||||
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 XSSDeeplinkDemo;
|
||||
@@ -2,6 +2,8 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useParticipant } from '../contexts/ParticipantContext';
|
||||
import { participantAPI } from '../services/api.service';
|
||||
import EventLeaderboard from '../components/EventLeaderboard';
|
||||
import EventComments from '../components/EventComments';
|
||||
|
||||
const EventLanding = () => {
|
||||
const [lessons, setLessons] = useState([]);
|
||||
@@ -29,6 +31,18 @@ const EventLanding = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleScoreUpdate = (newScore) => {
|
||||
setProgress(prev => ({
|
||||
...prev,
|
||||
total_score: newScore
|
||||
}));
|
||||
};
|
||||
|
||||
// Check if all lessons are completed (for Easter egg hint)
|
||||
const allLessonsCompleted = progress &&
|
||||
progress.lessons_completed === progress.total_lessons_available &&
|
||||
progress.total_lessons_available > 0;
|
||||
|
||||
if (loading) {
|
||||
return <div style={{ padding: '2rem', textAlign: 'center' }}>Loading...</div>;
|
||||
}
|
||||
@@ -64,19 +78,71 @@ const EventLanding = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EventLeaderboard eventId={event.id} />
|
||||
|
||||
{allLessonsCompleted && (
|
||||
<div style={{
|
||||
padding: '1.5rem',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white',
|
||||
borderRadius: '0.5rem',
|
||||
marginBottom: '2rem',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '1rem' }}>
|
||||
<div style={{ fontSize: '2rem' }}>🎓</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<h4 style={{ margin: '0 0 0.5rem 0', color: 'white' }}>
|
||||
Congratulations! All lessons completed! 🎉
|
||||
</h4>
|
||||
<p style={{ margin: '0 0 0.75rem 0', opacity: 0.95, lineHeight: '1.5' }}>
|
||||
You've mastered the fundamentals of web security. Want to put your skills to the test?
|
||||
</p>
|
||||
<div style={{
|
||||
background: 'rgba(255, 255, 255, 0.2)',
|
||||
padding: '0.75rem',
|
||||
borderRadius: '0.375rem',
|
||||
backdropFilter: 'blur(10px)',
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: '1.6'
|
||||
}}>
|
||||
<strong>💡 Pro Tip:</strong> Security researchers always inspect the inner workings of applications.
|
||||
Try opening your browser's <strong>Developer Tools</strong> (press <kbd style={{
|
||||
background: 'rgba(0,0,0,0.3)',
|
||||
padding: '0.125rem 0.375rem',
|
||||
borderRadius: '0.25rem',
|
||||
fontFamily: 'monospace'
|
||||
}}>F12</kbd>),
|
||||
check the <strong>Console</strong> and <strong>Network</strong> tabs, and reload this page.
|
||||
What information can you discover about how this application works? 🔍
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EventComments
|
||||
eventId={event.id}
|
||||
onScoreUpdate={handleScoreUpdate}
|
||||
/>
|
||||
|
||||
<h3 style={{ marginBottom: '1rem' }}>Lessons</h3>
|
||||
<div style={{ display: 'grid', gap: '1rem' }}>
|
||||
{lessons.map(lesson => (
|
||||
<div
|
||||
key={lesson.eventLessonId}
|
||||
className="card"
|
||||
style={{
|
||||
opacity: lesson.isUnlocked ? 1 : 0.6,
|
||||
cursor: lesson.isUnlocked ? 'pointer' : 'not-allowed',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onClick={() => lesson.isUnlocked && navigate(`/lesson/${lesson.eventLessonId}`)}
|
||||
>
|
||||
{lessons.map(lesson => {
|
||||
const isCompleted = lesson.progress?.status === 'completed';
|
||||
const canAccess = lesson.isUnlocked && !isCompleted;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={lesson.eventLessonId}
|
||||
className="card"
|
||||
style={{
|
||||
opacity: lesson.isUnlocked ? 1 : 0.6,
|
||||
cursor: canAccess ? 'pointer' : isCompleted ? 'default' : 'not-allowed',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onClick={() => canAccess && navigate(`/lesson/${lesson.eventLessonId}`)}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' }}>
|
||||
@@ -110,7 +176,8 @@ const EventLanding = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { participantAPI } from '../services/api.service';
|
||||
import SQLShopDemo from '../components/lessons/InteractiveContent/SQLShopDemo';
|
||||
import BitBDemo from '../components/lessons/InteractiveContent/BitBDemo';
|
||||
import XSSDeeplinkDemo from '../components/lessons/InteractiveContent/XSSDeeplinkDemo';
|
||||
import ForumScriptDemo from '../components/lessons/InteractiveContent/ForumScriptDemo';
|
||||
import SocialMediaPasswordDemo from '../components/lessons/InteractiveContent/SocialMediaPasswordDemo';
|
||||
import IDORDemo from '../components/lessons/InteractiveContent/IDORDemo';
|
||||
|
||||
const LessonView = () => {
|
||||
const { eventLessonId } = useParams();
|
||||
@@ -13,6 +18,7 @@ const LessonView = () => {
|
||||
const [feedback, setFeedback] = useState({});
|
||||
const [totalScore, setTotalScore] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [completedInteractiveSteps, setCompletedInteractiveSteps] = useState(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
loadLesson();
|
||||
@@ -21,7 +27,16 @@ const LessonView = () => {
|
||||
const loadLesson = async () => {
|
||||
try {
|
||||
const response = await participantAPI.getLessonContent(eventLessonId);
|
||||
setLesson(response.data.data);
|
||||
const lessonData = response.data.data;
|
||||
|
||||
// Check if lesson is already completed
|
||||
if (lessonData.progress?.status === 'completed') {
|
||||
// Redirect back to event page - lesson already completed
|
||||
navigate('/event');
|
||||
return;
|
||||
}
|
||||
|
||||
setLesson(lessonData);
|
||||
await participantAPI.startLesson(eventLessonId);
|
||||
} catch (error) {
|
||||
console.error('Failed to load lesson:', error);
|
||||
@@ -58,6 +73,20 @@ const LessonView = () => {
|
||||
const currentStep = lesson.steps[currentStepIndex];
|
||||
const isLastStep = currentStepIndex === lesson.steps.length - 1;
|
||||
|
||||
// Check if Previous button should be locked
|
||||
// Lock if any interactive step between 0 and current index has been completed
|
||||
const isPreviousLocked = Array.from(completedInteractiveSteps).some(
|
||||
completedIndex => completedIndex < currentStepIndex
|
||||
);
|
||||
|
||||
// Handler for Next button - mark interactive steps as completed
|
||||
const handleNext = () => {
|
||||
if (currentStep.type === 'interactive') {
|
||||
setCompletedInteractiveSteps(prev => new Set([...prev, currentStepIndex]));
|
||||
}
|
||||
setCurrentStepIndex(currentStepIndex + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: '#f9fafb' }}>
|
||||
<nav style={{ background: 'white', padding: '1rem 2rem', borderBottom: '1px solid #e5e7eb' }}>
|
||||
@@ -74,23 +103,35 @@ const LessonView = () => {
|
||||
<h2>{currentStep.title}</h2>
|
||||
|
||||
{currentStep.type === 'content' && (
|
||||
<div style={{ whiteSpace: 'pre-wrap', lineHeight: '1.8' }}>
|
||||
{currentStep.content}
|
||||
<div style={{ lineHeight: '1.8' }} className="markdown-content">
|
||||
<ReactMarkdown>{currentStep.content}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep.type === 'interactive' && (
|
||||
<div>
|
||||
{currentStep.content && (
|
||||
<div style={{ whiteSpace: 'pre-wrap', lineHeight: '1.8', marginBottom: '1.5rem' }}>
|
||||
{currentStep.content}
|
||||
<div style={{ lineHeight: '1.8', marginBottom: '1.5rem' }} className="markdown-content">
|
||||
<ReactMarkdown>{currentStep.content}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
{currentStep.interactiveComponent === 'SQLShopDemo' && (
|
||||
<SQLShopDemo lessonData={lesson} eventLessonId={eventLessonId} />
|
||||
<SQLShopDemo lessonData={currentStep} eventLessonId={eventLessonId} />
|
||||
)}
|
||||
{currentStep.interactiveComponent === 'BitBDemo' && (
|
||||
<BitBDemo lessonData={lesson} eventLessonId={eventLessonId} />
|
||||
<BitBDemo lessonData={currentStep} eventLessonId={eventLessonId} />
|
||||
)}
|
||||
{currentStep.interactiveComponent === 'XSSDeeplinkDemo' && (
|
||||
<XSSDeeplinkDemo lessonData={currentStep} eventLessonId={eventLessonId} />
|
||||
)}
|
||||
{currentStep.interactiveComponent === 'ForumScriptDemo' && (
|
||||
<ForumScriptDemo lessonData={currentStep} eventLessonId={eventLessonId} />
|
||||
)}
|
||||
{currentStep.interactiveComponent === 'SocialMediaPasswordDemo' && (
|
||||
<SocialMediaPasswordDemo lessonData={currentStep} eventLessonId={eventLessonId} />
|
||||
)}
|
||||
{currentStep.interactiveComponent === 'IDORDemo' && (
|
||||
<IDORDemo lessonData={currentStep} eventLessonId={eventLessonId} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -206,15 +247,17 @@ const LessonView = () => {
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '2rem' }}>
|
||||
<button
|
||||
onClick={() => setCurrentStepIndex(Math.max(0, currentStepIndex - 1))}
|
||||
disabled={currentStepIndex === 0}
|
||||
disabled={currentStepIndex === 0 || isPreviousLocked}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
background: '#6b7280',
|
||||
background: (currentStepIndex === 0 || isPreviousLocked) ? '#d1d5db' : '#6b7280',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.375rem',
|
||||
cursor: 'pointer'
|
||||
cursor: (currentStepIndex === 0 || isPreviousLocked) ? 'not-allowed' : 'pointer',
|
||||
opacity: (currentStepIndex === 0 || isPreviousLocked) ? 0.5 : 1
|
||||
}}
|
||||
title={isPreviousLocked ? 'Cannot go back after completing interactive steps' : ''}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
@@ -235,7 +278,7 @@ const LessonView = () => {
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setCurrentStepIndex(currentStepIndex + 1)}
|
||||
onClick={handleNext}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
background: '#2563eb',
|
||||
|
||||
@@ -77,7 +77,20 @@ export const participantAPI = {
|
||||
|
||||
// Execute lesson-specific actions (e.g., SQL query, interactive demos)
|
||||
executeLessonAction: (eventLessonId, action, data) =>
|
||||
api.post(`/lesson/${eventLessonId}/action/${action}`, data)
|
||||
api.post(`/lesson/${eventLessonId}/action/${action}`, data),
|
||||
|
||||
// Event leaderboard
|
||||
getEventLeaderboard: (eventId, filter = '') =>
|
||||
api.get(`/participant/event/${eventId}/leaderboard`, {
|
||||
params: filter ? { filter } : {}
|
||||
}),
|
||||
|
||||
// Event comments (hidden jackpot feature)
|
||||
addEventComment: (eventId, content) =>
|
||||
api.post(`/participant/event/${eventId}/comment`, { content }),
|
||||
|
||||
getEventComments: (eventId) =>
|
||||
api.get(`/participant/event/${eventId}/comments`)
|
||||
};
|
||||
|
||||
// Admin API
|
||||
@@ -117,6 +130,9 @@ export const adminAPI = {
|
||||
getEventAnalytics: (eventId) =>
|
||||
api.get(`/admin/events/${eventId}/analytics`),
|
||||
|
||||
getJackpotStats: (eventId) =>
|
||||
api.get(`/admin/events/${eventId}/jackpot-stats`),
|
||||
|
||||
// Lessons
|
||||
getAllLessons: () =>
|
||||
api.get('/admin/lessons'),
|
||||
|
||||
@@ -93,3 +93,125 @@ input:focus, textarea:focus {
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* Markdown content styling */
|
||||
.markdown-content {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.markdown-content p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.markdown-content ul,
|
||||
.markdown-content ol {
|
||||
margin-bottom: 1rem;
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.markdown-content ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.markdown-content ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown-content ul ul,
|
||||
.markdown-content ol ul {
|
||||
list-style-type: circle;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown-content strong {
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.markdown-content em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown-content code {
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 0.875rem;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
background-color: #1f2937;
|
||||
color: #f9fafb;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.markdown-content pre code {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.markdown-content h1,
|
||||
.markdown-content h2,
|
||||
.markdown-content h3,
|
||||
.markdown-content h4 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-content h1 {
|
||||
font-size: 1.875rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.markdown-content h2 {
|
||||
font-size: 1.5rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.markdown-content h3 {
|
||||
font-size: 1.25rem;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.markdown-content h4 {
|
||||
font-size: 1.125rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.markdown-content blockquote {
|
||||
border-left: 4px solid var(--primary-color);
|
||||
padding-left: 1rem;
|
||||
margin: 1rem 0;
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown-content a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.markdown-content a:hover {
|
||||
border-bottom-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.markdown-content hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user