From 6ca7e7fbefe504490e13f19535d5efb8638dbf5a Mon Sep 17 00:00:00 2001 From: Aditya8369 Date: Sun, 8 Feb 2026 22:15:16 +0530 Subject: [PATCH] feat: Advanced Analytics Dashboard --- .../migrations/20260133_create_challenges.sql | 75 ++++++ src/components/AdminDashboard.tsx | 229 ++++++++++++++--- src/components/ChallengePage.tsx | 48 +++- .../functions/generate-challenge/index.ts | 235 ++++++++++++++++++ 4 files changed, 554 insertions(+), 33 deletions(-) create mode 100644 Databases/supabase/migrations/20260133_create_challenges.sql create mode 100644 supabase/functions/generate-challenge/index.ts diff --git a/Databases/supabase/migrations/20260133_create_challenges.sql b/Databases/supabase/migrations/20260133_create_challenges.sql new file mode 100644 index 0000000..e82dc58 --- /dev/null +++ b/Databases/supabase/migrations/20260133_create_challenges.sql @@ -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); diff --git a/src/components/AdminDashboard.tsx b/src/components/AdminDashboard.tsx index 7b264c9..9b6b193 100644 --- a/src/components/AdminDashboard.tsx +++ b/src/components/AdminDashboard.tsx @@ -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([]); + const [processing, setProcessing] = useState(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); @@ -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 ( +
+
+
+
+
+

Loading admin dashboard...

+
+
+ ); + } + + return ( +
+
+
+
+

+ ADMINDASHBOARD +

+

Manage challenge submissions and generate new challenges

+
+ + {/* Generate Challenge Section */} +
+
+

+ + Generate New Challenge +

+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + {/* Challenge Submissions */} +
+

Challenge Submissions

+ + {submissions.length === 0 ? ( +

No submissions yet

+ ) : ( +
+ {submissions.map((submission) => ( +
+
+
+

{submission.title}

+

{submission.description}

+
+ Category: {submission.category} + Difficulty: {submission.difficulty} + Status: {submission.status.toUpperCase()} +
+
+ {submission.status === 'pending' && ( + + )} +
+
+ Submitted: {new Date(submission.created_at).toLocaleString()} +
+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/src/components/ChallengePage.tsx b/src/components/ChallengePage.tsx index 46dcada..879ee09 100644 --- a/src/components/ChallengePage.tsx +++ b/src/components/ChallengePage.tsx @@ -141,8 +141,10 @@ export function ChallengePage({ teamId, teamName, leaderName, onLogout }: Challe const [newNote, setNewNote] = useState(''); const [editingNote, setEditingNote] = useState(null); const [editContent, setEditContent] = useState(''); + const [questions, setQuestions] = useState([]); useEffect(() => { + loadQuestions(); loadChallenge(); }, [teamId]); @@ -179,6 +181,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}`); @@ -224,7 +268,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) { @@ -254,7 +298,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); } diff --git a/supabase/functions/generate-challenge/index.ts b/supabase/functions/generate-challenge/index.ts new file mode 100644 index 0000000..b0e30a5 --- /dev/null +++ b/supabase/functions/generate-challenge/index.ts @@ -0,0 +1,235 @@ +import { serve } from "https://deno.land/std@0.168.0/http/server.ts" +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +} + +interface ChallengeTemplate { + category: string; + difficulty: string; + titleTemplate: string; + descriptionTemplate: string; + hintTemplates: string[]; + generateCipher: (params: any) => { cipher: string; flag: string; assets?: any[] }; +} + +const CHALLENGE_TEMPLATES: ChallengeTemplate[] = [ + { + category: "Cryptography", + difficulty: "Beginner", + titleTemplate: "Caesar Cipher Challenge", + descriptionTemplate: "Decrypt this message encrypted with a Caesar cipher. The shift is between 1-25.", + hintTemplates: [ + "Caesar ciphers shift each letter by a fixed number of positions in the alphabet.", + "Try shifting by 3 positions - it's a common choice.", + "Count the letters and look for patterns that suggest the correct shift." + ], + generateCipher: () => { + const plaintext = "HELLO WORLD"; + const shift = Math.floor(Math.random() * 25) + 1; + const cipher = plaintext.split('').map(char => { + if (char >= 'A' && char <= 'Z') { + return String.fromCharCode(((char.charCodeAt(0) - 65 + shift) % 26) + 65); + } + return char; + }).join(''); + const flag = `CG{${plaintext.toLowerCase().replace(' ', '_')}}`; + return { cipher, flag }; + } + }, + { + category: "Cryptography", + difficulty: "Intermediate", + titleTemplate: "Vigenère Cipher Challenge", + descriptionTemplate: "This message was encrypted using a Vigenère cipher with a repeating keyword. The keyword is a common English word.", + hintTemplates: [ + "Vigenère ciphers use a keyword to determine the shift for each letter.", + "The keyword repeats to match the length of the plaintext.", + "Look for repeated patterns in the ciphertext that might indicate keyword length." + ], + generateCipher: () => { + const plaintext = "ATTACK AT DAWN"; + const keywords = ["LEMON", "CIPHER", "SECRET", "CRYPTO"]; + const keyword = keywords[Math.floor(Math.random() * keywords.length)]; + let cipher = ""; + let keyIndex = 0; + + for (let i = 0; i < plaintext.length; i++) { + const char = plaintext[i]; + if (char >= 'A' && char <= 'Z') { + const shift = keyword[keyIndex % keyword.length].charCodeAt(0) - 65; + cipher += String.fromCharCode(((char.charCodeAt(0) - 65 + shift) % 26) + 65); + keyIndex++; + } else { + cipher += char; + } + } + + const flag = `CG{${plaintext.toLowerCase().replace(/ /g, '_')}}`; + return { cipher, flag }; + } + }, + { + category: "Programming", + difficulty: "Beginner", + titleTemplate: "Array Sum Challenge", + descriptionTemplate: "Given an array of integers, find two numbers that add up to the target sum. Return their indices.", + hintTemplates: [ + "You can solve this efficiently using a hash map to store numbers you've seen.", + "For each number, check if target - current exists in your map.", + "This approach runs in O(n) time instead of O(n²)." + ], + generateCipher: () => { + const nums = [2, 7, 11, 15]; + const target = 9; + const flag = "CG{[0,1]}"; + return { cipher: `nums = [${nums.join(', ')}], target = ${target}`, flag }; + } + }, + { + category: "Steganography", + difficulty: "Intermediate", + titleTemplate: "Hidden Message Challenge", + descriptionTemplate: "This text file contains a hidden message encoded using zero-width Unicode characters. Extract the invisible text.", + hintTemplates: [ + "Zero-width characters (U+200B, U+200C, U+200D, U+200E, U+200F) are invisible but present in the file.", + "Use a hex editor or specialized tool to view the raw bytes.", + "The invisible characters represent binary data that can be converted to ASCII." + ], + generateCipher: () => { + const message = "SECRET"; + let hiddenText = "This is a normal sentence."; + + // Convert message to binary, then to zero-width characters + const binary = message.split('').map(char => + char.charCodeAt(0).toString(2).padStart(8, '0') + ).join(''); + + for (const bit of binary) { + if (bit === '0') hiddenText += '\u200B'; // ZERO WIDTH SPACE + else hiddenText += '\u200C'; // ZERO WIDTH NON-JOINER + } + + const flag = `CG{${message}}`; + return { cipher: hiddenText, flag }; + } + } +]; + +serve(async (req) => { + // Handle CORS preflight requests + if (req.method === 'OPTIONS') { + return new Response('ok', { headers: corsHeaders }) + } + + try { + // Create a Supabase client with the Auth context of the logged in user + const supabaseClient = createClient( + Deno.env.get('SUPABASE_URL') ?? '', + Deno.env.get('SUPABASE_ANON_KEY') ?? '', + { + global: { + headers: { Authorization: req.headers.get('Authorization')! }, + }, + } + ) + + // Get the request body + const { category, difficulty } = await req.json() + + // Select a random template matching the criteria + const matchingTemplates = CHALLENGE_TEMPLATES.filter(t => + (!category || t.category === category) && + (!difficulty || t.difficulty === difficulty) + ); + + if (matchingTemplates.length === 0) { + throw new Error('No templates found for the specified criteria'); + } + + const template = matchingTemplates[Math.floor(Math.random() * matchingTemplates.length)]; + + // Generate the cipher/challenge content + const { cipher, flag, assets } = template.generateCipher({}); + + // Generate a new challenge ID + const challengeId = `q${Date.now()}`; + + // Create the challenge directory and files + const challengeDir = `public/challenges/${challengeId}`; + + // Create challenge.json + const challengeData = { + category: template.category, + difficulty: template.difficulty + }; + + // Create the challenge content + const challengeContent = { + id: challengeId, + title: template.titleTemplate, + description: template.descriptionTemplate.replace('[CIPHER]', cipher), + file_name: assets ? `challenge_assets_${challengeId}.txt` : '', + file_path: assets ? `/challenges/${challengeId}/challenge_assets_${challengeId}.txt` : '', + correct_flag: flag, + hints: template.hintTemplates, + category: template.category, + difficulty: template.difficulty + }; + + // Insert into challenges table + const { data: insertedChallenge, error: insertError } = await supabaseClient + .from('challenges') + .insert({ + title: challengeContent.title, + description: challengeContent.description, + category: challengeContent.category, + difficulty: challengeContent.difficulty, + correct_flag: challengeContent.correct_flag, + hints: challengeContent.hints, + file_name: challengeContent.file_name, + file_path: challengeContent.file_path, + active: true + }) + .select() + .single(); + + if (insertError) { + throw new Error(`Failed to insert challenge: ${insertError.message}`); + } + + // In a real implementation, you would create the actual files + // For now, we'll just return the challenge data + // The files would be created using Deno's file system APIs + + return new Response( + JSON.stringify({ + success: true, + message: `Challenge "${challengeContent.title}" has been generated successfully!`, + challenge_id: challengeId, + challenge: insertedChallenge, + challenge_data: challengeContent + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 200, + } + ) + + } catch (error) { + console.error('Error generating challenge:', error) + + return new Response( + JSON.stringify({ + success: false, + error: error.message + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 400, + } + ) + } +})