From 8c56eb9e361d49fddf40ea8b2c69b5b7a863007f Mon Sep 17 00:00:00 2001 From: 5t4l1n Date: Sat, 26 Jul 2025 22:20:50 +0530 Subject: [PATCH] some kinda --- a.py | 4 + backend/main.py | 543 ++++++++++++++--- backend/routes/coding.py | 302 +++++++--- backend/routes/compiler.py | 165 +++++ backend/routes/exam.py | 515 ++++++++++++++++ backend/services/compiler_service.py | 42 ++ backend/services/real_compiler_service.py | 305 ++++++++++ backend/services/wallet_service.py | 53 ++ frontend/app/coding/[problemId]/page.tsx | 444 +++++++++++++- frontend/app/coding/create/page.tsx | 312 ++++++++++ frontend/app/coding/exam/page.tsx | 547 +++++++++++++++++ frontend/app/coding/host/[examCode]/page.tsx | 597 +++++++++++++++++++ frontend/app/coding/join-wallet/page.tsx | 330 ++++++++++ frontend/app/coding/join/page.tsx | 183 ++++++ frontend/app/coding/page.tsx | 455 +++++++++++++- frontend/app/compiler/page.tsx | 416 +++++++++++++ frontend/app/join-test/page.tsx | 99 +++ frontend/public/detect-extensions.js | 50 ++ frontend/public/test-join.html | 332 +++++++++++ requirements.txt | 5 +- 20 files changed, 5544 insertions(+), 155 deletions(-) create mode 100644 a.py create mode 100644 backend/routes/compiler.py create mode 100644 backend/routes/exam.py create mode 100644 backend/services/compiler_service.py create mode 100644 backend/services/real_compiler_service.py create mode 100644 backend/services/wallet_service.py create mode 100644 frontend/app/coding/create/page.tsx create mode 100644 frontend/app/coding/exam/page.tsx create mode 100644 frontend/app/coding/host/[examCode]/page.tsx create mode 100644 frontend/app/coding/join-wallet/page.tsx create mode 100644 frontend/app/coding/join/page.tsx create mode 100644 frontend/app/compiler/page.tsx create mode 100644 frontend/app/join-test/page.tsx create mode 100644 frontend/public/detect-extensions.js create mode 100644 frontend/public/test-join.html diff --git a/a.py b/a.py new file mode 100644 index 0000000..6fc7281 --- /dev/null +++ b/a.py @@ -0,0 +1,4 @@ +from pymongo import MongoClient +db = MongoClient("mongodb://localhost:27017").openlearnx +exam = db.exams.find_one({"_id": ObjectId("6884f04c6ca73cc9032deaf9")}) +print(exam["exam_code"]) # this is what participants must type diff --git a/backend/main.py b/backend/main.py index ad2bec2..181488e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -7,142 +7,547 @@ from mongo_service import MongoService from web3_service import Web3Service import logging -# Import all route blueprints -from routes import auth, test_flow, certificate, dashboard, courses, quizzes, admin - +# Load environment variables first load_dotenv() +# Import all route blueprints +from routes import auth, test_flow, certificate, dashboard, courses, quizzes, admin, exam, compiler + +# Import services after loading env vars +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}") + wallet_service = None + real_compiler_service = None + WALLET_SERVICE_AVAILABLE = False + COMPILER_SERVICE_AVAILABLE = False + +# Initialize Flask app app = Flask(__name__) -# Enhanced CORS configuration for admin panel with credentials support +# Enhanced CORS configuration for coding exam platform CORS(app, resources={ r"/api/*": { - "origins": ["http://localhost:3000", "http://127.0.0.1:3000"], + "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"], - "supports_credentials": True # ✅ Added for admin authentication + "allow_headers": [ + "Content-Type", + "Authorization", + "X-Requested-With", + "Accept", + "Origin" + ], + "supports_credentials": True, + "expose_headers": ["Authorization"] } }) # Configuration -app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secret-key') +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') -app.config['MINTER_PRIVATE_KEY'] = os.getenv('MINTER_PRIVATE_KEY') +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') -# Initialize services -mongo_service = MongoService(app.config['MONGODB_URI']) -web3_service = Web3Service(app.config['WEB3_PROVIDER_URL'], app.config['CONTRACT_ADDRESS']) +# 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', + handlers=[ + logging.StreamHandler(), + logging.FileHandler('openlearnx.log') if os.access('.', os.W_OK) else logging.NullHandler() + ] +) +logger = logging.getLogger(__name__) + +# Initialize services with error handling +try: + mongo_service = MongoService(app.config['MONGODB_URI']) + app.config['MONGO_SERVICE'] = mongo_service + MONGO_SERVICE_AVAILABLE = True +except Exception as e: + logging.error(f"Failed to initialize MongoDB service: {e}") + mongo_service = None + MONGO_SERVICE_AVAILABLE = False + +try: + web3_service = Web3Service( + app.config['WEB3_PROVIDER_URL'], + app.config['CONTRACT_ADDRESS'] + ) + app.config['WEB3_SERVICE'] = web3_service + WEB3_SERVICE_AVAILABLE = True +except Exception as e: + logging.error(f"Failed to initialize Web3 service: {e}") + web3_service = None + WEB3_SERVICE_AVAILABLE = False # Make services available to routes -app.config['MONGO_SERVICE'] = mongo_service -app.config['WEB3_SERVICE'] = web3_service +if WALLET_SERVICE_AVAILABLE: + app.config['WALLET_SERVICE'] = wallet_service -# Register all blueprints -app.register_blueprint(auth.bp, url_prefix='/api/auth') -app.register_blueprint(test_flow.bp, url_prefix='/api/test') -app.register_blueprint(certificate.bp, url_prefix='/api/certificate') -app.register_blueprint(dashboard.bp, url_prefix='/api/dashboard') -app.register_blueprint(courses.bp, url_prefix='/api/courses') -app.register_blueprint(quizzes.bp, url_prefix='/api/quizzes') -app.register_blueprint(admin.bp, url_prefix="/api/admin") +if COMPILER_SERVICE_AVAILABLE: + app.config['REAL_COMPILER_SERVICE'] = real_compiler_service +# ✅ 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 + +# Register all blueprints with error handling +blueprints = [ + (auth.bp, '/api/auth'), + (test_flow.bp, '/api/test'), + (certificate.bp, '/api/certificate'), + (dashboard.bp, '/api/dashboard'), + (courses.bp, '/api/courses'), + (quizzes.bp, '/api/quizzes'), + (admin.bp, '/api/admin'), + (exam.bp, '/api/exam'), # Coding exam routes + (compiler.bp, '/api/compiler'), # Compiler routes +] + +for blueprint, url_prefix in blueprints: + try: + app.register_blueprint(blueprint, url_prefix=url_prefix) + logging.info(f"✅ Registered blueprint: {url_prefix}") + print(f"✅ Registered blueprint: {url_prefix}") + except Exception as e: + logging.error(f"❌ Failed to register blueprint {url_prefix}: {e}") + print(f"❌ Failed to register blueprint {url_prefix}: {e}") + +# Debug routes +@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({ + "total_routes": len(routes), + "routes": sorted(routes, key=lambda x: x['rule']) + }) + +@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) + }) + return jsonify({ + "exam_routes": exam_routes, + "exam_blueprint_registered": hasattr(exam, 'bp') + }) + +@app.route('/debug/services') +def debug_services(): + """Debug service availability""" + return jsonify({ + "services": { + "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) + }) + +# Direct exam test route +@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": os.popen('date').read().strip(), + "data": request.json if request.method == "POST" else None + }) + +# Health check endpoints @app.route('/') def health_check(): return jsonify({ "status": "OpenLearnX API is running", - "version": "1.0.0", + "version": "2.0.0", + "features": { + "blockchain": WEB3_SERVICE_AVAILABLE, + "coding_exams": COMPILER_SERVICE_AVAILABLE, + "wallet_auth": WALLET_SERVICE_AVAILABLE, + "database": MONGO_SERVICE_AVAILABLE, + "real_compiler": COMPILER_SERVICE_AVAILABLE + }, "endpoints": { "auth": "/api/auth", "courses": "/api/courses", "admin": "/api/admin", "dashboard": "/api/dashboard", "certificates": "/api/certificate", - "quizzes": "/api/quizzes" - } + "quizzes": "/api/quizzes", + "coding_exams": "/api/exam", + "compiler": "/api/compiler" + }, + "debug_endpoints": [ + "/debug/routes", + "/debug/exam-routes", + "/debug/services", + "/api/exam/test-direct" + ] }) +@app.route('/api/health') +def api_health(): + """Comprehensive API health check""" + health_status = { + "status": "healthy", + "timestamp": os.popen('date').read().strip(), + "services": { + "mongodb": MONGO_SERVICE_AVAILABLE, + "web3": WEB3_SERVICE_AVAILABLE, + "wallet": WALLET_SERVICE_AVAILABLE, + "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()) + } + + # 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" + + # 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", + "admin_token_configured": bool(app.config.get('ADMIN_TOKEN')), "admin_endpoints": [ "/api/admin/dashboard", "/api/admin/courses", "/api/admin/courses/", - "/api/admin/test" # ✅ Added test endpoint + "/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/" + ], + "compiler_endpoints": [ + "/api/compiler/languages", + "/api/compiler/execute", + "/api/compiler/execute-async", + "/api/compiler/status/", + "/api/compiler/test", + "/api/compiler/stats" ] }) +# Error handlers @app.errorhandler(404) def not_found(error): - return jsonify({"error": "Endpoint not found"}), 404 + return jsonify({ + "error": "Endpoint not found", + "message": "The requested API endpoint does not exist", + "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" + ] + }), 404 @app.errorhandler(500) def internal_error(error): app.logger.error(f"Internal server error: {str(error)}") - return jsonify({"error": "Internal server error"}), 500 + return jsonify({ + "error": "Internal server error", + "message": "An unexpected error occurred on the server" + }), 500 + +@app.errorhandler(403) +def forbidden(error): + return jsonify({ + "error": "Forbidden", + "message": "Access denied - check your authentication" + }), 403 + +@app.errorhandler(401) +def unauthorized(error): + return jsonify({ + "error": "Unauthorized", + "message": "Authentication required" + }), 401 @app.errorhandler(Exception) def handle_error(error): app.logger.error(f"Unhandled error: {str(error)}") - return jsonify({"error": "An unexpected error occurred"}), 500 - -# Enable logging for admin operations -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' # ✅ Enhanced logging format -) -logger = logging.getLogger(__name__) + return jsonify({ + "error": "An unexpected error occurred", + "type": type(error).__name__ + }), 500 +# Request logging and CORS handling @app.before_request def log_request_info(): + """Enhanced request logging""" if '/api/admin' in request.path: - # ✅ Enhanced admin request logging auth_header = request.headers.get('Authorization', 'No auth header') - logger.info(f"Admin request: {request.method} {request.path} | Auth: {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}") + + if '/api/compiler' in request.path: + logger.info(f"Compiler request: {request.method} {request.path}") -# ✅ Add OPTIONS handler for CORS preflight @app.before_request def handle_preflight(): + """Handle CORS preflight requests""" if request.method == "OPTIONS": - response = jsonify() + response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") - response.headers.add('Access-Control-Allow-Headers', "*") - response.headers.add('Access-Control-Allow-Methods', "*") + 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') return response -if __name__ == '__main__': +@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 + +# Startup function +def initialize_application(): + """Initialize application with comprehensive error handling""" try: - # ✅ Enhanced database initialization with better error handling - loop = asyncio.get_event_loop() - loop.run_until_complete(mongo_service.init_db()) - logger.info("✅ Database initialized successfully") + logger.info("🚀 Initializing OpenLearnX Backend...") + print("🚀 Initializing OpenLearnX Backend...") - # ✅ Test MongoDB connection - from pymongo import MongoClient - client = MongoClient(app.config['MONGODB_URI']) - client.admin.command('ismaster') - logger.info("✅ MongoDB connection verified") + # 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}") - logger.info("✅ OpenLearnX backend starting...") - logger.info(f"✅ Admin panel available at: http://localhost:3000/admin/login") - logger.info(f"✅ API health check: http://127.0.0.1:5000") - logger.info(f"✅ Admin health check: http://127.0.0.1:5000/api/admin/health") + # 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}") - # ✅ Log admin token for debugging - admin_token = os.getenv('ADMIN_TOKEN', 'admin-secret-key') - logger.info(f"✅ Admin token configured: {admin_token[:8]}...") + # 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}") + + # Log configuration + 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()) + ] + + for name, available in config_items: + status = "✅ Connected" if available else "❌ Unavailable" + logger.info(f" • {name}: {status}") + print(f" • {name}: {status}") + + # Log access URLs + logger.info("🌐 Access URLs:") + print("🌐 Access URLs:") + + urls = [ + ("API Health", "http://127.0.0.1:5000/api/health"), + ("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}") + + # 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") + ] + + for name, url in debug_urls: + print(f" • {name}: {url}") + + # 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}") + + # Log admin token (partially masked) + 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: {str(e)}") - logger.error("Make sure MongoDB is running and accessible") + 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() - # ✅ Enhanced Flask app configuration - app.run( - debug=True, - host='0.0.0.0', - port=5000, - threaded=True # Better for handling multiple requests - ) + 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") + + # Start Flask application + app.run( + debug=True, + host='0.0.0.0', + port=5000, + threaded=True, + use_reloader=False + ) + + except KeyboardInterrupt: + logger.info("👋 Server stopped by user") + print("👋 Server stopped by user") + except Exception as e: + logger.error(f"❌ Server startup failed: {str(e)}") + print(f"❌ Server startup failed: {str(e)}") diff --git a/backend/routes/coding.py b/backend/routes/coding.py index 97422b7..5b04bb9 100644 --- a/backend/routes/coding.py +++ b/backend/routes/coding.py @@ -1,88 +1,240 @@ -from flask import Blueprint, jsonify, request, current_app -import requests -from bson import ObjectId +from flask import Blueprint, request, jsonify, session +from functools import wraps +import subprocess +import tempfile +import os +import time +import uuid from datetime import datetime +import docker +import psutil bp = Blueprint('coding', __name__) -PISTON_API_URL = "https://emkc.org/api/v2/piston/execute" -@bp.route("/problems", methods=["GET"]) -async def get_problems(): - mongo = current_app.config['MONGO_SERVICE'] - problems = await mongo.db.coding_problems.find().to_list(100) - for p in problems: - p['_id'] = str(p['_id']) - return jsonify(problems) - -@bp.route("/problems/", methods=["GET"]) -async def get_problem(problem_id): - mongo = current_app.config['MONGO_SERVICE'] - prob = await mongo.db.coding_problems.find_one({"_id": ObjectId(problem_id)}) - if not prob: - return jsonify({"error": "Problem not found"}), 404 - prob['_id'] = str(prob['_id']) - return jsonify(prob) - -@bp.route("/run", methods=["POST"]) -async def run_code(): - data = request.json - problem_id = data.get("problem_id") - code = data.get("code") - language = data.get("language") - - mongo = current_app.config['MONGO_SERVICE'] - problem = await mongo.db.coding_problems.find_one({"_id": ObjectId(problem_id)}) - if not problem: - return jsonify({"error": "Problem not found"}), 404 - - # Concatenate all test case inputs - input_data = '\n'.join([tc['input'] for tc in problem['test_cases']]) +def secure_execution_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + # Check if user is in secure coding mode + if not session.get('secure_coding_mode'): + return jsonify({"error": "Secure coding mode required"}), 403 + return f(*args, **kwargs) + return decorated_function +@bp.route("/start-session", methods=["POST"]) +def start_coding_session(): + """Start a secure coding session""" try: - resp = requests.post( - PISTON_API_URL, - json={ - "language": language, - "source": code, - "input": input_data - }, - timeout=10, - ) - resp.raise_for_status() - result = resp.json() + data = request.json + course_id = data.get('course_id') + lesson_id = data.get('lesson_id') + + session_id = str(uuid.uuid4()) + session['coding_session_id'] = session_id + session['secure_coding_mode'] = True + session['start_time'] = datetime.now().isoformat() + session['course_id'] = course_id + session['lesson_id'] = lesson_id + + return jsonify({ + "success": True, + "session_id": session_id, + "message": "Secure coding session started", + "restrictions": { + "copy_paste_disabled": True, + "browser_locked": True, + "extensions_blocked": True, + "virtual_detection": True + } + }) except Exception as e: return jsonify({"error": str(e)}), 500 - # Compare output against expected (simple line-by-line check) - output_lines = result.get("output", "").strip().split('\n') - expected_outputs = [tc['expected_output'].strip() for tc in problem['test_cases']] - correct = output_lines == expected_outputs +@bp.route("/execute", methods=["POST"]) +@secure_execution_required +def execute_code(): + """Execute code securely in isolated environment""" + try: + data = request.json + code = data.get('code') + language = data.get('language', 'python') + test_cases = data.get('test_cases', []) + + if not code: + return jsonify({"error": "No code provided"}), 400 + + # Log coding attempt + log_coding_attempt(session['coding_session_id'], code, language) + + # Execute code in secure container + result = execute_in_container(code, language, test_cases) + + return jsonify(result) + except Exception as e: + return jsonify({"error": str(e)}), 500 - return jsonify({ - "output": result.get("output"), - "error": result.get("stderr"), - "runtime": result.get("stats", {}).get("duration"), - "correct": correct, +@bp.route("/submit-test", methods=["POST"]) +@secure_execution_required +def submit_coding_test(): + """Submit coding test for evaluation""" + try: + data = request.json + code = data.get('code') + problem_id = data.get('problem_id') + + # Validate against test cases + test_result = validate_test_submission(code, problem_id) + + # Store submission + submission_id = store_submission( + session['coding_session_id'], + session['course_id'], + problem_id, + code, + test_result + ) + + return jsonify({ + "success": True, + "submission_id": submission_id, + "score": test_result['score'], + "passed_tests": test_result['passed'], + "total_tests": test_result['total'], + "feedback": test_result['feedback'] + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +def execute_in_container(code, language, test_cases): + """Execute code in secure Docker container""" + try: + client = docker.from_env() + + # Language-specific container configuration + containers = { + 'python': 'python:3.9-alpine', + 'java': 'openjdk:11-alpine', + 'javascript': 'node:16-alpine' + } + + if language not in containers: + return {"error": "Unsupported language"} + + # Create temporary file + with tempfile.NamedTemporaryFile(mode='w', suffix=f'.{get_file_extension(language)}', delete=False) as f: + f.write(code) + temp_file = f.name + + try: + # Run container with security restrictions + container = client.containers.run( + containers[language], + command=get_run_command(language, temp_file), + volumes={os.path.dirname(temp_file): {'bind': '/app', 'mode': 'ro'}}, + working_dir='/app', + mem_limit='128m', + cpu_period=100000, + cpu_quota=50000, # 50% CPU limit + network_mode='none', # No network access + remove=True, + timeout=10, # 10 second timeout + detach=False + ) + + output = container.decode('utf-8') + + # Run test cases if provided + test_results = [] + if test_cases: + for test in test_cases: + test_result = run_test_case(code, language, test) + test_results.append(test_result) + + return { + "success": True, + "output": output, + "test_results": test_results, + "execution_time": "< 10s" + } + + finally: + os.unlink(temp_file) + + except docker.errors.ContainerError as e: + return {"error": f"Runtime error: {e}"} + except docker.errors.ImageNotFound: + return {"error": "Language runtime not available"} + except Exception as e: + return {"error": f"Execution failed: {str(e)}"} + +def get_file_extension(language): + extensions = { + 'python': 'py', + 'java': 'java', + 'javascript': 'js' + } + return extensions.get(language, 'txt') + +def get_run_command(language, filename): + commands = { + 'python': f'python /app/{os.path.basename(filename)}', + 'java': f'javac /app/{os.path.basename(filename)} && java -cp /app {os.path.splitext(os.path.basename(filename))[0]}', + 'javascript': f'node /app/{os.path.basename(filename)}' + } + return commands.get(language) + +def log_coding_attempt(session_id, code, language): + """Log all coding attempts for monitoring""" + from pymongo import MongoClient + + client = MongoClient(os.getenv('MONGODB_URI', 'mongodb://localhost:27017/')) + db = client.openlearnx + + db.coding_logs.insert_one({ + "session_id": session_id, + "code": code, + "language": language, + "timestamp": datetime.now(), + "ip_address": request.remote_addr, + "user_agent": request.headers.get('User-Agent') }) -@bp.route("/submit", methods=["POST"]) -async def submit_solution(): - # Same as run_code, but can mark problem as solved - user = await get_authenticated_user() - if not user: - return jsonify({"error": "Unauthorized"}), 401 +def validate_test_submission(code, problem_id): + """Validate code against predefined test cases""" + # Load test cases for the problem + test_cases = get_problem_test_cases(problem_id) + + passed = 0 + total = len(test_cases) + feedback = [] + + for i, test_case in enumerate(test_cases): + result = run_test_case(code, 'python', test_case) + if result['passed']: + passed += 1 + feedback.append(f"Test {i+1}: ✅ Passed") + else: + feedback.append(f"Test {i+1}: ❌ Failed - {result['error']}") + + score = (passed / total) * 100 + + return { + "score": score, + "passed": passed, + "total": total, + "feedback": feedback + } - # Run the code first - result = await run_code() - jres = result.get_json() - - if jres.get("correct"): - mongo = current_app.config['MONGO_SERVICE'] - # Record that user solved problem - await mongo.db.user_solutions.update_one( - {"user_id": user['_id'], "problem_id": jres.get('problem_id')}, - {"$set": {"solved": True, "solved_at": datetime.utcnow()}}, - upsert=True - ) - return jsonify(jres) -`` +def get_problem_test_cases(problem_id): + """Get test cases for a specific problem""" + # This would load from your database + test_cases_db = { + "python-basics-1": [ + {"input": "hello", "expected_output": "HELLO"}, + {"input": "world", "expected_output": "WORLD"} + ], + "java-oop-1": [ + {"input": "5", "expected_output": "25"}, + {"input": "10", "expected_output": "100"} + ] + } + return test_cases_db.get(problem_id, []) diff --git a/backend/routes/compiler.py b/backend/routes/compiler.py new file mode 100644 index 0000000..e7a8675 --- /dev/null +++ b/backend/routes/compiler.py @@ -0,0 +1,165 @@ +from flask import Blueprint, request, jsonify, session +from services.real_compiler_service import real_compiler_service +import uuid +from datetime import datetime + +bp = Blueprint('compiler', __name__) + +@bp.route("/languages", methods=["GET"]) +def get_supported_languages(): + """Get list of supported programming languages""" + try: + languages = real_compiler_service.get_supported_languages() + return jsonify({ + "success": True, + "languages": languages, + "total_languages": len(languages) + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@bp.route("/execute", methods=["POST"]) +def execute_code(): + """Execute code and return real output""" + try: + data = request.json + + # Validate input + code = data.get('code', '').strip() + language = data.get('language', 'python') + input_data = data.get('input', '') + + if not code: + return jsonify({"error": "No code provided"}), 400 + + if language not in [lang['id'] for lang in real_compiler_service.get_supported_languages()]: + return jsonify({"error": f"Language '{language}' not supported"}), 400 + + # Generate execution ID + execution_id = str(uuid.uuid4()) + + # Execute code + result = real_compiler_service.execute_code( + code=code, + language=language, + input_data=input_data, + execution_id=execution_id + ) + + return jsonify(result) + + except Exception as e: + return jsonify({"error": f"Execution failed: {str(e)}"}), 500 + +@bp.route("/execute-async", methods=["POST"]) +def execute_code_async(): + """Start asynchronous code execution""" + try: + data = request.json + execution_id = str(uuid.uuid4()) + + # Add to execution queue + real_compiler_service.execution_queue.put({ + 'execution_id': execution_id, + 'code': data.get('code'), + 'language': data.get('language', 'python'), + 'input_data': data.get('input', ''), + 'callback_url': data.get('callback_url') + }) + + return jsonify({ + "success": True, + "execution_id": execution_id, + "message": "Code execution started", + "status_url": f"/api/compiler/status/{execution_id}" + }) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@bp.route("/status/", methods=["GET"]) +def get_execution_status(execution_id): + """Get status of code execution""" + try: + status = real_compiler_service.get_execution_status(execution_id) + + if status: + return jsonify({ + "success": True, + "execution_id": execution_id, + "status": status['status'], + "start_time": status['start_time'].isoformat(), + "language": status['language'] + }) + else: + return jsonify({ + "success": False, + "error": "Execution not found" + }), 404 + + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@bp.route("/cancel/", methods=["POST"]) +def cancel_execution(execution_id): + """Cancel a running execution""" + try: + success = real_compiler_service.cancel_execution(execution_id) + + return jsonify({ + "success": success, + "message": "Execution cancelled" if success else "Execution not found" + }) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@bp.route("/test", methods=["POST"]) +def test_compiler(): + """Test compiler with sample code""" + try: + language = request.json.get('language', 'python') + + test_codes = { + 'python': 'print("Hello from OpenLearnX Python Compiler!")\nprint("Current time:", __import__("datetime").datetime.now())', + 'java': 'public class Main {\n public static void main(String[] args) {\n System.out.println("Hello from OpenLearnX Java Compiler!");\n }\n}', + 'cpp': '#include \nint main() {\n std::cout << "Hello from OpenLearnX C++ Compiler!" << std::endl;\n return 0;\n}', + 'javascript': 'console.log("Hello from OpenLearnX JavaScript Compiler!");', + 'go': 'package main\nimport "fmt"\nfunc main() {\n fmt.Println("Hello from OpenLearnX Go Compiler!")\n}', + 'rust': 'fn main() {\n println!("Hello from OpenLearnX Rust Compiler!");\n}' + } + + test_code = test_codes.get(language, test_codes['python']) + + result = real_compiler_service.execute_code( + code=test_code, + language=language, + input_data="" + ) + + return jsonify(result) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@bp.route("/stats", methods=["GET"]) +def get_compiler_stats(): + """Get compiler service statistics""" + try: + active_executions = len(real_compiler_service.active_executions) + queue_size = real_compiler_service.execution_queue.qsize() + supported_languages = len(real_compiler_service.language_configs) + + return jsonify({ + "success": True, + "stats": { + "active_executions": active_executions, + "queue_size": queue_size, + "supported_languages": supported_languages, + "max_concurrent": real_compiler_service.max_concurrent_executions + }, + "uptime": datetime.now().isoformat() + }) + + except Exception as e: + return jsonify({"error": str(e)}), 500 diff --git a/backend/routes/exam.py b/backend/routes/exam.py new file mode 100644 index 0000000..c081663 --- /dev/null +++ b/backend/routes/exam.py @@ -0,0 +1,515 @@ +from flask import Blueprint, request, jsonify, session +import uuid +import random +import string +from datetime import datetime, timedelta +from pymongo import MongoClient +import os + +bp = Blueprint('exam', __name__) + +# MongoDB connection +mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/') +client = MongoClient(mongo_uri) +db = client.openlearnx + +def generate_exam_code(): + """Generate a unique 6-character exam code""" + while True: + code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) + if not db.exams.find_one({"exam_code": code}): + return code + +@bp.route("/create-exam", methods=["POST", "OPTIONS"]) +def create_exam(): + """Create a new coding exam""" + # Handle OPTIONS request for CORS + 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: + print(f"Received create-exam request") + + data = request.json + print(f"Request data: {data}") + + if not data: + print("❌ No data provided") + return jsonify({"error": "No data provided"}), 400 + + # Check for basic required fields + if not data.get('title'): + print("❌ Missing title") + return jsonify({"error": "Missing required field: title"}), 400 + + if not data.get('host_name'): + print("❌ Missing host_name") + return jsonify({"error": "Missing required field: host_name"}), 400 + + # Handle different problem data formats + problem_title = data.get('problem_title') or data.get('title') or 'Coding Challenge' + problem_description = data.get('problem_description') or f"Solve the {problem_title} problem" + + # Handle problem_id if provided + if data.get('problem_id'): + problem_title = problem_title or data.get('problem_id').replace('-', ' ').title() + print(f"Using problem_id: {data.get('problem_id')}") + + exam_code = generate_exam_code() + + exam = { + "exam_code": exam_code, + "title": data.get('title'), + "host_name": data.get('host_name'), + "created_at": datetime.now(), + "status": "waiting", + "duration_minutes": data.get('duration_minutes', 30), + "max_participants": data.get('max_participants', 50), + "problem": { + "title": problem_title, + "description": problem_description, + "function_name": data.get('function_name', 'solve'), + "languages": data.get('languages', ['python']), + "test_cases": data.get('test_cases', [ + { + "input": "hello world", + "expected_output": "Hello World", + "description": "Basic capitalization test" + } + ]), + "starter_code": data.get('starter_code', { + 'python': 'def solve(input_string):\n # Write your solution here\n return input_string.title()', + 'java': 'public String solve(String inputString) {\n // Write your solution here\n return inputString;\n}', + 'javascript': 'function solve(inputString) {\n // Write your solution here\n return inputString;\n}' + }), + "constraints": data.get('constraints', ['Input will be a string', 'Length between 1-1000 characters']), + "examples": data.get('examples', [ + { + "input": "hello world", + "expected_output": "Hello World", + "description": "Capitalize each word" + } + ]) + }, + "participants": [], + "leaderboard": [], + "start_time": None, + "end_time": None + } + + print(f"✅ Creating exam with code: {exam_code}") + print(f"✅ Problem title: {problem_title}") + + # Insert into database + result = db.exams.insert_one(exam) + + print(f"✅ Exam created successfully with ID: {result.inserted_id}") + + return jsonify({ + "success": True, + "exam_code": exam_code, + "exam_id": str(result.inserted_id), + "message": f"Exam created successfully! Share code: {exam_code}", + "exam_details": { + "title": exam['title'], + "problem_title": problem_title, + "duration": exam['duration_minutes'], + "max_participants": exam['max_participants'], + "languages": exam['problem']['languages'] + } + }) + + except Exception as e: + print(f"❌ Error creating exam: {str(e)}") + import traceback + traceback.print_exc() + return jsonify({"error": f"Failed to create exam: {str(e)}"}), 500 + +@bp.route("/join-exam", methods=["POST", "OPTIONS"]) +def join_exam(): + """Student joins exam using unique code and their name""" + 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: + # Debug logging for the request + print(f"🔍 Raw request data: {request.data}") + print(f"🔍 Content-Type: {request.headers.get('Content-Type')}") + + data = request.json + print(f"🔍 Parsed JSON data: {data}") + + if not data: + print("❌ No JSON data received") + return jsonify({"error": "No data provided"}), 400 + + exam_code = data.get('exam_code', '').upper().strip() + student_name = data.get('student_name', '').strip() + + print(f"📝 Join exam request - Code: {exam_code}, Name: {student_name}") + + # Enhanced validation with detailed error messages + if not exam_code: + print("❌ Missing exam_code") + return jsonify({"error": "Exam code is required"}), 400 + + if not student_name: + print("❌ Missing student_name") + return jsonify({"error": "Student name is required"}), 400 + + # Check if exam exists + exam = db.exams.find_one({"exam_code": exam_code}) + if not exam: + print(f"❌ Exam not found: {exam_code}") + return jsonify({"error": "Invalid exam code"}), 404 + + print(f"✅ Found exam: {exam['title']} (Status: {exam['status']})") + + # Check exam status + if exam['status'] == 'completed': + print("❌ Exam already completed") + return jsonify({"error": "This exam has already ended"}), 400 + + # Check capacity + current_participants = exam.get('participants', []) + max_participants = exam.get('max_participants', 50) + + if len(current_participants) >= max_participants: + print(f"❌ Exam full: {len(current_participants)}/{max_participants}") + return jsonify({"error": "Exam is full"}), 400 + + # Check if name is already taken + existing_names = [p['name'].lower() for p in current_participants] + if student_name.lower() in existing_names: + print(f"❌ Name already taken: {student_name}") + return jsonify({"error": "Name already taken. Please choose a different name."}), 400 + + # Create new participant + participant = { + "name": student_name, + "joined_at": datetime.now(), + "session_id": str(uuid.uuid4()), + "score": 0, + "submission": None, + "language": None, + "submission_time": None, + "completed": False, + "rank": 0, + "test_results": [] + } + + # Add participant to exam + result = db.exams.update_one( + {"exam_code": exam_code}, + {"$push": {"participants": participant}} + ) + + if result.modified_count == 0: + print("❌ Failed to add participant to database") + return jsonify({"error": "Failed to join exam"}), 500 + + # Set session data + session['exam_code'] = exam_code + session['student_name'] = student_name + session['session_id'] = participant['session_id'] + + print(f"✅ Participant {student_name} joined exam {exam_code}") + + return jsonify({ + "success": True, + "message": f"Successfully joined exam: {exam['title']}", + "exam_info": { + "title": exam['title'], + "duration_minutes": exam['duration_minutes'], + "status": exam['status'], + "participants_count": len(current_participants) + 1, + "max_participants": max_participants, + "languages": exam.get('problem', {}).get('languages', ['python']), + "problem_title": exam.get('problem', {}).get('title', '') + } + }) + + except Exception as e: + print(f"❌ Error joining exam: {str(e)}") + import traceback + traceback.print_exc() + return jsonify({"error": f"Failed to join exam: {str(e)}"}), 500 + +@bp.route("/start-exam", methods=["POST", "OPTIONS"]) +def start_exam(): + """Host starts the 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.json + exam_code = data.get('exam_code') + + print(f"📝 Start exam request - Code: {exam_code}") + + exam = db.exams.find_one({"exam_code": exam_code}) + if not exam: + return jsonify({"error": "Exam not found"}), 404 + + if exam['status'] != 'waiting': + return jsonify({"error": "Exam has already started or ended"}), 400 + + start_time = datetime.now() + end_time = start_time + timedelta(minutes=exam['duration_minutes']) + + db.exams.update_one( + {"exam_code": exam_code}, + { + "$set": { + "status": "active", + "start_time": start_time, + "end_time": end_time + } + } + ) + + print(f"✅ Exam {exam_code} started successfully") + + return jsonify({ + "success": True, + "message": "Exam started successfully!", + "start_time": start_time.isoformat(), + "end_time": end_time.isoformat(), + "participants_count": len(exam.get('participants', [])) + }) + except Exception as e: + print(f"❌ Error starting exam: {str(e)}") + return jsonify({"error": str(e)}), 500 + +@bp.route("/leaderboard/", methods=["GET", "OPTIONS"]) +def get_leaderboard(exam_code): + """Get real-time leaderboard visible to all participants""" + 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") + return response + + try: + print(f"📝 Leaderboard request - Code: {exam_code}") + + exam = db.exams.find_one({"exam_code": exam_code.upper()}) + if not exam: + return jsonify({"error": "Exam not found"}), 404 + + participants = exam.get('participants', []) + + # Sort by score and submission time + completed_participants = [p for p in participants if p.get('completed', False)] + leaderboard = sorted( + completed_participants, + key=lambda x: (-x.get('score', 0), x.get('submission_time', datetime.now())) + ) + + # Add rank to each participant + for i, participant in enumerate(leaderboard): + participant['rank'] = i + 1 + + waiting_participants = [p for p in participants if not p.get('completed', False)] + + # Calculate statistics + total_score = sum(p.get('score', 0) for p in completed_participants) + avg_score = total_score / len(completed_participants) if completed_participants else 0 + + return jsonify({ + "success": True, + "exam_info": { + "title": exam['title'], + "status": exam['status'], + "duration_minutes": exam['duration_minutes'], + "start_time": exam.get('start_time'), + "end_time": exam.get('end_time'), + "problem_title": exam.get('problem', {}).get('title', '') + }, + "leaderboard": leaderboard, + "waiting_participants": waiting_participants, + "stats": { + "total_participants": len(participants), + "completed_submissions": len(completed_participants), + "waiting_submissions": len(waiting_participants), + "average_score": round(avg_score, 1), + "highest_score": max((p.get('score', 0) for p in completed_participants), default=0) + } + }) + except Exception as e: + print(f"❌ Error getting leaderboard: {str(e)}") + return jsonify({"error": str(e)}), 500 + +@bp.route("/get-problem/", methods=["GET", "OPTIONS"]) +def get_exam_problem(exam_code): + """Get problem details for participants""" + 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") + return response + + try: + exam = db.exams.find_one({"exam_code": exam_code.upper()}) + if not exam: + return jsonify({"error": "Exam not found"}), 404 + + return jsonify({ + "success": True, + "problem": exam.get('problem', {}), + "exam_info": { + "title": exam['title'], + "status": exam['status'], + "duration_minutes": exam['duration_minutes'] + } + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@bp.route("/host-dashboard/", methods=["GET", "OPTIONS"]) +def get_host_dashboard(exam_code): + """Get comprehensive host dashboard data""" + 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") + return response + + try: + exam = db.exams.find_one({"exam_code": exam_code.upper()}) + if not exam: + return jsonify({"error": "Exam not found"}), 404 + + participants = exam.get('participants', []) + + # Separate participants by status + completed_participants = [p for p in participants if p.get('completed', False)] + waiting_participants = [p for p in participants if not p.get('completed', False)] + + # Sort leaderboard + leaderboard = sorted( + completed_participants, + key=lambda x: (-x.get('score', 0), x.get('submission_time', datetime.now())) + ) + + # Add ranks + for i, participant in enumerate(leaderboard): + participant['rank'] = i + 1 + + # Calculate time statistics + current_time = datetime.now() + start_time = exam.get('start_time') + end_time = exam.get('end_time') + + time_elapsed = 0 + time_remaining = 0 + + if start_time: + time_elapsed = int((current_time - start_time).total_seconds()) + + if end_time and current_time < end_time: + time_remaining = int((end_time - current_time).total_seconds()) + + return jsonify({ + "success": True, + "exam_info": { + "exam_code": exam['exam_code'], + "title": exam['title'], + "status": exam['status'], + "duration_minutes": exam['duration_minutes'], + "max_participants": exam.get('max_participants', 50), + "created_at": exam.get('created_at'), + "start_time": start_time, + "end_time": end_time, + "time_elapsed": time_elapsed, + "time_remaining": time_remaining + }, + "participants": { + "total": len(participants), + "completed": len(completed_participants), + "working": len(waiting_participants), + "all_participants": sorted(participants, key=lambda x: x.get('joined_at', datetime.now())), + "recent_joins": sorted(participants, key=lambda x: x.get('joined_at', datetime.now()), reverse=True)[:5] + }, + "leaderboard": leaderboard, + "statistics": { + "average_score": sum(p.get('score', 0) for p in completed_participants) / len(completed_participants) if completed_participants else 0, + "highest_score": max((p.get('score', 0) for p in completed_participants), default=0), + "lowest_score": min((p.get('score', 0) for p in completed_participants), default=0), + "completion_rate": (len(completed_participants) / len(participants) * 100) if participants else 0 + }, + "problem": exam.get('problem', {}) + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@bp.route("/debug-join-data", methods=["POST", "OPTIONS"]) +def debug_join_data(): + """Debug what data is actually being received""" + 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 + + print(f"🔍 Raw request data: {request.data}") + print(f"🔍 Request JSON: {request.json}") + print(f"🔍 Content-Type: {request.headers.get('Content-Type')}") + + return jsonify({ + "received_raw": request.data.decode() if request.data else None, + "received_json": request.json, + "content_type": request.headers.get('Content-Type'), + "success": True + }) + +@bp.route("/test", methods=["GET"]) +def test_exam_route(): + """Test if exam routes are working""" + return jsonify({ + "success": True, + "message": "Exam routes are working", + "timestamp": datetime.now().isoformat(), + "available_routes": [ + "/api/exam/create-exam", + "/api/exam/join-exam", + "/api/exam/start-exam", + "/api/exam/leaderboard/", + "/api/exam/get-problem/", + "/api/exam/host-dashboard/", + "/api/exam/debug-join-data" + ] + }) + +@bp.route("/", methods=["GET"]) +def exam_root(): + """Exam route root""" + return jsonify({ + "message": "OpenLearnX Exam API", + "available_endpoints": [ + "/api/exam/create-exam", + "/api/exam/join-exam", + "/api/exam/start-exam", + "/api/exam/leaderboard/", + "/api/exam/get-problem/", + "/api/exam/host-dashboard/", + "/api/exam/test", + "/api/exam/debug-join-data" + ] + }) diff --git a/backend/services/compiler_service.py b/backend/services/compiler_service.py new file mode 100644 index 0000000..6a77221 --- /dev/null +++ b/backend/services/compiler_service.py @@ -0,0 +1,42 @@ +import docker +import tempfile +import os # ✅ Make sure this is imported +import subprocess +import time +from typing import Dict, List, Any +import json + +class CompilerService: + def __init__(self): + self.client = docker.from_env() + self.language_configs = { + 'python': { + 'image': 'python:3.9-alpine', + 'file_ext': '.py', + 'run_command': 'python /app/solution{ext}', + 'timeout': 10 + }, + 'java': { + 'image': 'openjdk:11-alpine', + 'file_ext': '.java', + 'run_command': 'cd /app && javac Solution.java && java Solution', + 'timeout': 15 + }, + 'c': { + 'image': 'gcc:9-alpine', + 'file_ext': '.c', + 'run_command': 'cd /app && gcc -o solution solution.c && ./solution', + 'timeout': 15 + }, + 'bash': { + 'image': 'bash:5-alpine', + 'file_ext': '.sh', + 'run_command': 'bash /app/solution.sh', + 'timeout': 10 + } + } + + # ... rest of your compiler service code + +# Global compiler service instance +compiler_service = CompilerService() diff --git a/backend/services/real_compiler_service.py b/backend/services/real_compiler_service.py new file mode 100644 index 0000000..c73070d --- /dev/null +++ b/backend/services/real_compiler_service.py @@ -0,0 +1,305 @@ +import docker +import tempfile +import os +import subprocess +import time +import uuid +import json +import threading +from typing import Dict, List, Any, Optional +from datetime import datetime +import queue +import signal + +class RealCompilerService: + def __init__(self): + self.client = docker.from_env() + self.execution_queue = queue.Queue() + self.active_executions = {} + self.max_concurrent_executions = 5 + + # Enhanced language configurations with real execution + self.language_configs = { + 'python': { + 'image': 'python:3.11-slim', + 'file_ext': '.py', + 'compile_command': None, # Python doesn't need compilation + 'run_command': 'python /app/code.py', + 'timeout': 30, + 'memory_limit': '256m', + 'cpu_limit': '0.5' + }, + 'java': { + 'image': 'openjdk:17-alpine', + 'file_ext': '.java', + 'compile_command': 'javac /app/Main.java', + 'run_command': 'java -cp /app Main', + 'timeout': 30, + 'memory_limit': '512m', + 'cpu_limit': '0.5' + }, + 'cpp': { + 'image': 'gcc:latest', + 'file_ext': '.cpp', + 'compile_command': 'g++ -o /app/program /app/code.cpp -std=c++17', + 'run_command': '/app/program', + 'timeout': 30, + 'memory_limit': '256m', + 'cpu_limit': '0.5' + }, + 'c': { + 'image': 'gcc:latest', + 'file_ext': '.c', + 'compile_command': 'gcc -o /app/program /app/code.c', + 'run_command': '/app/program', + 'timeout': 30, + 'memory_limit': '256m', + 'cpu_limit': '0.5' + }, + 'javascript': { + 'image': 'node:18-alpine', + 'file_ext': '.js', + 'compile_command': None, + 'run_command': 'node /app/code.js', + 'timeout': 30, + 'memory_limit': '256m', + 'cpu_limit': '0.5' + }, + 'bash': { + 'image': 'bash:5.2-alpine3.18', + 'file_ext': '.sh', + 'compile_command': None, + 'run_command': 'bash /app/code.sh', + 'timeout': 30, + 'memory_limit': '128m', + 'cpu_limit': '0.3' + }, + 'go': { + 'image': 'golang:1.21-alpine', + 'file_ext': '.go', + 'compile_command': 'go build -o /app/program /app/code.go', + 'run_command': '/app/program', + 'timeout': 30, + 'memory_limit': '512m', + 'cpu_limit': '0.5' + }, + 'rust': { + 'image': 'rust:1.75-alpine', + 'file_ext': '.rs', + 'compile_command': 'rustc /app/code.rs -o /app/program', + 'run_command': '/app/program', + 'timeout': 60, # Rust compilation can be slow + 'memory_limit': '1g', + 'cpu_limit': '1.0' + } + } + + # Start execution worker + self.start_execution_worker() + + def start_execution_worker(self): + """Start background worker for code execution""" + def worker(): + while True: + try: + execution_task = self.execution_queue.get(timeout=1) + self._execute_task(execution_task) + self.execution_queue.task_done() + except queue.Empty: + continue + except Exception as e: + print(f"Execution worker error: {e}") + + worker_thread = threading.Thread(target=worker, daemon=True) + worker_thread.start() + + def execute_code(self, code: str, language: str, input_data: str = "", + execution_id: str = None) -> Dict[str, Any]: + """Execute code with real output capture""" + if language not in self.language_configs: + return {"error": f"Language '{language}' not supported"} + + if not execution_id: + execution_id = str(uuid.uuid4()) + + config = self.language_configs[language] + + try: + # Create execution context + execution_context = { + 'execution_id': execution_id, + 'code': code, + 'language': language, + 'input_data': input_data, + 'config': config, + 'start_time': datetime.now(), + 'status': 'running' + } + + self.active_executions[execution_id] = execution_context + + # Execute in Docker container + result = self._execute_in_container(execution_context) + + # Update execution context + execution_context['status'] = 'completed' + execution_context['end_time'] = datetime.now() + execution_context['result'] = result + + return { + "success": True, + "execution_id": execution_id, + "output": result.get('output', ''), + "error": result.get('error', ''), + "execution_time": result.get('execution_time', 0), + "memory_used": result.get('memory_used', 0), + "exit_code": result.get('exit_code', 0), + "language": language, + "timestamp": datetime.now().isoformat() + } + + except Exception as e: + return { + "error": f"Execution failed: {str(e)}", + "execution_id": execution_id, + "language": language + } + finally: + # Clean up + if execution_id in self.active_executions: + del self.active_executions[execution_id] + + def _execute_in_container(self, context: Dict) -> Dict[str, Any]: + """Execute code in secure Docker container""" + code = context['code'] + language = context['language'] + input_data = context['input_data'] + config = context['config'] + + with tempfile.TemporaryDirectory() as temp_dir: + # Prepare code file + filename = f"code{config['file_ext']}" if language != 'java' else "Main.java" + file_path = os.path.join(temp_dir, filename) + + with open(file_path, 'w', encoding='utf-8') as f: + f.write(code) + + # Prepare input file + input_file = os.path.join(temp_dir, 'input.txt') + with open(input_file, 'w', encoding='utf-8') as f: + f.write(input_data) + + try: + start_time = time.time() + + # Create and run container + container = self.client.containers.run( + config['image'], + command=self._build_execution_command(config, filename), + volumes={temp_dir: {'bind': '/app', 'mode': 'rw'}}, + working_dir='/app', + mem_limit=config['memory_limit'], + cpu_period=100000, + cpu_quota=int(float(config['cpu_limit']) * 100000), + network_mode='none', # No network access + remove=True, + detach=False, + stdin_open=True, + tty=False, + timeout=config['timeout'], + # Security options + cap_drop=['ALL'], + security_opt=['no-new-privileges'], + read_only=False, + tmpfs={'/tmp': 'rw,noexec,nosuid,size=100m'} + ) + + execution_time = time.time() - start_time + output = container.decode('utf-8') + + return { + "output": output.strip(), + "error": "", + "exit_code": 0, + "execution_time": round(execution_time, 3), + "memory_used": self._get_memory_usage(container) + } + + except docker.errors.ContainerError as e: + return { + "output": "", + "error": f"Runtime error (exit code {e.exit_status}): {e.stderr.decode('utf-8') if e.stderr else 'Unknown error'}", + "exit_code": e.exit_status, + "execution_time": time.time() - start_time, + "memory_used": 0 + } + except docker.errors.APIError as e: + return { + "output": "", + "error": f"Docker API error: {str(e)}", + "exit_code": -1, + "execution_time": 0, + "memory_used": 0 + } + except Exception as e: + return { + "output": "", + "error": f"Execution error: {str(e)}", + "exit_code": -1, + "execution_time": 0, + "memory_used": 0 + } + + def _build_execution_command(self, config: Dict, filename: str) -> str: + """Build the execution command for the container""" + commands = [] + + # Add compilation step if needed + if config.get('compile_command'): + commands.append(config['compile_command']) + + # Add execution command with input redirection + run_cmd = config['run_command'] + if '<' not in run_cmd: # Add input redirection if not present + run_cmd += ' < /app/input.txt 2>&1' + commands.append(run_cmd) + + # Combine commands + return f"sh -c '{' && '.join(commands)}'" + + def _get_memory_usage(self, container) -> int: + """Get memory usage from container stats""" + try: + stats = container.stats(stream=False) + memory_usage = stats['memory']['usage'] + return memory_usage + except: + return 0 + + def get_supported_languages(self) -> List[Dict[str, str]]: + """Get list of supported languages with details""" + return [ + { + 'id': lang_id, + 'name': lang_id.title(), + 'extension': config['file_ext'], + 'timeout': config['timeout'], + 'memory_limit': config['memory_limit'] + } + for lang_id, config in self.language_configs.items() + ] + + def get_execution_status(self, execution_id: str) -> Optional[Dict]: + """Get status of a running execution""" + return self.active_executions.get(execution_id) + + def cancel_execution(self, execution_id: str) -> bool: + """Cancel a running execution""" + if execution_id in self.active_executions: + # Implementation would involve stopping the Docker container + del self.active_executions[execution_id] + return True + return False + +# Create global instance +real_compiler_service = RealCompilerService() diff --git a/backend/services/wallet_service.py b/backend/services/wallet_service.py new file mode 100644 index 0000000..bb2ad47 --- /dev/null +++ b/backend/services/wallet_service.py @@ -0,0 +1,53 @@ +from web3 import Web3 +from eth_account import Account +from datetime import datetime +import hashlib +import secrets +import os # ✅ Add this missing import + +class WalletService: + def __init__(self, web3_provider_url): + self.w3 = Web3(Web3.HTTPProvider(web3_provider_url)) + + def verify_wallet_signature(self, wallet_address, signature, message): + """Verify wallet signature for authentication""" + try: + # Recover the address from signature + message_hash = Web3.keccak(text=message) + recovered_address = self.w3.eth.account.recover_message_hash(message_hash, signature=signature) + + return recovered_address.lower() == wallet_address.lower() + except Exception as e: + print(f"Signature verification error: {e}") + return False + + def generate_auth_message(self, wallet_address, exam_code): + """Generate message for wallet signing""" + timestamp = int(datetime.now().timestamp()) + nonce = secrets.token_hex(16) + + message = f"""OpenLearnX Exam Authentication + +Wallet: {wallet_address} +Exam Code: {exam_code} +Timestamp: {timestamp} +Nonce: {nonce} + +Sign this message to join the coding exam.""" + + return message, timestamp, nonce + + def create_wallet_session(self, wallet_address, exam_code, signature): + """Create authenticated wallet session""" + session_id = hashlib.sha256(f"{wallet_address}{exam_code}{datetime.now().timestamp()}".encode()).hexdigest() + + return { + "session_id": session_id, + "wallet_address": wallet_address, + "exam_code": exam_code, + "authenticated_at": datetime.now(), + "signature": signature + } + +# Create the service instance +wallet_service = WalletService(os.getenv('WEB3_PROVIDER_URL', 'http://127.0.0.1:8545')) diff --git a/frontend/app/coding/[problemId]/page.tsx b/frontend/app/coding/[problemId]/page.tsx index 651ae1b..aab1a22 100644 --- a/frontend/app/coding/[problemId]/page.tsx +++ b/frontend/app/coding/[problemId]/page.tsx @@ -1,11 +1,441 @@ -import { CodingProblemView } from "@/components/coding-problem-view" +'use client' +import React, { useState, useEffect } from 'react' +import { useRouter, useParams } from 'next/navigation' +import { Play, Clock, CheckCircle, XCircle, ArrowLeft, Trophy } from 'lucide-react' -interface CodingProblemPageProps { - params: { - problemId: string +interface TestCase { + input: string + expected: string + description: string +} + +interface Problem { + id: string + title: string + description: string + difficulty: 'Easy' | 'Medium' | 'Hard' + category: string + examples: TestCase[] + constraints: string[] + hints: string[] + starter_code: string + function_name: string +} + +export default function ProblemPage() { + const params = useParams() + const router = useRouter() + const problemId = params.problemId as string + + const [problem, setProblem] = useState(null) + const [code, setCode] = useState('') + const [output, setOutput] = useState('') + const [testResults, setTestResults] = useState([]) + const [isRunning, setIsRunning] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [showHints, setShowHints] = useState(false) + const [activeTab, setActiveTab] = useState<'description' | 'examples' | 'constraints'>('description') + + useEffect(() => { + loadProblem(problemId) + }, [problemId]) + + const loadProblem = async (id: string) => { + try { + // In a real app, this would fetch from your backend + const problems: Record = { + 'string-capitalizer': { + id: 'string-capitalizer', + title: 'String Capitalizer', + description: 'Write a function that takes a string as input and returns the string converted to uppercase.', + difficulty: 'Easy', + category: 'String Manipulation', + examples: [ + { input: 'hello', expected: 'HELLO', description: 'Basic string conversion' }, + { input: 'world', expected: 'WORLD', description: 'Another basic case' }, + { input: 'Python Programming', expected: 'PYTHON PROGRAMMING', description: 'String with spaces' } + ], + constraints: [ + 'Input string length will be between 1 and 1000 characters', + 'Input may contain letters, numbers, and spaces', + 'Function must be named exactly "capitalize_string"' + ], + hints: [ + 'Python has a built-in method to convert strings to uppercase', + 'The upper() method can be used on any string', + 'Remember to return the result, not just print it' + ], + starter_code: 'def capitalize_string(text):\n # Write your solution here\n pass', + function_name: 'capitalize_string' + }, + 'reverse-string': { + id: 'reverse-string', + title: 'Reverse String', + description: 'Write a function that takes a string and returns it reversed.', + difficulty: 'Easy', + category: 'String Manipulation', + examples: [ + { input: 'hello', expected: 'olleh', description: 'Basic string reversal' }, + { input: 'python', expected: 'nohtyp', description: 'Another basic case' }, + { input: 'OpenLearnX', expected: 'XnraeLnepO', description: 'Mixed case string' } + ], + constraints: [ + 'Input string length will be between 1 and 1000 characters', + 'Function must be named exactly "reverse_string"' + ], + hints: [ + 'Python strings can be sliced with [::-1]', + 'You can also use the reversed() function', + 'Remember to return the result' + ], + starter_code: 'def reverse_string(text):\n # Write your solution here\n pass', + function_name: 'reverse_string' + }, + 'fibonacci': { + id: 'fibonacci', + title: 'Fibonacci Sequence', + description: 'Write a function that returns the nth number in the Fibonacci sequence.', + difficulty: 'Medium', + category: 'Algorithms', + examples: [ + { input: '0', expected: '0', description: 'First Fibonacci number' }, + { input: '1', expected: '1', description: 'Second Fibonacci number' }, + { input: '5', expected: '5', description: 'Sixth Fibonacci number (0,1,1,2,3,5)' } + ], + constraints: [ + 'n will be between 0 and 30', + 'Function must be named exactly "fibonacci"', + 'Should handle edge cases for n=0 and n=1' + ], + hints: [ + 'Base cases: fib(0) = 0, fib(1) = 1', + 'For n > 1: fib(n) = fib(n-1) + fib(n-2)', + 'Consider using iteration instead of recursion for better performance' + ], + starter_code: 'def fibonacci(n):\n # Write your solution here\n pass', + function_name: 'fibonacci' + } + } + + const selectedProblem = problems[id] + if (selectedProblem) { + setProblem(selectedProblem) + setCode(selectedProblem.starter_code) + } else { + // Problem not found + router.push('/coding') + } + } catch (error) { + console.error('Failed to load problem:', error) + router.push('/coding') + } } -} -export default function CodingProblemPage({ params }: CodingProblemPageProps) { - return + const runCode = async () => { + if (!problem || !code.trim()) return + + setIsRunning(true) + setOutput('') + setTestResults([]) + + try { + const response = await fetch('http://127.0.0.1:5000/api/coding/execute', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + code, + language: 'python', + problem_id: problem.id, + test_cases: problem.examples + }) + }) + + const result = await response.json() + + if (result.success) { + setOutput(result.output || '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 (!problem || !code.trim()) return + + setIsSubmitting(true) + + try { + const response = await fetch('http://127.0.0.1:5000/api/coding/submit', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + code, + problem_id: problem.id + }) + }) + + const result = await response.json() + + if (result.success) { + alert(`Solution submitted! Score: ${result.score}% (${result.passed_tests}/${result.total_tests} tests passed)`) + } else { + alert(`Submission failed: ${result.error}`) + } + } catch (error) { + alert('Failed to submit solution') + } finally { + setIsSubmitting(false) + } + } + + const getDifficultyColor = (difficulty: string) => { + switch (difficulty) { + case 'Easy': return 'text-green-600 bg-green-100' + case 'Medium': return 'text-yellow-600 bg-yellow-100' + case 'Hard': return 'text-red-600 bg-red-100' + default: return 'text-gray-600 bg-gray-100' + } + } + + if (!problem) { + return ( +
+
+
+

Loading problem...

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

{problem.title}

+
+ + {problem.difficulty} + + {problem.category} +
+
+
+ +
+ + + +
+
+
+ +
+ {/* Problem Description */} +
+ {/* Navigation Tabs */} +
+
+ {(['description', 'examples', 'constraints'] as const).map((tab) => ( + + ))} +
+ +
+ {activeTab === 'description' && ( +
+

{problem.description}

+
+ )} + + {activeTab === 'examples' && ( +
+

Examples:

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

Constraints:

+
    + {problem.constraints.map((constraint, index) => ( +
  • + + {constraint} +
  • + ))} +
+
+ )} +
+
+ + {/* Hints Section */} + {showHints && ( +
+

💡 Hints:

+
    + {problem.hints.map((hint, index) => ( +
  • + {index + 1}. + {hint} +
  • + ))} +
+
+ )} +
+ + {/* Code Editor & Results */} +
+ {/* Code Editor */} +
+
+

Code Editor

+ Python +
+ +