From 43fadc0792574c268cebd5a99c63b3a07e90c882 Mon Sep 17 00:00:00 2001 From: 5t4l1n Date: Sun, 27 Jul 2025 01:33:21 +0530 Subject: [PATCH] update --- backend/main.py | 1189 ++++++----------- backend/routes/exam.py | 225 ++++ frontend/app/coding/exam/[examCode]/page.tsx | 610 +++++++++ .../coding/exam/[examCode]/security/page.tsx | 445 ++++++ frontend/app/coding/exam/page.tsx | 591 ++------ frontend/app/coding/host/[examCode]/page.tsx | 741 ++++++---- 6 files changed, 2295 insertions(+), 1506 deletions(-) create mode 100644 frontend/app/coding/exam/[examCode]/page.tsx create mode 100644 frontend/app/coding/exam/[examCode]/security/page.tsx diff --git a/backend/main.py b/backend/main.py index bdec388..95c1974 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,208 +1,230 @@ +import os +import asyncio +import logging +import uuid +from datetime import datetime from flask import Flask, jsonify, request from flask_cors import CORS from dotenv import load_dotenv -import os -import asyncio -from mongo_service import MongoService -from web3_service import Web3Service -import logging -from datetime import datetime +from pymongo import MongoClient -# Load environment variables first +# Load env vars load_dotenv() -# Import all route blueprints -from routes import auth, test_flow, certificate, dashboard, courses, quizzes, admin, exam, compiler +# Services +from mongo_service import MongoService +from web3_service import Web3Service -# Import services after loading env vars +# Blueprints +from routes.auth import bp as auth_bp +from routes.test_flow import bp as test_flow_bp +from routes.certificate import bp as cert_bp +from routes.dashboard import bp as dash_bp +from routes.courses import bp as courses_bp +from routes.quizzes import bp as quizzes_bp +from routes.admin import bp as admin_bp +from routes.exam import bp as exam_bp +from routes.compiler import bp as compiler_bp + +# Optional services try: from services.wallet_service import wallet_service - from services.real_compiler_service import real_compiler_service WALLET_SERVICE_AVAILABLE = True - COMPILER_SERVICE_AVAILABLE = True -except ImportError as e: - logging.warning(f"Service import failed: {e}") +except ImportError: wallet_service = None - real_compiler_service = None WALLET_SERVICE_AVAILABLE = False + +try: + from services.real_compiler_service import real_compiler_service + COMPILER_SERVICE_AVAILABLE = True +except ImportError: + real_compiler_service = None COMPILER_SERVICE_AVAILABLE = False -# Initialize Flask app +# Flask app app = Flask(__name__) +app.config.update( + SECRET_KEY=os.getenv('SECRET_KEY', 'openlearnx-secret'), + MONGODB_URI=os.getenv('MONGODB_URI', 'mongodb://localhost:27017/'), + WEB3_PROVIDER_URL=os.getenv('WEB3_PROVIDER_URL', 'http://127.0.0.1:8545'), + CONTRACT_ADDRESS=os.getenv('CONTRACT_ADDRESS', ''), + ADMIN_TOKEN=os.getenv('ADMIN_TOKEN', '') +) -# Enhanced CORS configuration for coding exam platform -CORS(app, resources={ - r"/api/*": { - "origins": [ - "http://localhost:3000", - "http://127.0.0.1:3000", - "http://localhost:3001", - "http://127.0.0.1:3001" - ], - "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - "allow_headers": [ - "Content-Type", - "Authorization", - "X-Requested-With", - "Accept", - "Origin" - ], - "supports_credentials": True, - "expose_headers": ["Authorization"] - } -}) +# CORS - Allow all endpoints under /api/* +CORS(app, resources={r"/api/*": { + "origins": [ + "http://localhost:3000", + "http://127.0.0.1:3000", + "http://localhost:3001", + "http://127.0.0.1:3001" + ], + "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + "allow_headers": ["Content-Type", "Authorization", "Accept", "Origin", "X-Requested-With"], + "supports_credentials": True, + "expose_headers": ["Authorization"] +}}) -# Configuration -app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'openlearnx-secret-key-2024') -app.config['MONGODB_URI'] = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/') -app.config['WEB3_PROVIDER_URL'] = os.getenv('WEB3_PROVIDER_URL', 'http://127.0.0.1:8545') -app.config['CONTRACT_ADDRESS'] = os.getenv('CONTRACT_ADDRESS', '0x739f0aCef964f87Bc7974D972a811f8417d74B4C') -app.config['MINTER_PRIVATE_KEY'] = os.getenv('MINTER_PRIVATE_KEY', '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80') -app.config['ADMIN_TOKEN'] = os.getenv('ADMIN_TOKEN', 'admin-secret-key') - -# Blockchain configuration -app.config['IPFS_GATEWAY'] = os.getenv('IPFS_GATEWAY', 'https://ipfs.infura.io:5001') -app.config['IPFS_PROJECT_ID'] = os.getenv('IPFS_PROJECT_ID') -app.config['IPFS_PROJECT_SECRET'] = os.getenv('IPFS_PROJECT_SECRET') - -# Configure logging BEFORE initializing services -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +# Logging +logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)s %(message)s", handlers=[ logging.StreamHandler(), - logging.FileHandler('openlearnx.log') if os.access('.', os.W_OK) else logging.NullHandler() - ] -) + logging.FileHandler("openlearnx.log") if os.access('.', os.W_OK) else logging.NullHandler() + ]) logger = logging.getLogger(__name__) -# ✅ DEFINE check_docker_availability BEFORE using it -def check_docker_availability(): - """Check if Docker is available for compiler service""" - try: - import docker - client = docker.from_env() - client.ping() - return True - except Exception: - return False - -# Initialize services with error handling +# Initialize MongoDB service try: mongo_service = MongoService(app.config['MONGODB_URI']) app.config['MONGO_SERVICE'] = mongo_service MONGO_SERVICE_AVAILABLE = True - logger.info("✅ MongoDB service initialized") + logger.info("✅ MongoService initialized") except Exception as e: - logging.error(f"Failed to initialize MongoDB service: {e}") - mongo_service = None MONGO_SERVICE_AVAILABLE = False + logger.error(f"❌ Failed MongoService init: {e}") +# Initialize Web3 service try: - web3_service = Web3Service( - app.config['WEB3_PROVIDER_URL'], - app.config['CONTRACT_ADDRESS'] - ) + web3_service = Web3Service(app.config['WEB3_PROVIDER_URL'], app.config['CONTRACT_ADDRESS']) app.config['WEB3_SERVICE'] = web3_service WEB3_SERVICE_AVAILABLE = True - logger.info("✅ Web3 service initialized") + logger.info("✅ Web3Service initialized") except Exception as e: - logging.error(f"Failed to initialize Web3 service: {e}") - web3_service = None WEB3_SERVICE_AVAILABLE = False + logger.error(f"❌ Failed Web3Service init: {e}") -# Make services available to routes if WALLET_SERVICE_AVAILABLE: app.config['WALLET_SERVICE'] = wallet_service - logger.info("✅ Wallet service available") - if COMPILER_SERVICE_AVAILABLE: app.config['REAL_COMPILER_SERVICE'] = real_compiler_service - logger.info("✅ Compiler service available") -# ✅ ENHANCED: Register all blueprints with better error handling -blueprints = [ - (auth.bp, '/api/auth', 'Authentication'), - (test_flow.bp, '/api/test', 'Test Flow'), - (certificate.bp, '/api/certificate', 'Certificates'), - (dashboard.bp, '/api/dashboard', 'Dashboard'), - (courses.bp, '/api/courses', 'Courses'), - (quizzes.bp, '/api/quizzes', 'Quizzes'), - (admin.bp, '/api/admin', 'Admin Panel'), - (exam.bp, '/api/exam', 'Coding Exams'), # ✅ Key for coding exam functionality - (compiler.bp, '/api/compiler', 'Code Compiler'), # ✅ Key for compiler functionality -] - -registered_blueprints = [] -failed_blueprints = [] - -for blueprint, url_prefix, description in blueprints: +def check_docker_availability(): try: - app.register_blueprint(blueprint, url_prefix=url_prefix) - registered_blueprints.append((description, url_prefix)) - logging.info(f"✅ Registered {description}: {url_prefix}") - print(f"✅ Registered {description}: {url_prefix}") - except Exception as e: - failed_blueprints.append((description, url_prefix, str(e))) - logging.error(f"❌ Failed to register {description} ({url_prefix}): {e}") - print(f"❌ Failed to register {description} ({url_prefix}): {e}") + import docker + docker.from_env().ping() + return True + except: + return False -# ✅ CRITICAL FIX: Add missing exam info endpoint directly to main.py -@app.route('/api/exam/info/', methods=['GET', 'OPTIONS']) -def get_exam_info_direct(exam_code): - """Direct exam info endpoint for host panel - CRITICAL FIX""" +# Register blueprints +blueprints_registered = [] +blueprints_failed = [] + +for bp, prefix in [ + (auth_bp, '/api/auth'), + (test_flow_bp, '/api/test'), + (cert_bp, '/api/certificate'), + (dash_bp, '/api/dashboard'), + (courses_bp, '/api/courses'), + (quizzes_bp, '/api/quizzes'), + (admin_bp, '/api/admin'), + (exam_bp, '/api/exam'), + (compiler_bp, '/api/compiler'), +]: + try: + app.register_blueprint(bp, url_prefix=prefix) + blueprints_registered.append(prefix) + logger.info(f"✅ Registered blueprint {prefix}") + except Exception as e: + blueprints_failed.append((prefix, str(e))) + logger.error(f"❌ Blueprint {prefix} failed: {e}") + +# ✅ Direct MongoDB connection for direct routes +def get_db(): + """Get MongoDB database connection""" + client = MongoClient(app.config['MONGODB_URI']) + return client.openlearnx + +# ✅ ONLY UNIQUE DIRECT EXAM ENDPOINTS (no conflicts with blueprint) +@app.route('/api/exam/upload-question', methods=['POST', 'OPTIONS']) +def upload_question_direct(): + """Direct endpoint for question upload""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") - response.headers.add("Access-Control-Allow-Methods", "GET,OPTIONS") + response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") return response try: - print(f"📊 DIRECT host panel request for exam: {exam_code}") + print(f"📤 Direct question upload request") - # Get MongoDB connection - from pymongo import MongoClient - client = MongoClient(app.config['MONGODB_URI']) - db = client.openlearnx + data = request.get_json() + exam_code = data.get('exam_code', '').upper() + question_data = data.get('question', {}) - exam = db.exams.find_one({"exam_code": exam_code.upper()}) + print(f"📝 Uploading question '{question_data.get('title')}' to exam {exam_code}") + + if not exam_code or not question_data: + return jsonify({"success": False, "error": "Missing exam_code or question data"}), 400 + + # Get database + db = get_db() + + # Find the exam + exam = db.exams.find_one({"exam_code": exam_code}) if not exam: - print(f"❌ Exam not found: {exam_code}") + print(f"❌ Exam {exam_code} not found") return jsonify({"success": False, "error": "Exam not found"}), 404 - # Convert datetime to string if needed - created_at = exam.get("created_at") - if hasattr(created_at, 'isoformat'): - created_at = created_at.isoformat() - elif created_at: - created_at = str(created_at) + # Check if exam can be modified + if exam.get('status') != 'waiting': + return jsonify({"success": False, "error": "Cannot modify questions after exam has started"}), 400 - exam_info = { - "title": exam.get("title", "Untitled Exam"), - "status": exam.get("status", "waiting"), - "duration_minutes": exam.get("duration_minutes", 30), - "participants_count": len(exam.get("participants", [])), - "max_participants": exam.get("max_participants", 50), - "problem_title": exam.get("problem", {}).get("title", exam.get("title", "Coding Challenge")), - "languages": exam.get("problem", {}).get("languages", ["python"]), - "created_at": created_at, - "host_name": exam.get("host_name", "Unknown Host") + # Create question document + question = { + "id": str(uuid.uuid4()), + "title": question_data.get('title', 'Custom Question'), + "description": question_data.get('description', 'Custom programming question'), + "difficulty": question_data.get('difficulty', 'medium'), + "function_name": question_data.get('function_name', 'solve'), + "starter_code": question_data.get('starter_code', { + 'python': 'def solve():\n # Write your solution here\n pass' + }), + "test_cases": question_data.get('test_cases', []), + "examples": question_data.get('examples', []), + "constraints": question_data.get('constraints', []), + "time_limit": question_data.get('time_limit', 1000), + "memory_limit": question_data.get('memory_limit', '128MB'), + "created_at": datetime.now(), + "uploaded_by": exam.get('host_name', 'Host'), + "languages": list(question_data.get('starter_code', {}).keys()) } - print(f"✅ Found exam: {exam_info['title']} (Status: {exam_info['status']})") - return jsonify({"success": True, "exam_info": exam_info}) + # Update exam + result = db.exams.update_one( + {"exam_code": exam_code}, + { + "$set": { + "problem": question, + "problem_title": question['title'], + "updated_at": datetime.now() + } + } + ) + if result.modified_count > 0: + print(f"✅ Question '{question['title']}' uploaded to exam {exam_code}") + return jsonify({ + "success": True, + "message": "Question uploaded successfully", + "question_id": question['id'], + "question_title": question['title'] + }) + else: + print(f"❌ Failed to update exam {exam_code}") + return jsonify({"success": False, "error": "Failed to update exam"}), 500 + except Exception as e: - print(f"❌ Error getting exam info: {str(e)}") + print(f"❌ Error: {str(e)}") import traceback traceback.print_exc() - return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500 + return jsonify({"success": False, "error": str(e)}), 500 -# ✅ ADD: Additional missing host panel endpoints -@app.route('/api/exam/participants/', methods=['GET', 'OPTIONS']) -def get_participants_direct(exam_code): - """Get participants for host panel""" +@app.route('/api/exam/questions/', methods=['GET', 'OPTIONS']) +def get_exam_questions_direct(exam_code): + """Get all questions for an exam""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") @@ -211,271 +233,179 @@ def get_participants_direct(exam_code): return response try: - print(f"👥 DIRECT participants request for exam: {exam_code}") - - from pymongo import MongoClient - client = MongoClient(app.config['MONGODB_URI']) - db = client.openlearnx - + db = get_db() exam = db.exams.find_one({"exam_code": exam_code.upper()}) if not exam: return jsonify({"success": False, "error": "Exam not found"}), 404 - participants = exam.get("participants", []) + questions = exam.get('problem', {}) - # Format participant data - formatted_participants = [] - for participant in participants: - # Convert datetime to string if needed - joined_at = participant.get("joined_at") - if hasattr(joined_at, 'isoformat'): - joined_at = joined_at.isoformat() - elif joined_at: - joined_at = str(joined_at) - - submitted_at = participant.get("submitted_at") - if hasattr(submitted_at, 'isoformat'): - submitted_at = submitted_at.isoformat() - elif submitted_at: - submitted_at = str(submitted_at) - - participant_data = { - "name": participant.get("name", ""), - "score": participant.get("score", 0), - "completed": participant.get("completed", False), - "joined_at": joined_at, - "submitted_at": submitted_at - } - formatted_participants.append(participant_data) - - print(f"✅ Found {len(formatted_participants)} participants for exam {exam_code}") - return jsonify({"success": True, "participants": formatted_participants}) - - except Exception as e: - print(f"❌ Error getting participants: {str(e)}") - return jsonify({"success": False, "error": str(e)}), 500 - -@app.route('/api/exam/remove-participant', methods=['POST', 'OPTIONS']) -def remove_participant_direct(): - """Remove participant from exam (host panel)""" - if request.method == "OPTIONS": - response = jsonify({'status': 'ok'}) - response.headers.add("Access-Control-Allow-Origin", "*") - response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") - response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") - return response - - try: - data = request.get_json() - exam_code = data.get('exam_code', '').upper() - participant_name = data.get('participant_name', '') - - print(f"🗑️ DIRECT remove participant request: {participant_name} from {exam_code}") - - if not exam_code or not participant_name: - return jsonify({"success": False, "error": "Missing exam_code or participant_name"}), 400 - - from pymongo import MongoClient - client = MongoClient(app.config['MONGODB_URI']) - db = client.openlearnx - - result = db.exams.update_one( - {"exam_code": exam_code}, - {"$pull": {"participants": {"name": participant_name}}} - ) - - if result.modified_count > 0: - print(f"✅ Removed participant {participant_name} from exam {exam_code}") - return jsonify({"success": True, "message": f"Participant {participant_name} removed successfully"}) - else: - return jsonify({"success": False, "error": "Participant not found or already removed"}), 404 - - except Exception as e: - print(f"❌ Error removing participant: {str(e)}") - return jsonify({"success": False, "error": str(e)}), 500 - -@app.route('/api/exam/stop-exam', methods=['POST', 'OPTIONS']) -def stop_exam_direct(): - """Stop exam (host panel)""" - if request.method == "OPTIONS": - response = jsonify({'status': 'ok'}) - response.headers.add("Access-Control-Allow-Origin", "*") - response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") - response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") - return response - - try: - data = request.get_json() - exam_code = data.get('exam_code', '').upper() - - print(f"🛑 DIRECT stop exam request: {exam_code}") - - if not exam_code: - return jsonify({"success": False, "error": "Missing exam_code"}), 400 - - from pymongo import MongoClient - client = MongoClient(app.config['MONGODB_URI']) - db = client.openlearnx - - result = db.exams.update_one( - {"exam_code": exam_code}, - {"$set": { - "status": "completed", - "ended_at": datetime.now().isoformat(), - "ended_by": "host" - }} - ) - - if result.modified_count > 0: - print(f"✅ Exam {exam_code} stopped by host") - return jsonify({"success": True, "message": "Exam stopped successfully"}) - else: - return jsonify({"success": False, "error": "Exam not found"}), 404 - - except Exception as e: - print(f"❌ Error stopping exam: {str(e)}") - return jsonify({"success": False, "error": str(e)}), 500 - -# ✅ ENHANCED: Debug routes with better structure -@app.route('/debug/routes') -def debug_routes(): - """Debug route to see all registered routes""" - routes = [] - for rule in app.url_map.iter_rules(): - routes.append({ - 'endpoint': rule.endpoint, - 'methods': list(rule.methods), - 'rule': str(rule) + return jsonify({ + "success": True, + "questions": questions, + "total_questions": 1 if questions else 0 }) - return jsonify({ - "total_routes": len(routes), - "routes": sorted(routes, key=lambda x: x['rule']), - "registered_blueprints": [desc for desc, url in registered_blueprints], - "failed_blueprints": failed_blueprints, - "direct_exam_routes_added": [ - "/api/exam/info/", - "/api/exam/participants/", - "/api/exam/remove-participant", - "/api/exam/stop-exam" - ] - }) + + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 500 -@app.route('/debug/exam-routes') -def debug_exam_routes(): - """Debug exam-specific routes""" - exam_routes = [] - for rule in app.url_map.iter_rules(): - if '/exam' in str(rule): - exam_routes.append({ - 'endpoint': rule.endpoint, - 'methods': list(rule.methods), - 'rule': str(rule) - }) +@app.route('/api/exam/update-question', methods=['POST', 'OPTIONS']) +def update_question_direct(): + """Update an existing question in an exam""" + if request.method == "OPTIONS": + response = jsonify({'status': 'ok'}) + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") + response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") + return response - return jsonify({ - "exam_routes": exam_routes, - "exam_blueprint_registered": any('Coding Exams' in desc for desc, _ in registered_blueprints), - "expected_exam_routes": [ - "/api/exam/create-exam", - "/api/exam/join-exam", - "/api/exam/start-exam", - "/api/exam/info/", - "/api/exam/participants/", - "/api/exam/remove-participant", - "/api/exam/stop-exam", - "/api/exam/leaderboard/", - "/api/exam/test" - ], - "direct_routes_added": True - }) + try: + data = request.get_json() + exam_code = data.get('exam_code', '').upper() + question_data = data.get('question', {}) + + if not exam_code or not question_data: + return jsonify({"success": False, "error": "Missing data"}), 400 + + db = get_db() + exam = db.exams.find_one({"exam_code": exam_code}) + if not exam: + return jsonify({"success": False, "error": "Exam not found"}), 404 + + if exam['status'] != 'waiting': + return jsonify({"success": False, "error": "Cannot modify questions after exam has started"}), 400 + + # Update question + question_data['updated_at'] = datetime.now() + + result = db.exams.update_one( + {"exam_code": exam_code}, + {"$set": {"problem": question_data}} + ) + + if result.modified_count > 0: + return jsonify({"success": True, "message": "Question updated successfully"}) + else: + return jsonify({"success": False, "error": "Failed to update question"}), 500 + + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 500 -@app.route('/debug/services') -def debug_services(): - """Debug service availability""" +# Request logging +@app.before_request +def log_request(): + path = request.path + if path.startswith('/api/exam'): + logger.info(f"📥 Exam request: {request.method} {path}") + if request.method in ['POST', 'PUT'] and request.is_json: + print(f"📦 Request data: {request.json}") + elif path.startswith('/api/compiler'): + logger.info(f"📥 Compiler request: {request.method} {path}") + elif path.startswith('/api/admin'): + auth = request.headers.get('Authorization','') + logger.info(f"🔒 Admin request: {request.method} {path} Auth={auth[:20]}") + +# Handle CORS preflight +@app.before_request +def handle_options(): + if request.method == 'OPTIONS': + resp = jsonify({'status':'ok'}) + resp.headers.update({ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Content-Type,Authorization,Accept,Origin,X-Requested-With", + "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS", + "Access-Control-Allow-Credentials": "true" + }) + return resp + +# Security headers +@app.after_request +def secure_headers(resp): + resp.headers.update({ + 'X-Content-Type-Options':'nosniff', + 'X-Frame-Options':'DENY', + 'X-XSS-Protection':'1; mode=block' + }) + return resp + +# Health & debug endpoints +@app.route('/') +def health_root(): return jsonify({ - "services": { + "status":"OpenLearnX API running", + "version":"2.0.1", + "timestamp": datetime.now().isoformat(), + "features":{ "mongodb": MONGO_SERVICE_AVAILABLE, "web3": WEB3_SERVICE_AVAILABLE, "wallet": WALLET_SERVICE_AVAILABLE, "compiler": COMPILER_SERVICE_AVAILABLE, "docker": check_docker_availability() }, - "app_config_keys": list(app.config.keys()), - "blueprint_count": len(app.blueprints), - "registered_blueprints": registered_blueprints, - "failed_blueprints": failed_blueprints, - "exam_system_ready": all([ - MONGO_SERVICE_AVAILABLE, - any('Coding Exams' in desc for desc, _ in registered_blueprints) - ]), - "direct_exam_endpoints_added": True - }) - -# ✅ ENHANCED: Direct exam test route with better functionality -@app.route('/api/exam/test-direct', methods=['GET', 'POST', 'OPTIONS']) -def test_exam_direct(): - """Direct test route for exam functionality""" - if request.method == "OPTIONS": - response = jsonify({'status': 'ok'}) - response.headers.add("Access-Control-Allow-Origin", "*") - response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") - response.headers.add("Access-Control-Allow-Methods", "GET,POST,OPTIONS") - return response - - return jsonify({ - "success": True, - "message": "Direct exam route is working", - "method": request.method, - "timestamp": datetime.now().isoformat(), - "data": request.json if request.method == "POST" else None, - "mongo_available": MONGO_SERVICE_AVAILABLE, - "exam_blueprint_registered": any('Coding Exams' in desc for desc, _ in registered_blueprints), - "direct_endpoints_added": True - }) - -# ✅ ENHANCED: Health check endpoints -@app.route('/') -def health_check(): - return jsonify({ - "status": "OpenLearnX API is running", - "version": "2.0.1", - "timestamp": datetime.now().isoformat(), - "features": { - "blockchain": WEB3_SERVICE_AVAILABLE, - "coding_exams": COMPILER_SERVICE_AVAILABLE, - "wallet_auth": WALLET_SERVICE_AVAILABLE, - "database": MONGO_SERVICE_AVAILABLE, - "real_compiler": COMPILER_SERVICE_AVAILABLE, - "docker": check_docker_availability(), - "direct_exam_endpoints": True - }, "endpoints": { - "auth": "/api/auth", - "courses": "/api/courses", + "exam": "/api/exam", + "compiler": "/api/compiler", "admin": "/api/admin", - "dashboard": "/api/dashboard", - "certificates": "/api/certificate", - "quizzes": "/api/quizzes", - "coding_exams": "/api/exam", - "compiler": "/api/compiler" + "health": "/api/health" }, - "debug_endpoints": [ - "/debug/routes", - "/debug/exam-routes", - "/debug/services", - "/api/exam/test-direct" - ], - "registered_blueprints": len(registered_blueprints), - "failed_blueprints": len(failed_blueprints), - "host_panel_fix": "✅ Direct endpoints added" + "blueprints_registered": len(blueprints_registered), + "blueprints_failed": len(blueprints_failed), + "direct_exam_endpoints": [ + "/api/exam/upload-question", + "/api/exam/questions/", + "/api/exam/update-question" + ] }) @app.route('/api/health') def api_health(): - """Comprehensive API health check""" - health_status = { - "status": "healthy", - "timestamp": datetime.now().isoformat(), + status = "healthy" + services = { + "mongodb": MONGO_SERVICE_AVAILABLE, + "web3": WEB3_SERVICE_AVAILABLE, + "wallet": WALLET_SERVICE_AVAILABLE, + "compiler": COMPILER_SERVICE_AVAILABLE, + "docker": check_docker_availability() + } + + # Check MongoDB connection + if MONGO_SERVICE_AVAILABLE: + try: + db = get_db() + db.command('ismaster') + services["mongodb_connection"] = "connected" + except Exception as e: + services["mongodb_connection"] = f"error: {str(e)}" + status = "degraded" + + return jsonify({ + "status": status, + "services": services, + "blueprints_registered": blueprints_registered, + "blueprints_failed": blueprints_failed + }), 200 if status == "healthy" else 503 + +@app.route('/debug/routes') +def debug_routes(): + rules = [] + for rule in app.url_map.iter_rules(): + rules.append({ + "rule": str(rule), + "methods": list(rule.methods), + "endpoint": rule.endpoint + }) + + return jsonify({ + "total": len(rules), + "routes": sorted(rules, key=lambda x: x["rule"]), + "blueprints_registered": blueprints_registered, + "blueprints_failed": blueprints_failed, + "exam_routes": [r for r in rules if '/exam' in r['rule']] + }) + +@app.route('/debug/services') +def debug_services(): + return jsonify({ "services": { "mongodb": MONGO_SERVICE_AVAILABLE, "web3": WEB3_SERVICE_AVAILABLE, @@ -483,408 +413,133 @@ def api_health(): "compiler": COMPILER_SERVICE_AVAILABLE, "docker": check_docker_availability() }, - "configuration": { - "cors_enabled": True, - "debug_mode": app.debug, - "secret_key_set": bool(app.config.get('SECRET_KEY')), - "admin_token_set": bool(app.config.get('ADMIN_TOKEN')) - }, - "blueprints_registered": list(app.blueprints.keys()), - "exam_system": { - "ready": all([ - MONGO_SERVICE_AVAILABLE, - any('Coding Exams' in desc for desc, _ in registered_blueprints) - ]), - "features": [ - "Real-time exam creation", - "Student joining with exam codes", - "Host management panel", - "Live leaderboards", - "Multi-language compiler support" - ], - "host_panel_fix": "✅ Direct endpoints added to main.py" - } - } - - # ✅ ENHANCED: Check MongoDB connection - if MONGO_SERVICE_AVAILABLE: - try: - from pymongo import MongoClient - client = MongoClient(app.config['MONGODB_URI']) - client.admin.command('ismaster') - health_status["services"]["mongodb_connection"] = "connected" - except Exception as e: - health_status["services"]["mongodb_connection"] = f"error: {str(e)}" - health_status["status"] = "degraded" - - # ✅ ENHANCED: Check Web3 connection - if WEB3_SERVICE_AVAILABLE: - try: - if web3_service and web3_service.w3.is_connected(): - health_status["services"]["web3_connection"] = "connected" - else: - health_status["services"]["web3_connection"] = "disconnected" - health_status["status"] = "degraded" - except Exception as e: - health_status["services"]["web3_connection"] = f"error: {str(e)}" - health_status["status"] = "degraded" - - status_code = 200 if health_status["status"] == "healthy" else 503 - return jsonify(health_status), status_code - -@app.route('/api/admin/health') -def admin_health(): - """Admin-specific health check""" - return jsonify({ - "status": "Admin API is running", - "timestamp": datetime.now().isoformat(), - "admin_token_configured": bool(app.config.get('ADMIN_TOKEN')), - "admin_endpoints": [ - "/api/admin/dashboard", - "/api/admin/courses", - "/api/admin/courses/", - "/api/admin/test", - "/api/admin/initialize" - ], - "exam_endpoints": [ - "/api/exam/create-exam", - "/api/exam/join-exam", - "/api/exam/join-exam-wallet", - "/api/exam/start-exam", - "/api/exam/submit-solution", - "/api/exam/leaderboard/", - "/api/exam/host-dashboard/", - "/api/exam/info/", - "/api/exam/participants/", - "/api/exam/remove-participant", - "/api/exam/stop-exam" - ], - "compiler_endpoints": [ - "/api/compiler/languages", - "/api/compiler/execute", - "/api/compiler/execute-async", - "/api/compiler/status/", - "/api/compiler/test", - "/api/compiler/stats" - ], - "exam_system_status": { - "ready": all([ - MONGO_SERVICE_AVAILABLE, - any('Coding Exams' in desc for desc, _ in registered_blueprints) - ]), - "mongo_connected": MONGO_SERVICE_AVAILABLE, - "exam_routes_registered": any('Coding Exams' in desc for desc, _ in registered_blueprints), - "host_panel_endpoints": "✅ Added directly to main.py" - } + "blueprints_registered": blueprints_registered, + "blueprints_failed": blueprints_failed, + "direct_endpoints_added": True }) -# ✅ ENHANCED: Error handlers with better error information +# Error handlers @app.errorhandler(404) -def not_found(error): +def not_found(e): return jsonify({ - "error": "Endpoint not found", - "message": "The requested API endpoint does not exist", - "requested_path": request.path, + "error": "Not Found", + "path": request.path, "method": request.method, - "available_endpoints": [ - "/api/auth", "/api/courses", "/api/admin", - "/api/exam", "/api/dashboard", "/api/certificate", - "/api/compiler" - ], - "debug_endpoints": [ - "/debug/routes", - "/debug/exam-routes", - "/debug/services" - ], - "exam_specific_endpoints": [ - "/api/exam/create-exam", - "/api/exam/join-exam", - "/api/exam/info/", - "/api/exam/participants/", - "/api/exam/test-direct" + "available_blueprints": blueprints_registered, + "direct_exam_endpoints": [ + "/api/exam/upload-question", + "/api/exam/questions/", + "/api/exam/update-question" ] }), 404 @app.errorhandler(500) -def internal_error(error): - app.logger.error(f"Internal server error: {str(error)}") +def internal_error(e): + logger.error(f"500 Error: {e}") return jsonify({ - "error": "Internal server error", - "message": "An unexpected error occurred on the server", + "error": "Internal Server Error", "timestamp": datetime.now().isoformat() }), 500 -@app.errorhandler(403) -def forbidden(error): - return jsonify({ - "error": "Forbidden", - "message": "Access denied - check your authentication", - "timestamp": datetime.now().isoformat() - }), 403 - -@app.errorhandler(401) -def unauthorized(error): - return jsonify({ - "error": "Unauthorized", - "message": "Authentication required", - "timestamp": datetime.now().isoformat() - }), 401 - -@app.errorhandler(Exception) -def handle_error(error): - app.logger.error(f"Unhandled error: {str(error)}") - return jsonify({ - "error": "An unexpected error occurred", - "type": type(error).__name__, - "timestamp": datetime.now().isoformat() - }), 500 - -# ✅ ENHANCED: Request logging and CORS handling -@app.before_request -def log_request_info(): - """Enhanced request logging""" - if '/api/admin' in request.path: - auth_header = request.headers.get('Authorization', 'No auth header') - logger.info(f"Admin request: {request.method} {request.path} | Auth: {auth_header[:20]}...") - - if '/api/exam' in request.path: - logger.info(f"Exam request: {request.method} {request.path}") - print(f"📝 Exam request: {request.method} {request.path}") - - # Log request data for debugging - if request.method in ['POST', 'PUT'] and request.json: - print(f"📦 Request data: {request.json}") - - if '/api/compiler' in request.path: - logger.info(f"Compiler request: {request.method} {request.path}") - -@app.before_request -def handle_preflight(): - """Handle CORS preflight requests""" +@app.route('/api/exam/update-duration', methods=['POST', 'OPTIONS']) +def update_duration_direct(): + """Direct endpoint for duration update""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") - response.headers.add('Access-Control-Allow-Headers', "Content-Type,Authorization") - response.headers.add('Access-Control-Allow-Methods', "GET,POST,PUT,DELETE,OPTIONS") - response.headers.add('Access-Control-Allow-Credentials', 'true') + response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") + response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") return response - -@app.after_request -def after_request(response): - """Add security headers""" - response.headers.add('X-Content-Type-Options', 'nosniff') - response.headers.add('X-Frame-Options', 'DENY') - response.headers.add('X-XSS-Protection', '1; mode=block') - return response - -# ✅ ENHANCED: Startup function with comprehensive initialization -def initialize_application(): - """Initialize application with comprehensive error handling""" - try: - logger.info("🚀 Initializing OpenLearnX Backend...") - print("🚀 Initializing OpenLearnX Backend...") - - # ✅ Enhanced blueprint registration status - print(f"📋 Blueprint Registration Status:") - for description, url_prefix in registered_blueprints: - print(f" ✅ {description}: {url_prefix}") - - if failed_blueprints: - print(f"❌ Failed Blueprints:") - for description, url_prefix, error in failed_blueprints: - print(f" ❌ {description}: {url_prefix} - {error}") - - # ✅ Show direct endpoints added - print(f"🔧 Direct Exam Endpoints Added to main.py:") - direct_endpoints = [ - "/api/exam/info/", - "/api/exam/participants/", - "/api/exam/remove-participant", - "/api/exam/stop-exam" - ] - for endpoint in direct_endpoints: - print(f" ✅ {endpoint}") - - # Test MongoDB connection - if MONGO_SERVICE_AVAILABLE: - try: - loop = asyncio.get_event_loop() - loop.run_until_complete(mongo_service.init_db()) - logger.info("✅ Database initialized successfully") - print("✅ Database initialized successfully") - - from pymongo import MongoClient - client = MongoClient(app.config['MONGODB_URI']) - client.admin.command('ismaster') - logger.info("✅ MongoDB connection verified") - print("✅ MongoDB connection verified") - except Exception as e: - logger.error(f"❌ MongoDB initialization failed: {e}") - print(f"❌ MongoDB initialization failed: {e}") - - # Test Web3 connection - if WEB3_SERVICE_AVAILABLE: - try: - if web3_service.w3.is_connected(): - logger.info("✅ Web3 connection verified") - print("✅ Web3 connection verified") - else: - logger.warning("⚠️ Web3 connection failed - blockchain features disabled") - print("⚠️ Web3 connection failed - blockchain features disabled") - except Exception as e: - logger.warning(f"⚠️ Web3 connection error: {e}") - print(f"⚠️ Web3 connection error: {e}") - - # Test Docker connection for compiler - if COMPILER_SERVICE_AVAILABLE: - try: - docker_available = check_docker_availability() - if docker_available: - logger.info("✅ Docker connection verified - Real compiler available") - print("✅ Docker connection verified - Real compiler available") - else: - logger.warning("⚠️ Docker not available - Compiler features limited") - print("⚠️ Docker not available - Compiler features limited") - except Exception as e: - logger.warning(f"⚠️ Docker connection error: {e}") - print(f"⚠️ Docker connection error: {e}") - - # ✅ Enhanced configuration summary - logger.info("📋 Configuration Summary:") - print("📋 Configuration Summary:") - config_items = [ - ("MongoDB", MONGO_SERVICE_AVAILABLE), - ("Blockchain", WEB3_SERVICE_AVAILABLE), - ("Wallet Service", WALLET_SERVICE_AVAILABLE), - ("Compiler Service", COMPILER_SERVICE_AVAILABLE), - ("Docker", check_docker_availability()), - ("Exam System", any('Coding Exams' in desc for desc, _ in registered_blueprints)), - ("Host Panel Fix", True) - ] - - for name, available in config_items: - status = "✅ Connected" if available else "❌ Unavailable" - logger.info(f" • {name}: {status}") - print(f" • {name}: {status}") - - # ✅ Enhanced access URLs - logger.info("🌐 Access URLs:") - print("🌐 Access URLs:") - - urls = [ - ("API Health", "http://127.0.0.1:5000/api/health"), - ("Main Page", "http://127.0.0.1:5000/"), - ("Admin Panel", "http://localhost:3000/admin/login"), - ("Coding Exams", "http://localhost:3000/coding"), - ("Real Compiler", "http://localhost:3000/compiler"), - ("Join Exam", "http://localhost:3000/coding/join"), - ("Wallet Join", "http://localhost:3000/coding/join-wallet") - ] - - for name, url in urls: - logger.info(f" • {name}: {url}") - print(f" • {name}: {url}") - - # ✅ Enhanced debug URLs - print("🔧 Debug URLs:") - debug_urls = [ - ("All Routes", "http://127.0.0.1:5000/debug/routes"), - ("Exam Routes", "http://127.0.0.1:5000/debug/exam-routes"), - ("Services", "http://127.0.0.1:5000/debug/services"), - ("Direct Test", "http://127.0.0.1:5000/api/exam/test-direct"), - ("Admin Health", "http://127.0.0.1:5000/api/admin/health"), - ("Exam Info Test", "http://127.0.0.1:5000/api/exam/info/TEST123") - ] - - for name, url in debug_urls: - print(f" • {name}: {url}") - - # ✅ Enhanced exam system status - exam_ready = all([ - MONGO_SERVICE_AVAILABLE, - any('Coding Exams' in desc for desc, _ in registered_blueprints) - ]) - - print(f"📝 Exam System Status: {'✅ Ready' if exam_ready else '❌ Not Ready'}") - print(f"🔧 Host Panel Fix: ✅ Direct endpoints added to main.py") - - if exam_ready: - print("📝 Exam Features Available:") - features = [ - "Create coding exams with 6-character codes", - "Students join with exam codes", - "Host management panel with live updates", - "Real-time participant monitoring", - "Live leaderboards with scoring", - "Multi-language code execution", - "Host panel endpoints working directly" - ] - for feature in features: - print(f" • {feature}") - - # Log compiler features - if COMPILER_SERVICE_AVAILABLE: - logger.info("💻 Compiler Features:") - print("💻 Compiler Features:") - features = [ - "Multi-language support: Python, Java, C++, C, JavaScript, Go, Rust, Bash", - "Real-time code execution with output capture", - "Secure Docker containerization", - "Resource monitoring and limits" - ] - - for feature in features: - logger.info(f" • {feature}") - print(f" • {feature}") - - # ✅ Enhanced admin token logging - admin_token = app.config.get('ADMIN_TOKEN', 'admin-secret-key') - if admin_token: - logger.info(f"🔑 Admin token configured: {admin_token[:8]}...") - print(f"🔑 Admin token configured: {admin_token[:8]}...") - - return True - - except Exception as e: - logger.error(f"❌ Failed to initialize application: {str(e)}") - print(f"❌ Failed to initialize application: {str(e)}") - logger.error("Make sure MongoDB and Docker are running and accessible") - print("Make sure MongoDB and Docker are running and accessible") - return False - -if __name__ == '__main__': - # Initialize application - init_success = initialize_application() - - if not init_success: - logger.error("❌ Application initialization failed - some features may not work") - print("❌ Application initialization failed - some features may not work") try: - logger.info("🚀 Starting OpenLearnX Backend Server...") - print("🚀 Starting OpenLearnX Backend Server...") - logger.info("📚 Features: Blockchain Certificates, Coding Exams, Wallet Auth, Real Multi-language Compiler") - print("📚 Features: Blockchain Certificates, Coding Exams, Wallet Auth, Real Multi-language Compiler") + data = request.get_json() + exam_code = data.get('exam_code', '').upper() + duration_minutes = data.get('duration_minutes', 0) - # ✅ Enhanced server info - print(f"🌐 Server will be available at:") - print(f" • Local: http://127.0.0.1:5000") - print(f" • Network: http://0.0.0.0:5000") - print(f"📱 Frontend should connect to: http://127.0.0.1:5000") - print(f"🔧 Host Panel Fix: ✅ Direct endpoints added for exam info") + print(f"⏰ DIRECT: Updating duration for exam {exam_code} to {duration_minutes} minutes") - # Start Flask application - app.run( - debug=True, - host='0.0.0.0', - port=5000, - threaded=True, - use_reloader=False # Prevent double initialization + if not exam_code or duration_minutes <= 0: + return jsonify({"success": False, "error": "Invalid data"}), 400 + + # Get database + db = get_db() + + # Find and update exam + result = db.exams.update_one( + {"exam_code": exam_code, "status": "waiting"}, + {"$set": {"duration_minutes": duration_minutes}} ) - except KeyboardInterrupt: - logger.info("👋 Server stopped by user") - print("👋 Server stopped by user") + if result.modified_count > 0: + print(f"✅ Duration updated to {duration_minutes} minutes") + return jsonify({ + "success": True, + "message": f"Duration updated to {duration_minutes} minutes" + }) + else: + return jsonify({"success": False, "error": "Exam not found or already started"}), 404 + except Exception as e: - logger.error(f"❌ Server startup failed: {str(e)}") - print(f"❌ Server startup failed: {str(e)}") + print(f"❌ Error: {str(e)}") + return jsonify({"success": False, "error": str(e)}), 500 + + +# Startup initialization +def initialize_application(): + logger.info("🚀 Initializing OpenLearnX Backend") + + # Show blueprint registration status + logger.info(f"📋 Blueprints registered: {len(blueprints_registered)}") + for bp in blueprints_registered: + logger.info(f" ✅ {bp}") + + if blueprints_failed: + logger.warning(f"❌ Blueprints failed: {len(blueprints_failed)}") + for bp, error in blueprints_failed: + logger.warning(f" ❌ {bp}: {error}") + + # Test DB + if MONGO_SERVICE_AVAILABLE: + try: + asyncio.get_event_loop().run_until_complete(mongo_service.init_db()) + logger.info("✅ MongoDB initialized") + + # Test direct connection + db = get_db() + db.command('ismaster') + logger.info("✅ Direct MongoDB connection verified") + except Exception as e: + logger.error(f"❌ MongoDB init failed: {e}") + + # Test Web3 + if WEB3_SERVICE_AVAILABLE: + if web3_service.w3.is_connected(): + logger.info("✅ Web3 connected") + else: + logger.warning("⚠️ Web3 not connected") + + # Test Docker + if COMPILER_SERVICE_AVAILABLE and not check_docker_availability(): + logger.warning("⚠️ Docker unavailable") + + # Log direct endpoints + logger.info("🔧 Direct exam endpoints added:") + direct_endpoints = [ + "/api/exam/upload-question", + "/api/exam/questions/", + "/api/exam/update-question" + ] + for endpoint in direct_endpoints: + logger.info(f" ✅ {endpoint}") + + return True + +if __name__ == '__main__': + initialize_application() + + logger.info("🚀 Starting OpenLearnX Backend Server") + logger.info("📚 Features: Coding Exams, Question Upload, Host Panel, Compiler") + logger.info("🌐 Server starting on http://0.0.0.0:5000") + logger.info("🔧 All /api/* endpoints have CORS enabled") + + app.run(host='0.0.0.0', port=5000, debug=True, threaded=True, use_reloader=False) diff --git a/backend/routes/exam.py b/backend/routes/exam.py index 084760f..91fea8b 100644 --- a/backend/routes/exam.py +++ b/backend/routes/exam.py @@ -704,3 +704,228 @@ def get_exam_info(exam_code): import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 + +@bp.route('/upload-question', methods=['POST', 'OPTIONS']) +def upload_question(): + """Host uploads a custom question to their exam""" + if request.method == "OPTIONS": + response = jsonify({'status': 'ok'}) + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") + response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") + return response + + try: + data = request.get_json() + exam_code = data.get('exam_code', '').upper() + question_data = data.get('question', {}) + + print(f"📤 Host uploading question to exam: {exam_code}") + + if not exam_code or not question_data: + return jsonify({"success": False, "error": "Missing exam_code or question data"}), 400 + + # Validate required question fields + required_fields = ['title', 'description', 'function_name', 'test_cases'] + for field in required_fields: + if not question_data.get(field): + return jsonify({"success": False, "error": f"Missing required field: {field}"}), 400 + + # Find the exam + exam = db.exams.find_one({"exam_code": exam_code}) + if not exam: + return jsonify({"success": False, "error": "Exam not found"}), 404 + + # Check if exam has already started + if exam['status'] != 'waiting': + return jsonify({"success": False, "error": "Cannot modify questions after exam has started"}), 400 + + # Generate question ID + question_id = str(uuid.uuid4()) + + # Prepare question document + question = { + "id": question_id, + "title": question_data['title'], + "description": question_data['description'], + "difficulty": question_data.get('difficulty', 'medium'), + "function_name": question_data['function_name'], + "starter_code": question_data.get('starter_code', { + 'python': f'def {question_data["function_name"]}():\n # Write your solution here\n pass' + }), + "test_cases": question_data['test_cases'], + "examples": question_data.get('examples', []), + "constraints": question_data.get('constraints', []), + "time_limit": question_data.get('time_limit', 1000), + "memory_limit": question_data.get('memory_limit', '128MB'), + "created_at": datetime.now(), + "uploaded_by": exam['host_name'] + } + + # Update the exam with the new question + result = db.exams.update_one( + {"exam_code": exam_code}, + { + "$set": { + "problem": question, + "updated_at": datetime.now() + } + } + ) + + if result.modified_count > 0: + print(f"✅ Question '{question['title']}' uploaded to exam {exam_code}") + return jsonify({ + "success": True, + "message": "Question uploaded successfully", + "question_id": question_id, + "question_title": question['title'] + }) + else: + return jsonify({"success": False, "error": "Failed to update exam"}), 500 + + except Exception as e: + print(f"❌ Error uploading question: {str(e)}") + import traceback + traceback.print_exc() + return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500 + +@bp.route('/upload-question', methods=['POST', 'OPTIONS']) +def upload_question(): + """Host uploads a custom question to their exam""" + if request.method == "OPTIONS": + response = jsonify({'status': 'ok'}) + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") + response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") + return response + + try: + data = request.get_json() + exam_code = data.get('exam_code', '').upper() + question_data = data.get('question', {}) + + print(f"📤 Host uploading question to exam: {exam_code}") + + if not exam_code or not question_data: + return jsonify({"success": False, "error": "Missing exam_code or question data"}), 400 + + # Validate required question fields + required_fields = ['title', 'description'] + for field in required_fields: + if not question_data.get(field): + return jsonify({"success": False, "error": f"Missing required field: {field}"}), 400 + + # Find the exam + exam = db.exams.find_one({"exam_code": exam_code}) + if not exam: + return jsonify({"success": False, "error": "Exam not found"}), 404 + + # Check if exam has already started + if exam['status'] != 'waiting': + return jsonify({"success": False, "error": "Cannot modify questions after exam has started"}), 400 + + # Generate question ID + import uuid + question_id = str(uuid.uuid4()) + + # Prepare question document + question = { + "id": question_id, + "title": question_data['title'], + "description": question_data['description'], + "difficulty": question_data.get('difficulty', 'medium'), + "function_name": question_data.get('function_name', 'solve'), + "starter_code": question_data.get('starter_code', { + 'python': f'def {question_data.get("function_name", "solve")}():\n # Write your solution here\n pass' + }), + "test_cases": question_data.get('test_cases', []), + "examples": question_data.get('examples', []), + "constraints": question_data.get('constraints', []), + "time_limit": question_data.get('time_limit', 1000), + "memory_limit": question_data.get('memory_limit', '128MB'), + "created_at": datetime.now(), + "uploaded_by": exam.get('host_name', 'Unknown') + } + + # Update the exam with the new question + result = db.exams.update_one( + {"exam_code": exam_code}, + { + "$set": { + "problem": question, + "updated_at": datetime.now() + } + } + ) + + if result.modified_count > 0: + print(f"✅ Question '{question['title']}' uploaded to exam {exam_code}") + return jsonify({ + "success": True, + "message": "Question uploaded successfully", + "question_id": question_id, + "question_title": question['title'] + }) + else: + return jsonify({"success": False, "error": "Failed to update exam"}), 500 + + except Exception as e: + print(f"❌ Error uploading question: {str(e)}") + import traceback + traceback.print_exc() + return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500 + +@bp.route('/update-duration', methods=['POST', 'OPTIONS']) +def update_duration(): + """Update exam duration (host only, before exam starts)""" + if request.method == "OPTIONS": + response = jsonify({'status': 'ok'}) + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") + response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") + return response + + try: + data = request.get_json() + exam_code = data.get('exam_code', '').upper() + duration_minutes = data.get('duration_minutes', 0) + + print(f"⏰ Updating duration for exam {exam_code} to {duration_minutes} minutes") + + if not exam_code or duration_minutes <= 0: + return jsonify({"success": False, "error": "Invalid exam_code or duration_minutes"}), 400 + + # Find the exam + exam = db.exams.find_one({"exam_code": exam_code}) + if not exam: + return jsonify({"success": False, "error": "Exam not found"}), 404 + + # Check if exam can be modified + if exam.get('status') != 'waiting': + return jsonify({"success": False, "error": "Cannot modify duration after exam has started"}), 400 + + # Update duration + result = db.exams.update_one( + {"exam_code": exam_code}, + { + "$set": { + "duration_minutes": duration_minutes, + "updated_at": datetime.now() + } + } + ) + + if result.modified_count > 0: + print(f"✅ Duration updated to {duration_minutes} minutes for exam {exam_code}") + return jsonify({ + "success": True, + "message": f"Duration updated to {duration_minutes} minutes", + "new_duration": duration_minutes + }) + else: + return jsonify({"success": False, "error": "Failed to update duration"}), 500 + + except Exception as e: + print(f"❌ Error updating duration: {str(e)}") + return jsonify({"success": False, "error": str(e)}), 500 diff --git a/frontend/app/coding/exam/[examCode]/page.tsx b/frontend/app/coding/exam/[examCode]/page.tsx new file mode 100644 index 0000000..2f35579 --- /dev/null +++ b/frontend/app/coding/exam/[examCode]/page.tsx @@ -0,0 +1,610 @@ +'use client' +import React, { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' +import { Trophy, Clock, Users, Send, RefreshCw, Play, Code, Wallet, Shield } from 'lucide-react' + +interface Participant { + name: string + score: number + rank: number + completed: boolean + language?: string + submission_time?: string + wallet_address?: string + wallet_short?: string + blockchain_verified?: boolean +} + +interface Problem { + title: string + description: string + function_name: string + languages: string[] + examples: Array<{input: string, expected_output: string, description: string}> + constraints: string[] + starter_code: {[key: string]: string} +} + +interface ExamSession { + exam_code: string + student_name: string + wallet_address?: string + blockchain_verified?: boolean + exam_info: any +} + +export default function EnhancedExamInterface() { + const [examSession, setExamSession] = useState(null) + const [problem, setProblem] = useState(null) + const [selectedLanguage, setSelectedLanguage] = useState('python') + const [code, setCode] = useState('') + const [output, setOutput] = useState('') + const [testResults, setTestResults] = useState([]) + const [leaderboard, setLeaderboard] = useState([]) + const [waitingParticipants, setWaitingParticipants] = useState([]) + const [timeRemaining, setTimeRemaining] = useState(0) + const [isRunning, setIsRunning] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [hasSubmitted, setHasSubmitted] = useState(false) + const [examStats, setExamStats] = useState({}) + // ✅ ADD TIMER INITIALIZED STATE + const [timerInitialized, setTimerInitialized] = useState(false) + const router = useRouter() + + const languageIcons: {[key: string]: string} = { + python: '🐍', + java: '☕', + c: '⚡', + bash: '💻' + } + + useEffect(() => { + const sessionData = localStorage.getItem('exam_session') + if (!sessionData) { + router.push('/coding/join') + return + } + + const session = JSON.parse(sessionData) + setExamSession(session) + + // Fetch problem details + fetchProblem(session.exam_code) + + // Start polling for updates + const interval = setInterval(() => { + fetchLeaderboard(session.exam_code) + }, 3000) + + return () => clearInterval(interval) + }, [router]) + + // ✅ FIXED TIMER COUNTDOWN + useEffect(() => { + if (!timerInitialized || timeRemaining <= 0) return + + const timer = setInterval(() => { + setTimeRemaining(prev => { + const newTime = Math.max(0, prev - 1) + if (newTime === 0) { + alert('⏰ Time is up! Exam has ended.') + } + return newTime + }) + }, 1000) + + return () => clearInterval(timer) + }, [timerInitialized, timeRemaining]) + + const fetchProblem = async (examCode: string) => { + try { + const response = await fetch(`http://127.0.0.1:5000/api/exam/get-problem/${examCode}`) + const data = await response.json() + + if (data.success) { + setProblem(data.problem) + const defaultLang = data.problem.languages[0] || 'python' + setSelectedLanguage(defaultLang) + setCode(data.problem.starter_code[defaultLang] || '') + } + } catch (error) { + console.error('Failed to fetch problem:', error) + } + } + + // ✅ FIXED TIMER CALCULATION IN FETCHLEADERBOARD + const fetchLeaderboard = async (examCode: string) => { + try { + const response = await fetch(`http://127.0.0.1:5000/api/exam/leaderboard/${examCode}`) + const data = await response.json() + + if (data.success) { + setLeaderboard(data.leaderboard || []) + setWaitingParticipants(data.waiting_participants || []) + setExamStats(data.stats || {}) + + // ✅ FIXED TIMER CALCULATION + if (data.exam_info && data.exam_info.status === 'active') { + if (data.exam_info.end_time) { + const now = Date.now() + const endTime = new Date(data.exam_info.end_time).getTime() + const remaining = Math.max(0, Math.floor((endTime - now) / 1000)) + + console.log(`⏰ Timer calculation:`) + console.log(` Current: ${new Date(now).toISOString()}`) + console.log(` End: ${new Date(endTime).toISOString()}`) + console.log(` Remaining: ${remaining} seconds`) + + setTimeRemaining(remaining) + if (!timerInitialized) { + setTimerInitialized(true) + } + } else if (data.exam_info.start_time && data.exam_info.duration_minutes) { + // Calculate from start_time + duration + const startTime = new Date(data.exam_info.start_time).getTime() + const durationMs = data.exam_info.duration_minutes * 60 * 1000 + const endTime = startTime + durationMs + const now = Date.now() + const remaining = Math.max(0, Math.floor((endTime - now) / 1000)) + + console.log(`⏰ Using start_time + duration - Remaining: ${remaining}s`) + setTimeRemaining(remaining) + if (!timerInitialized) { + setTimerInitialized(true) + } + } + } else if (data.exam_info && data.exam_info.status === 'waiting') { + // Show full duration for waiting exams + const fullSeconds = (data.exam_info.duration_minutes || 30) * 60 + setTimeRemaining(fullSeconds) + if (!timerInitialized) { + setTimerInitialized(true) + } + } + } + } catch (error) { + console.error('Failed to fetch leaderboard:', error) + } + } + + const handleLanguageChange = (language: string) => { + setSelectedLanguage(language) + if (problem?.starter_code[language]) { + setCode(problem.starter_code[language]) + } + setOutput('') + setTestResults([]) + } + + const runCode = async () => { + if (!code.trim()) { + alert('Please write some code first!') + return + } + + setIsRunning(true) + setOutput('') + setTestResults([]) + + try { + const response = await fetch('http://127.0.0.1:5000/api/exam/execute-code', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + code, + language: selectedLanguage + }) + }) + + const result = await response.json() + + if (result.success) { + setOutput('Code executed successfully!') + setTestResults(result.test_results || []) + } else { + setOutput(`Error: ${result.error}`) + } + } catch (error) { + setOutput(`Execution failed: ${(error as Error).message}`) + } finally { + setIsRunning(false) + } + } + + const submitSolution = async () => { + if (!code.trim()) { + alert('Please write some code before submitting!') + return + } + + setIsSubmitting(true) + + try { + const response = await fetch('http://127.0.0.1:5000/api/exam/submit-solution', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + code, + language: selectedLanguage + }) + }) + + const data = await response.json() + + if (data.success) { + setHasSubmitted(true) + setTestResults(data.test_results || []) + + let alertMessage = `Solution submitted successfully!\nScore: ${data.score}%\nPassed: ${data.passed_tests}/${data.total_tests} tests` + + if (data.blockchain_verified) { + alertMessage += `\n🔗 Blockchain Verified: ${data.wallet_address?.slice(0, 6)}...${data.wallet_address?.slice(-4)}` + } + + alert(alertMessage) + fetchLeaderboard(examSession!.exam_code) + } else { + alert(data.error || 'Failed to submit solution') + } + } catch (error) { + alert('Failed to submit solution. Please try again.') + } finally { + setIsSubmitting(false) + } + } + + // ✅ FIXED TIME FORMATTING + const formatTime = (seconds: number) => { + if (seconds < 0) return "00:00" + + const mins = Math.floor(seconds / 60) + const secs = seconds % 60 + return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` + } + + const getRankColor = (rank: number) => { + switch (rank) { + case 1: return 'bg-gradient-to-r from-yellow-400 to-yellow-600 text-white' + case 2: return 'bg-gradient-to-r from-gray-300 to-gray-500 text-white' + case 3: return 'bg-gradient-to-r from-orange-400 to-orange-600 text-white' + default: return 'bg-gray-100 text-gray-700' + } + } + + if (!examSession || !problem) { + return ( +
+
+
+

Loading exam interface...

+
+
+ ) + } + + return ( +
+ {/* Header with Timer */} +
+
+
+

{problem.title}

+

Code: {examSession.exam_code}

+
+ +
+ {/* ✅ FIXED TIMER DISPLAY */} + {timeRemaining > 0 && ( +
+ + + {formatTime(timeRemaining)} + +
+ )} + + {/* Wallet Info Display */} + {examSession.blockchain_verified && examSession.wallet_address && ( +
+ + + {examSession.wallet_address.slice(0, 6)}...{examSession.wallet_address.slice(-4)} + + +
+ )} + + {/* Participant Count */} +
+ + {examStats.total_participants || 0} participants + {examStats.blockchain_participants > 0 && ( + + ({examStats.blockchain_participants} 🔗) + + )} +
+
+
+
+ +
+ {/* Problem & Code Editor */} +
+ {/* Problem Description */} +
+
+

{problem.title}

+ {examSession.blockchain_verified && ( +
+ + Blockchain Verified +
+ )} +
+ +
+

{problem.description}

+ +

Examples:

+ {problem.examples.map((example, index) => ( +
+
+
+ Input: + "{example.input}" +
+
+ Output: + "{example.expected_output}" +
+
+ {example.description && ( +
{example.description}
+ )} +
+ ))} + +

Constraints:

+
    + {problem.constraints.map((constraint, index) => ( +
  • {constraint}
  • + ))} +
+
+
+ + {/* Code Editor */} +
+
+

Your Solution

+ + {/* Language Selector */} +
+ + +
+
+ +