Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions Databases/supabase/migrations/20260133_create_challenges.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
# Create Challenges Table

1. New Tables
- `challenges`
- `id` (uuid, primary key)
- `title` (text)
- `description` (text)
- `category` (text)
- `difficulty` (text)
- `correct_flag` (text)
- `hints` (text array)
- `file_name` (text, optional)
- `file_path` (text, optional)
- `active` (boolean, default true)
- `created_at` (timestamp)
- `updated_at` (timestamp)

2. Security
- Enable RLS on `challenges` table
- Add policies for admins to manage challenges
- Add policies for users to view active challenges
*/

CREATE TABLE IF NOT EXISTS challenges (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
title text NOT NULL,
description text NOT NULL,
category text NOT NULL,
difficulty text NOT NULL,
correct_flag text NOT NULL,
hints text[] DEFAULT '{}',
file_name text,
file_path text,
active boolean DEFAULT true,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);

ALTER TABLE challenges ENABLE ROW LEVEL SECURITY;

-- Users can view active challenges
CREATE POLICY "Users can view active challenges"
ON challenges
FOR SELECT
USING (active = true);

-- Admins can view all challenges
CREATE POLICY "Admins can view all challenges"
ON challenges
FOR SELECT
USING (auth.jwt() ->> 'role' = 'admin');

-- Admins can insert challenges
CREATE POLICY "Admins can insert challenges"
ON challenges
FOR INSERT
WITH CHECK (auth.jwt() ->> 'role' = 'admin');

-- Admins can update challenges
CREATE POLICY "Admins can update challenges"
ON challenges
FOR UPDATE
USING (auth.jwt() ->> 'role' = 'admin')
WITH CHECK (auth.jwt() ->> 'role' = 'admin');

-- Admins can delete challenges
CREATE POLICY "Admins can delete challenges"
ON challenges
FOR DELETE
USING (auth.jwt() ->> 'role' = 'admin');

CREATE INDEX IF NOT EXISTS idx_challenges_category ON challenges(category);
CREATE INDEX IF NOT EXISTS idx_challenges_difficulty ON challenges(difficulty);
CREATE INDEX IF NOT EXISTS idx_challenges_active ON challenges(active);
229 changes: 198 additions & 31 deletions src/components/AdminDashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,48 @@
try {
setProcessing(submission.id);
import React, { useState, useEffect } from 'react';
import { supabase } from '../lib/supabase';
import { Sparkles, Plus } from 'lucide-react';

// Create the challenge directory and files
const challengeId = `q${Date.now()}`;
const challengeDir = `public/challenges/${challengeId}`;
interface ChallengeSubmission {
id: string;
title: string;
description: string;
category: string;
difficulty: string;
correct_flag: string;
hints: string[];
assets: any[];
status: 'pending' | 'approved' | 'rejected';
created_at: string;
updated_at: string;
}

// Create challenge.json
const challengeData = {
category: submission.category,
difficulty: submission.difficulty
};
export function AdminDashboard() {
const [submissions, setSubmissions] = useState<ChallengeSubmission[]>([]);
const [processing, setProcessing] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [generateCategory, setGenerateCategory] = useState('');
const [generateDifficulty, setGenerateDifficulty] = useState('');

// For now, we'll simulate the approval process
// In a real implementation, you'd use Supabase Edge Functions
// to create the files and move assets
useEffect(() => {
loadSubmissions();
}, []);

const { error } = await supabase
const loadSubmissions = async () => {
try {
const { data, error } = await supabase
.from('challenge_submissions')
.update({
status: 'approved',
updated_at: new Date().toISOString()
})
.eq('id', submission.id);
.select('*')
.order('created_at', { ascending: false });

if (error) throw error;

setSubmissions(prev =>
prev.map(s =>
s.id === submission.id ? { ...s, status: 'approved' as const } : s
)
);

alert(`Challenge "${submission.title}" has been approved and added to the challenge pool!`);
setSubmissions(data || []);
} catch (err) {
console.error('Error approving submission:', err);
alert('Error approving submission. Please try again.');
console.error('Error loading submissions:', err);
} finally {
setProcessing(null);
setLoading(false);
}
};
=======

const handleApprove = async (submission: ChallengeSubmission) => {
try {
setProcessing(submission.id);
Expand Down Expand Up @@ -73,3 +76,167 @@ try {
setProcessing(null);
}
};

const handleGenerateChallenge = async () => {
if (!generateCategory || !generateDifficulty) {
alert('Please select both category and difficulty');
return;
}

try {
setProcessing('generate');

const { data, error } = await supabase.functions.invoke('generate-challenge', {
body: {
category: generateCategory,
difficulty: generateDifficulty
}
});

if (error) {
throw error;
}

if (!data.success) {
throw new Error(data.error || 'Failed to generate challenge');
}

alert(data.message);
setGenerateCategory('');
setGenerateDifficulty('');
} catch (err: any) {
console.error('Error generating challenge:', err);
alert('Error generating challenge: ' + err.message);
} finally {
setProcessing(null);
}
};

if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-green-400 font-mono flex items-center justify-center">
<div className="text-center">
<div className="animate-spin mb-4">
<div className="w-12 h-12 text-green-500"></div>
</div>
<p>Loading admin dashboard...</p>
</div>
</div>
);
}

return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-green-400 font-mono">
<div className="scanlines"></div>
<div className="relative z-10 container mx-auto px-4 py-6 max-w-6xl">
<header className="mb-8">
<h1 className="text-3xl font-bold">
ADMIN<span className="text-green-500">DASHBOARD</span>
</h1>
<p className="text-green-300/60 text-sm mt-2">Manage challenge submissions and generate new challenges</p>
</header>

{/* Generate Challenge Section */}
<div className="mb-8">
<div className="bg-black/50 border border-green-500/30 rounded-lg p-6">
<h2 className="text-xl font-bold text-green-400 mb-4 flex items-center gap-2">
<Sparkles className="w-5 h-5" />
Generate New Challenge
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<label className="block text-green-400 mb-2 text-sm">Category</label>
<select
value={generateCategory}
onChange={(e) => setGenerateCategory(e.target.value)}
className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-green-400 focus:border-green-500 focus:outline-none"
>
<option value="">Select Category</option>
<option value="Cryptography">Cryptography</option>
<option value="Programming">Programming</option>
<option value="Steganography">Steganography</option>
</select>
</div>
<div>
<label className="block text-green-400 mb-2 text-sm">Difficulty</label>
<select
value={generateDifficulty}
onChange={(e) => setGenerateDifficulty(e.target.value)}
className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-green-400 focus:border-green-500 focus:outline-none"
>
<option value="">Select Difficulty</option>
<option value="Beginner">Beginner</option>
<option value="Intermediate">Intermediate</option>
<option value="Advanced">Advanced</option>
</select>
</div>
<div className="flex items-end">
<button
onClick={handleGenerateChallenge}
disabled={processing === 'generate' || !generateCategory || !generateDifficulty}
className="w-full bg-green-600 hover:bg-green-700 text-black font-bold py-2 px-4 rounded transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{processing === 'generate' ? (
<div className="w-4 h-4 border-2 border-black border-t-transparent rounded-full animate-spin"></div>
) : (
<Plus className="w-4 h-4" />
)}
Generate Challenge
</button>
</div>
</div>
</div>
</div>

{/* Challenge Submissions */}
<div className="bg-black/50 border border-green-500/30 rounded-lg p-6">
<h2 className="text-xl font-bold text-green-400 mb-4">Challenge Submissions</h2>

{submissions.length === 0 ? (
<p className="text-green-300/60 text-center py-8">No submissions yet</p>
) : (
<div className="space-y-4">
{submissions.map((submission) => (
<div
key={submission.id}
className="border border-green-500/20 rounded-lg p-4 bg-black/30"
>
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h3 className="text-lg font-bold text-green-400">{submission.title}</h3>
<p className="text-green-300/80 text-sm mt-1">{submission.description}</p>
<div className="flex gap-4 mt-2 text-xs text-green-300/60">
<span>Category: {submission.category}</span>
<span>Difficulty: {submission.difficulty}</span>
<span>Status: <span className={`font-bold ${
submission.status === 'approved' ? 'text-green-500' :
submission.status === 'rejected' ? 'text-red-500' : 'text-yellow-500'
}`}>{submission.status.toUpperCase()}</span></span>
</div>
</div>
{submission.status === 'pending' && (
<button
onClick={() => handleApprove(submission)}
disabled={processing === submission.id}
className="bg-green-600 hover:bg-green-700 text-black font-bold py-2 px-4 rounded transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 text-sm"
>
{processing === submission.id ? (
<div className="w-4 h-4 border-2 border-black border-t-transparent rounded-full animate-spin"></div>
) : (
'Approve'
)}
</button>
)}
</div>
<div className="text-xs text-green-300/40">
Submitted: {new Date(submission.created_at).toLocaleString()}
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
);
}
47 changes: 45 additions & 2 deletions src/components/ChallengePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export function ChallengePage({ teamId, teamName, leaderName, onLogout }: Challe
const [currentEvent, setCurrentEvent] = useState<Event | null>(null);

useEffect(() => {
loadQuestions();
loadChallenge();
}, [teamId]);

Expand Down Expand Up @@ -173,6 +174,48 @@ export function ChallengePage({ teamId, teamName, leaderName, onLogout }: Challe
};
}, [isRunning, challenge, teamId]);

const loadQuestions = async () => {
try {
if (!isSupabaseConfigured) {
// Fallback to sample questions if Supabase is not configured
setQuestions(SAMPLE_QUESTIONS);
return;
}

const { data, error } = await supabase
.from('challenges')
.select('*')
.eq('active', true)
.order('created_at', { ascending: false });

if (error) throw error;

// If no challenges in database, use sample questions
const dbQuestions = data || [];
if (dbQuestions.length === 0) {
setQuestions(SAMPLE_QUESTIONS);
} else {
// Convert database format to Question interface
const formattedQuestions: Question[] = dbQuestions.map(challenge => ({
id: challenge.id,
title: challenge.title,
description: challenge.description,
file_name: challenge.file_name || '',
file_path: challenge.file_path || '',
correct_flag: challenge.correct_flag,
hints: challenge.hints || [],
category: challenge.category,
difficulty: challenge.difficulty
}));
setQuestions(formattedQuestions);
}
} catch (err) {
console.error('Error loading questions:', err);
// Fallback to sample questions on error
setQuestions(SAMPLE_QUESTIONS);
}
};

const loadChallenge = async () => {
try {
const saved = localStorage.getItem(`cybergauntlet_progress_${teamId}`);
Expand Down Expand Up @@ -218,7 +261,7 @@ export function ChallengePage({ teamId, teamName, leaderName, onLogout }: Challe
setElapsedTime(parsed.elapsedTime || 0);
setRevealedHints(parsed.revealedHints || [0]);
} else {
let availableQuestions = SAMPLE_QUESTIONS.filter(q => !(completed ? JSON.parse(completed) : []).includes(q.id));
let availableQuestions = questions.filter(q => !(completed ? JSON.parse(completed) : []).includes(q.id));

// Filter by active event if one exists
if (currentEvent) {
Expand Down Expand Up @@ -248,7 +291,7 @@ export function ChallengePage({ teamId, teamName, leaderName, onLogout }: Challe

setChallenge(localChallenge);

const q = SAMPLE_QUESTIONS.find(q => q.id === localChallenge.questionId);
const q = questions.find(q => q.id === localChallenge.questionId);
if (q) {
setQuestion(q);
}
Expand Down
Loading
Loading