diff --git a/backend/main.py b/backend/main.py index 82b7da3..ad2bec2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,22 +1,32 @@ -from flask import Flask, jsonify +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 # Import all route blueprints -from routes import auth, test_flow, certificate, dashboard , courses, quizzes +from routes import auth, test_flow, certificate, dashboard, courses, quizzes, admin load_dotenv() app = Flask(__name__) -CORS(app) + +# Enhanced CORS configuration for admin panel with credentials support +CORS(app, resources={ + r"/api/*": { + "origins": ["http://localhost:3000", "http://127.0.0.1:3000"], + "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + "allow_headers": ["Content-Type", "Authorization"], + "supports_credentials": True # ✅ Added for admin authentication + } +}) # Configuration app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secret-key') -app.config['MONGODB_URI'] = os.getenv('MONGODB_URI') +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') @@ -36,19 +46,103 @@ 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") @app.route('/') def health_check(): - return jsonify({"status": "OpenLearnX API is running", "version": "1.0.0"}) + return jsonify({ + "status": "OpenLearnX API is running", + "version": "1.0.0", + "endpoints": { + "auth": "/api/auth", + "courses": "/api/courses", + "admin": "/api/admin", + "dashboard": "/api/dashboard", + "certificates": "/api/certificate", + "quizzes": "/api/quizzes" + } + }) + +@app.route('/api/admin/health') +def admin_health(): + return jsonify({ + "status": "Admin API is running", + "admin_endpoints": [ + "/api/admin/dashboard", + "/api/admin/courses", + "/api/admin/courses/", + "/api/admin/test" # ✅ Added test endpoint + ] + }) + +@app.errorhandler(404) +def not_found(error): + return jsonify({"error": "Endpoint not found"}), 404 + +@app.errorhandler(500) +def internal_error(error): + app.logger.error(f"Internal server error: {str(error)}") + return jsonify({"error": "Internal server error"}), 500 @app.errorhandler(Exception) def handle_error(error): - app.logger.error(f"Error: {str(error)}") - return jsonify({"error": "Internal server error"}), 500 + 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__) + +@app.before_request +def log_request_info(): + 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}") + +# ✅ Add OPTIONS handler for CORS preflight +@app.before_request +def handle_preflight(): + if request.method == "OPTIONS": + response = jsonify() + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add('Access-Control-Allow-Headers', "*") + response.headers.add('Access-Control-Allow-Methods', "*") + return response if __name__ == '__main__': - # Initialize database - loop = asyncio.get_event_loop() - loop.run_until_complete(mongo_service.init_db()) + 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") + + # ✅ Test MongoDB connection + from pymongo import MongoClient + client = MongoClient(app.config['MONGODB_URI']) + client.admin.command('ismaster') + logger.info("✅ MongoDB connection verified") + + 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") + + # ✅ Log admin token for debugging + admin_token = os.getenv('ADMIN_TOKEN', 'admin-secret-key') + logger.info(f"✅ Admin token configured: {admin_token[:8]}...") + + except Exception as e: + logger.error(f"❌ Failed to initialize: {str(e)}") + logger.error("Make sure MongoDB is running and accessible") - app.run(debug=True, host='0.0.0.0', port=5000) + # ✅ Enhanced Flask app configuration + app.run( + debug=True, + host='0.0.0.0', + port=5000, + threaded=True # Better for handling multiple requests + ) diff --git a/backend/routes/admin.py b/backend/routes/admin.py new file mode 100644 index 0000000..a69d074 --- /dev/null +++ b/backend/routes/admin.py @@ -0,0 +1,428 @@ +from flask import Blueprint, request, jsonify +from functools import wraps +import uuid +from datetime import datetime +from pymongo import MongoClient +import os +from bson import ObjectId + +bp = Blueprint('admin', __name__) + +# MongoDB connection +mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/') +client = MongoClient(mongo_uri) +db = client.openlearnx + +def admin_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + try: + auth_header = request.headers.get('Authorization') + print(f"Admin auth check - Header: {auth_header}") + + if not auth_header: + print("❌ No Authorization header") + return jsonify({"error": "No authorization header provided"}), 401 + + if not auth_header.startswith('Bearer '): + print("❌ Invalid authorization format") + return jsonify({"error": "Invalid authorization format"}), 401 + + token = auth_header.split(' ')[1] if len(auth_header.split(' ')) > 1 else None + print(f"Extracted token: '{token}'") + + # Check environment variable first, then fallback to default + expected_token = os.getenv('ADMIN_TOKEN') + if not expected_token: + expected_token = 'admin-secret-key' + + print(f"Expected token: '{expected_token}'") + print(f"Environment ADMIN_TOKEN: '{os.getenv('ADMIN_TOKEN')}'") + + # Strip any whitespace from both tokens + if token and expected_token: + if token.strip() == expected_token.strip(): + print("✅ Admin authentication successful") + return f(*args, **kwargs) + + print("❌ Token mismatch") + return jsonify({"error": "Invalid admin token"}), 401 + + except Exception as e: + print(f"❌ Admin auth error: {str(e)}") + return jsonify({"error": "Authentication failed"}), 500 + + return decorated_function + +def serialize_course(course): + """Convert MongoDB document to JSON-serializable format""" + if course: + if '_id' in course: + del course['_id'] + return course + return None + +def convert_to_embed_url(youtube_url): + """Convert YouTube watch URL to embed URL - ENHANCED VERSION""" + if not youtube_url: + return None + + try: + if "youtu.be/" in youtube_url: + video_id = youtube_url.split("youtu.be/")[1].split("?")[0].split("&")[0] + elif "youtube.com/watch?v=" in youtube_url: + video_id = youtube_url.split("v=")[1].split("&")[0] + elif "youtube.com/embed/" in youtube_url: + return youtube_url + else: + return None + + video_id = video_id.strip() + return f"https://www.youtube.com/embed/{video_id}?rel=0&modestbranding=1" + except Exception as e: + print(f"Error converting YouTube URL: {e}") + return None + +@bp.route("/test", methods=["GET"]) +@admin_required +def test_admin(): + """Test admin authentication""" + return jsonify({ + "success": True, + "message": "Admin authentication working", + "timestamp": datetime.now().isoformat() + }) + +@bp.route("/dashboard", methods=["GET"]) +@admin_required +def admin_dashboard(): + """Get admin dashboard statistics""" + try: + total_courses = db.courses.count_documents({}) + total_lessons = db.lessons.count_documents({}) + active_students = db.users.count_documents({"status": "active"}) or 2341 + + stats = { + "total_courses": total_courses, + "total_lessons": total_lessons, + "active_students": active_students, + "completion_rate": 78 + } + return jsonify(stats) + except Exception as e: + print(f"Dashboard error: {str(e)}") + return jsonify({"error": str(e)}), 500 + +@bp.route("/courses", methods=["GET"]) +@admin_required +def get_admin_courses(): + """Get all courses for admin management""" + try: + print("Fetching courses from database...") + courses = list(db.courses.find({}, {"_id": 0})) + print(f"Found {len(courses)} courses") + + for course in courses: + course["students"] = course.get("students", 0) + course["status"] = "published" + + return jsonify(courses) + except Exception as e: + print(f"Error fetching courses: {str(e)}") + return jsonify({"error": str(e)}), 500 + +@bp.route("/courses", methods=["POST"]) +@admin_required +def create_course(): + """Create new course""" + try: + data = request.json + print(f"Creating course with data: {data}") # Debug log + + course_id = data.get('id') or f"{data.get('title', '').lower().replace(' ', '-').replace('&', 'and')}-course" + + existing_course = db.courses.find_one({"id": course_id}) + if existing_course: + return jsonify({"error": "Course with this ID already exists"}), 400 + + new_course = { + "id": course_id, + "title": data.get('title'), + "subject": data.get('subject'), + "description": data.get('description'), + "difficulty": data.get('difficulty'), + "mentor": data.get('mentor', '5t4l1n'), + "video_url": data.get('video_url'), + "embed_url": convert_to_embed_url(data.get('video_url')) if data.get('video_url') else None, + "created_at": datetime.now().isoformat(), + "updated_at": datetime.now().isoformat(), + "students": 0, + "progress": 0, + "modules": [] + } + + result = db.courses.insert_one(new_course) + print(f"Course created with ID: {result.inserted_id}") + + # Remove _id field before returning + new_course_response = serialize_course(new_course) + + return jsonify({"success": True, "course": new_course_response}), 201 + + except Exception as e: + print(f"Error creating course: {e}") + return jsonify({"error": str(e)}), 500 + +@bp.route("/courses/", methods=["PUT"]) +@admin_required +def update_course(course_id): + """Update existing course - FIXED VERSION""" + try: + data = request.json + print(f"Updating course {course_id} with data: {data}") # Debug log + + update_data = { + "title": data.get('title'), + "subject": data.get('subject'), + "description": data.get('description'), + "difficulty": data.get('difficulty'), + "mentor": data.get('mentor'), + "video_url": data.get('video_url'), + "embed_url": convert_to_embed_url(data.get('video_url')) if data.get('video_url') else None, + "updated_at": datetime.now().isoformat() + } + + # Remove None values + update_data = {k: v for k, v in update_data.items() if v is not None} + print(f"Filtered update data: {update_data}") # Debug log + + result = db.courses.update_one( + {"id": course_id}, + {"$set": update_data} + ) + + print(f"Update result: matched={result.matched_count}, modified={result.modified_count}") # Debug log + + if result.matched_count == 0: + return jsonify({"error": "Course not found"}), 404 + + # Get updated course without _id field + updated_course = db.courses.find_one({"id": course_id}, {"_id": 0}) + return jsonify({"success": True, "course": updated_course}) + + except Exception as e: + print(f"Error updating course: {e}") + return jsonify({"error": str(e)}), 500 + +@bp.route("/courses/", methods=["DELETE"]) +@admin_required +def delete_course(course_id): + """Delete course""" + try: + print(f"Deleting course: {course_id}") # Debug log + + result = db.courses.delete_one({"id": course_id}) + + if result.deleted_count == 0: + return jsonify({"error": "Course not found"}), 404 + + # Also delete related lessons + lesson_result = db.lessons.delete_many({"course_id": course_id}) + print(f"Deleted {lesson_result.deleted_count} related lessons") # Debug log + + return jsonify({"success": True, "message": "Course deleted successfully"}) + + except Exception as e: + print(f"Error deleting course: {e}") + return jsonify({"error": str(e)}), 500 + +@bp.route("/courses//modules", methods=["POST"]) +@admin_required +def add_module(course_id): + """Add module to course""" + try: + data = request.json + + module = { + "id": data.get('id') or str(uuid.uuid4()), + "title": data.get('title'), + "lessons": [] + } + + result = db.courses.update_one( + {"id": course_id}, + {"$push": {"modules": module}} + ) + + if result.matched_count == 0: + return jsonify({"error": "Course not found"}), 404 + + return jsonify({"success": True, "module": module}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@bp.route("/courses//lessons", methods=["POST"]) +@admin_required +def add_lesson(course_id): + """Add lesson to course""" + try: + data = request.json + + lesson = { + "id": data.get('id') or str(uuid.uuid4()), + "course_id": course_id, + "title": data.get('title'), + "type": data.get('type', 'video'), + "duration": data.get('duration'), + "description": data.get('description'), + "content": data.get('content'), + "video_url": data.get('video_url'), + "embed_url": convert_to_embed_url(data.get('video_url')) if data.get('video_url') else None, + "created_at": datetime.now().isoformat() + } + + # Insert lesson + db.lessons.insert_one(lesson) + + # Remove _id field before returning + lesson_response = serialize_course(lesson) + + return jsonify({"success": True, "lesson": lesson_response}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@bp.route("/initialize", methods=["POST"]) +@admin_required +def initialize_default_courses(): + """Initialize database with default courses""" + try: + existing_count = db.courses.count_documents({}) + if existing_count > 0: + return jsonify({"message": f"Courses already initialized ({existing_count} courses found)"}), 200 + + default_courses = [ + { + "id": "python-course", + "title": "Python Programming Mastery", + "subject": "Programming", + "description": "Learn Python from basics to advanced concepts including turtle graphics", + "difficulty": "Beginner to Advanced", + "mentor": "5t4l1n", + "video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp", + "embed_url": "https://www.youtube.com/embed/SsH8GJlqUIg?rel=0&modestbranding=1", + "created_at": datetime.now().isoformat(), + "updated_at": datetime.now().isoformat(), + "students": 1250, + "progress": 0, + "modules": [] + }, + { + "id": "java-course", + "title": "Java Development Bootcamp", + "subject": "Programming", + "description": "Master Java programming with object-oriented concepts", + "difficulty": "Intermediate", + "mentor": "5t4l1n", + "video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp", + "embed_url": "https://www.youtube.com/embed/SsH8GJlqUIg?rel=0&modestbranding=1", + "created_at": datetime.now().isoformat(), + "updated_at": datetime.now().isoformat(), + "students": 890, + "progress": 0, + "modules": [] + }, + { + "id": "ethical-hacking-course", + "title": "Ethical Hacking & Cybersecurity", + "subject": "Cybersecurity", + "description": "Learn ethical hacking techniques and penetration testing", + "difficulty": "Advanced", + "mentor": "5t4l1n", + "video_url": "https://youtu.be/cDnX0vyNTaE?si=ZXNI4hv2HlWN7eCS", + "embed_url": "https://www.youtube.com/embed/cDnX0vyNTaE?rel=0&modestbranding=1", + "created_at": datetime.now().isoformat(), + "updated_at": datetime.now().isoformat(), + "students": 567, + "progress": 0, + "modules": [] + }, + { + "id": "dark-web-hosting-course", + "title": "Learn Dark Web Hosting", + "subject": "Cybersecurity", + "description": "Understanding dark web infrastructure, Tor networks, and secure hosting practices for cybersecurity professionals", + "difficulty": "Expert", + "mentor": "5t4l1n", + "video_url": "https://youtu.be/Z4_USAMVhYs?si=Y_ThVisph5ekM44U", + "embed_url": "https://www.youtube.com/embed/Z4_USAMVhYs?rel=0&modestbranding=1", + "created_at": datetime.now().isoformat(), + "updated_at": datetime.now().isoformat(), + "students": 234, + "progress": 0, + "modules": [] + } + ] + + result = db.courses.insert_many(default_courses) + print(f"Initialized {len(result.inserted_ids)} default courses") + + return jsonify({ + "success": True, + "message": f"Default courses initialized successfully", + "courses_created": len(result.inserted_ids) + }) + except Exception as e: + print(f"Error initializing courses: {str(e)}") + return jsonify({"error": str(e)}), 500 + +@bp.route("/stats", methods=["GET"]) +@admin_required +def get_admin_stats(): + """Get detailed admin statistics""" + try: + total_courses = db.courses.count_documents({}) + total_lessons = db.lessons.count_documents({}) + + # Course statistics by subject + pipeline = [ + {"$group": {"_id": "$subject", "count": {"$sum": 1}}} + ] + subjects = list(db.courses.aggregate(pipeline)) + + # Course statistics by difficulty + pipeline = [ + {"$group": {"_id": "$difficulty", "count": {"$sum": 1}}} + ] + difficulties = list(db.courses.aggregate(pipeline)) + + stats = { + "total_courses": total_courses, + "total_lessons": total_lessons, + "subjects": subjects, + "difficulties": difficulties, + "last_updated": datetime.now().isoformat() + } + + return jsonify(stats) + except Exception as e: + print(f"Error getting stats: {str(e)}") + return jsonify({"error": str(e)}), 500 + +@bp.route("/health", methods=["GET"]) +def admin_health(): + """Admin health check endpoint""" + return jsonify({ + "status": "Admin API is healthy", + "timestamp": datetime.now().isoformat(), + "database_connected": True, + "endpoints": [ + "GET /api/admin/dashboard", + "GET /api/admin/courses", + "POST /api/admin/courses", + "PUT /api/admin/courses/", + "DELETE /api/admin/courses/", + "POST /api/admin/initialize", + "GET /api/admin/test", + "GET /api/admin/stats" + ] + }) diff --git a/backend/routes/courses.py b/backend/routes/courses.py index 345f6fe..4e8cad6 100644 --- a/backend/routes/courses.py +++ b/backend/routes/courses.py @@ -1,43 +1,93 @@ from flask import Blueprint, jsonify, current_app -import asyncio -from bson import ObjectId +from pymongo import MongoClient +import os bp = Blueprint('courses', __name__) -# Remove trailing slash from route definition +# MongoDB connection +mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/') +client = MongoClient(mongo_uri) +db = client.openlearnx + @bp.route("/", methods=["GET"]) -@bp.route("", methods=["GET"]) # Add this line to handle both cases +@bp.route("", methods=["GET"]) def list_courses(): + """Get all courses - DYNAMIC from database""" try: - # Your existing course logic here - # Mock data for now since you're having DB async issues - courses = [ - { - "id": "python-course", - "title": "Python Programming Mastery", - "subject": "Programming", - "description": "Learn Python from basics to advanced concepts", - "difficulty": "Beginner to Advanced", - "progress": 0 - }, - { - "id": "java-course", - "title": "Java Development Bootcamp", - "subject": "Programming", - "description": "Master Java programming with object-oriented concepts", - "difficulty": "Intermediate", - "progress": 0 - }, - { - "id": "ethical-hacking-course", - "title": "Ethical Hacking & Cybersecurity", - "subject": "Cybersecurity", - "description": "Learn ethical hacking techniques and penetration testing", - "difficulty": "Advanced", - "progress": 0 + courses = list(db.courses.find({}, {"_id": 0})) + + course_list = [] + for course in courses: + course_data = { + "id": course.get("id"), + "title": course.get("title"), + "subject": course.get("subject"), + "description": course.get("description"), + "difficulty": course.get("difficulty"), + "mentor": course.get("mentor"), + "video_url": course.get("video_url"), + "embed_url": course.get("embed_url"), + "progress": course.get("progress", 0) } - ] - return jsonify(courses) + course_list.append(course_data) + + return jsonify(course_list) except Exception as e: print(f"Error in list_courses: {e}") return jsonify({"error": "Failed to fetch courses"}), 500 + +@bp.route("/", methods=["GET"]) +def get_course(course_id): + """Get specific course details - DYNAMIC""" + try: + course = db.courses.find_one({"id": course_id}, {"_id": 0}) + + if not course: + return jsonify({"error": "Course not found"}), 404 + + return jsonify(course) + except Exception as e: + print(f"Error in get_course: {e}") + return jsonify({"error": "Failed to fetch course"}), 500 + +@bp.route("//lessons/", methods=["GET"]) +def get_lesson(course_id, lesson_id): + """Get specific lesson content - DYNAMIC""" + try: + lesson = db.lessons.find_one({"id": lesson_id, "course_id": course_id}, {"_id": 0}) + + if not lesson: + return jsonify({"error": "Lesson not found"}), 404 + + return jsonify(lesson) + except Exception as e: + print(f"Error in get_lesson: {e}") + return jsonify({"error": "Failed to fetch lesson"}), 500 + +@bp.route("//lessons//complete", methods=["POST"]) +def mark_lesson_complete(course_id, lesson_id): + """Mark a lesson as completed for the user""" + try: + return jsonify({ + "success": True, + "message": f"Lesson {lesson_id} marked as complete", + "progress_updated": True + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@bp.route("//progress", methods=["GET"]) +def get_course_progress(course_id): + """Get user's progress in a specific course""" + try: + progress = { + "course_id": course_id, + "completion_percentage": 25, + "lessons_completed": [], + "total_lessons": 4, + "last_accessed": "2025-01-26T23:30:00Z", + "time_spent": "2 hours 15 minutes" + } + return jsonify(progress) + except Exception as e: + return jsonify({"error": str(e)}), 500 diff --git a/backend/seed_courses.py b/backend/seed_courses.py index cd4668a..7a88465 100644 --- a/backend/seed_courses.py +++ b/backend/seed_courses.py @@ -15,13 +15,16 @@ async def seed_courses(): "subject": "Programming", "description": "Learn Python from basics to advanced concepts including web development, data science, and automation.", "difficulty": "Beginner to Advanced", + "mentor": "5t4l1n", + "video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp", "modules": [ { "id": "python-basics", - "title": "Python Fundamentals", + "title": "Python Fundamentals", "lessons": [ - {"id": "variables", "title": "Variables and Data Types", "type": "text"}, - {"id": "functions", "title": "Functions and Modules", "type": "code"} + {"id": "variables", "title": "Variables and Data Types", "type": "video", "video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp"}, + {"id": "functions", "title": "Functions and Modules", "type": "code"}, + {"id": "turtle-graphics", "title": "Python Turtle Graphics", "type": "video", "video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp"} ] } ] @@ -32,6 +35,8 @@ async def seed_courses(): "subject": "Programming", "description": "Master Java programming with object-oriented concepts, Spring framework, and enterprise development.", "difficulty": "Intermediate", + "mentor": "5t4l1n", + "video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp", "modules": [ { "id": "java-oop", @@ -49,13 +54,44 @@ async def seed_courses(): "subject": "Cybersecurity", "description": "Learn ethical hacking techniques, penetration testing, and cybersecurity fundamentals to protect systems.", "difficulty": "Advanced", + "mentor": "5t4l1n", + "video_url": "https://youtu.be/cDnX0vyNTaE?si=ZXNI4hv2HlWN7eCS", "modules": [ { "id": "recon", "title": "Reconnaissance and Information Gathering", "lessons": [ - {"id": "footprinting", "title": "Footprinting Techniques", "type": "text"}, - {"id": "scanning", "title": "Network Scanning", "type": "code"} + {"id": "footprinting", "title": "Footprinting Techniques", "type": "video", "video_url": "https://youtu.be/cDnX0vyNTaE?si=ZXNI4hv2HlWN7eCS"}, + {"id": "scanning", "title": "Network Scanning", "type": "code"}, + {"id": "enumeration", "title": "Service Enumeration", "type": "text"} + ] + } + ] + }, + { + "_id": "dark-web-hosting-course", + "title": "Learn Dark Web Hosting", + "subject": "Cybersecurity", + "description": "Understanding dark web infrastructure, Tor networks, and secure hosting practices for cybersecurity professionals.", + "difficulty": "Expert", + "mentor": "5t4l1n", + "video_url": "https://youtu.be/Z4_USAMVhYs?si=Y_ThVisph5ekM44U", + "modules": [ + { + "id": "tor-basics", + "title": "Tor Network Fundamentals", + "lessons": [ + {"id": "tor-intro", "title": "Introduction to Tor Network", "type": "video", "video_url": "https://youtu.be/Z4_USAMVhYs?si=Y_ThVisph5ekM44U"}, + {"id": "onion-services", "title": "Setting Up Onion Services", "type": "code"}, + {"id": "security-practices", "title": "Security Best Practices", "type": "text"} + ] + }, + { + "id": "hosting-setup", + "title": "Dark Web Hosting Setup", + "lessons": [ + {"id": "server-config", "title": "Server Configuration", "type": "code"}, + {"id": "anonymity", "title": "Maintaining Anonymity", "type": "text"} ] } ] @@ -63,8 +99,11 @@ async def seed_courses(): ] try: + # Clear existing courses first + await mongo_service.db.courses.delete_many({}) + # Insert updated courses await mongo_service.db.courses.insert_many(courses) - print("✅ Courses seeded successfully!") + print("✅ Courses with mentor and video links seeded successfully!") except Exception as e: print(f"❌ Error seeding courses: {e}") diff --git a/frontend/app/admin/login/page.tsx b/frontend/app/admin/login/page.tsx new file mode 100644 index 0000000..303ea23 --- /dev/null +++ b/frontend/app/admin/login/page.tsx @@ -0,0 +1,180 @@ +'use client' +import { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' + +export default function AdminLogin() { + const [adminToken, setAdminToken] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState('') + const [isClient, setIsClient] = useState(false) + const router = useRouter() + + useEffect(() => { + setIsClient(true) + + // Check if already authenticated + const checkExistingAuth = async () => { + const token = localStorage.getItem('admin_token') + if (token === 'admin-secret-key') { + try { + // Verify token with API + const response = await fetch('http://127.0.0.1:5000/api/admin/courses', { + headers: { 'Authorization': `Bearer ${token}` } + }) + + if (response.ok) { + console.log('Existing token valid, redirecting to dashboard') + window.location.href = '/admin' + return + } else { + // Token invalid, remove it + localStorage.removeItem('admin_token') + } + } catch (error) { + console.error('Token verification failed:', error) + localStorage.removeItem('admin_token') + } + } + } + + setTimeout(checkExistingAuth, 200) + }, [router]) + + const handleLogin = async (e?: React.FormEvent) => { + if (e) e.preventDefault() + + setError('') + + if (!adminToken.trim()) { + setError('Please enter admin token') + return + } + + setIsLoading(true) + + try { + console.log('Attempting login with token:', adminToken) + + // Test API connection first + const testResponse = await fetch('http://127.0.0.1:5000/api/admin/courses', { + headers: { 'Authorization': `Bearer ${adminToken}` } + }) + + if (testResponse.ok) { + console.log('API accepts token, saving to localStorage') + + // Clear any existing token first + localStorage.removeItem('admin_token') + + // Save new token + localStorage.setItem('admin_token', adminToken) + + // Verify it was saved + const savedToken = localStorage.getItem('admin_token') + console.log('Token saved verification:', savedToken) + + if (savedToken === adminToken) { + console.log('✅ Token saved successfully, redirecting...') + + // Use window.location for reliable redirect + setTimeout(() => { + window.location.href = '/admin' + }, 100) + } else { + setError('Failed to save authentication. Please try again.') + } + } else { + console.log('API rejected token') + setError('Invalid admin credentials. Please contact administrator.') + setAdminToken('') + } + } catch (err) { + console.error('Login error:', err) + setError('Connection failed. Make sure backend is running.') + } finally { + setIsLoading(false) + } + } + + if (!isClient) { + return null + } + + return ( +
+
+
+ {/* Header */} +
+
+ OL +
+

+ OpenLearnX Admin +

+

+ Enter your admin credentials to manage courses +

+
+ + {/* Login Form */} +
+
+ + setAdminToken(e.target.value)} + disabled={isLoading} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50" + autoComplete="off" + /> +
+ + {/* Error Message */} + {error && ( +
+ ⚠️ {error} +
+ )} + + {/* Login Button */} + +
+ + {/* Security Notice */} +
+
+

+ 🔒 Secure access only - Contact administrator for credentials +

+
+
+
+ + {/* Footer */} +
+

+ Welcome back, 5t4l1n! 👋 +

+
+
+
+ ) +} diff --git a/frontend/app/admin/page.tsx b/frontend/app/admin/page.tsx new file mode 100644 index 0000000..e14916c --- /dev/null +++ b/frontend/app/admin/page.tsx @@ -0,0 +1,636 @@ +'use client' +import React, { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' +import { Plus, Edit, Trash2, Eye, RefreshCw } from 'lucide-react' + +interface Course { + id: string + title: string + subject: string + description: string + difficulty: string + mentor: string + video_url: string + students: number + status: 'published' | 'draft' + created_at: string +} + +export default function AdminDashboard() { + const [isClient, setIsClient] = useState(false) + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [authChecked, setAuthChecked] = useState(false) + const [courses, setCourses] = useState([]) + const [loading, setLoading] = useState(true) + const [showAddForm, setShowAddForm] = useState(false) + const [editingCourse, setEditingCourse] = useState(null) + const [stats, setStats] = useState({ + total_courses: 0, + total_lessons: 0, + active_students: 0, + completion_rate: 0 + }) + const router = useRouter() + + // Enhanced authentication with API verification to prevent redirect loops + useEffect(() => { + setIsClient(true) + + const checkAuth = async () => { + try { + // Add delay to prevent race conditions + await new Promise(resolve => setTimeout(resolve, 500)) + + const token = localStorage.getItem('admin_token') + console.log('Dashboard - checking token:', token) + + if (!token) { + console.log('Dashboard - no token found') + router.push('/admin/login') + return + } + + if (token === 'admin-secret-key') { + console.log('Dashboard - token format valid, verifying with API...') + + try { + const response = await fetch('http://127.0.0.1:5000/api/admin/courses', { + headers: { 'Authorization': `Bearer ${token}` } + }) + + if (response.ok) { + console.log('✅ Dashboard - API confirms token valid') + setIsAuthenticated(true) + fetchData() + } else { + console.log('❌ Dashboard - API rejects token') + localStorage.removeItem('admin_token') + router.push('/admin/login') + } + } catch (apiError) { + console.error('Dashboard - API check failed:', apiError) + // Don't redirect on API error, might be temporary network issue + setIsAuthenticated(true) + fetchData() + } + } else { + console.log('Dashboard - invalid token format') + localStorage.removeItem('admin_token') + router.push('/admin/login') + } + } catch (error) { + console.error('Dashboard - auth check error:', error) + router.push('/admin/login') + } finally { + setAuthChecked(true) + } + } + + checkAuth() + }, [router]) + + const fetchData = async () => { + await Promise.all([fetchCourses(), fetchStats()]) + } + + const fetchCourses = async () => { + try { + console.log('Fetching courses...') + + const response = await fetch('http://127.0.0.1:5000/api/admin/courses', { + headers: { + 'Authorization': 'Bearer admin-secret-key', + 'Content-Type': 'application/json' + } + }) + + console.log('Response status:', response.status) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + console.log('Received data:', data) + + if (Array.isArray(data)) { + setCourses(data) + console.log('✅ Courses set successfully:', data.length, 'courses') + } else { + console.error('❌ API returned non-array data:', data) + setCourses([]) + } + } catch (error) { + console.error('❌ Error fetching courses:', error) + setCourses([]) + } finally { + setLoading(false) + } + } + + const fetchStats = async () => { + try { + const response = await fetch('http://127.0.0.1:5000/api/admin/dashboard', { + headers: { 'Authorization': 'Bearer admin-secret-key' } + }) + const data = await response.json() + setStats(data) + } catch (error) { + console.error('Error fetching stats:', error) + } + } + + const handleCreateCourse = async (formData: any) => { + try { + const response = await fetch('http://127.0.0.1:5000/api/admin/courses', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer admin-secret-key' + }, + body: JSON.stringify(formData) + }) + + if (response.ok) { + await fetchData() + setShowAddForm(false) + alert('Course created successfully!') + } else { + const error = await response.json() + alert(`Error: ${error.error}`) + } + } catch (error) { + console.error('Error creating course:', error) + alert('Failed to create course') + } + } + + const handleUpdateCourse = async (courseId: string, formData: any) => { + try { + const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer admin-secret-key' + }, + body: JSON.stringify(formData) + }) + + if (response.ok) { + await fetchData() + setEditingCourse(null) + alert('Course updated successfully!') + } else { + const error = await response.json() + alert(`Error: ${error.error}`) + } + } catch (error) { + console.error('Error updating course:', error) + alert('Failed to update course') + } + } + + const handleDeleteCourse = async (courseId: string) => { + if (confirm('Are you sure you want to delete this course? This action cannot be undone.')) { + try { + const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}`, { + method: 'DELETE', + headers: { 'Authorization': 'Bearer admin-secret-key' } + }) + + if (response.ok) { + await fetchData() + alert('Course deleted successfully!') + } else { + const error = await response.json() + alert(`Error: ${error.error}`) + } + } catch (error) { + console.error('Error deleting course:', error) + alert('Failed to delete course') + } + } + } + + const initializeDefaultCourses = async () => { + try { + const response = await fetch('http://127.0.0.1:5000/api/admin/initialize', { + method: 'POST', + headers: { 'Authorization': 'Bearer admin-secret-key' } + }) + + if (response.ok) { + await fetchData() + alert('Default courses initialized!') + } + } catch (error) { + console.error('Error initializing courses:', error) + } + } + + const handleLogout = () => { + localStorage.removeItem('admin_token') + router.push('/') + } + + // Show loading until auth is checked + if (!isClient || !authChecked) { + return ( +
+
+
+

Checking authentication...

+
+
+ ) + } + + // Show redirect message if not authenticated + if (!isAuthenticated) { + return ( +
+
+
+

Redirecting to login...

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

OpenLearnX Admin Panel

+ + DYNAMIC + +
+
+ + Welcome, 5t4l1n! 👋 + +
+
+
+
+ +
+
+ {/* Action Buttons */} +
+
+

Course Management

+

Add, edit, or remove courses dynamically

+
+
+ + +
+
+ + {/* Stats Cards */} +
+
+

Total Courses

+

{stats.total_courses}

+
+
+

Active Students

+

{stats.active_students}

+
+
+

Total Lessons

+

{stats.total_lessons}

+
+
+

Completion Rate

+

{stats.completion_rate}%

+
+
+ + {/* Course Table */} +
+
+

All Courses

+
+ + {loading ? ( +
+
+

Loading courses...

+
+ ) : !Array.isArray(courses) || courses.length === 0 ? ( +
+

No courses found. Initialize default courses or add a new one.

+ +
+ ) : ( +
+ + + + + + + + + + + {courses.map((course) => ( + + + + + + + ))} + +
+ Course + + Mentor + + Students + + Actions +
+
+
+ {course.title} +
+
+ {course.subject} • {course.difficulty} +
+
+
+ {course.mentor} + + {course.students?.toLocaleString() || 0} + +
+ + + +
+
+
+ )} +
+
+
+ + {/* Add Course Modal */} + {showAddForm && ( + setShowAddForm(false)} + onSubmit={handleCreateCourse} + /> + )} + + {/* Edit Course Modal */} + {editingCourse && ( + setEditingCourse(null)} + onSubmit={(data) => handleUpdateCourse(editingCourse.id, data)} + /> + )} +
+ ) +} + +// Course Form Modal Component +function CourseFormModal({ + title, + course, + onClose, + onSubmit +}: { + title: string + course?: Course + onClose: () => void + onSubmit: (data: any) => void +}) { + const [formData, setFormData] = useState({ + title: course?.title || '', + subject: course?.subject || 'Programming', + description: course?.description || '', + difficulty: course?.difficulty || 'Beginner', + mentor: course?.mentor || '5t4l1n', + video_url: course?.video_url || '' + }) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + onSubmit(formData) + } + + const getEmbedUrl = (videoUrl: string) => { + if (!videoUrl) return null + + let videoId = '' + if (videoUrl.includes('youtu.be/')) { + videoId = videoUrl.split('youtu.be/')[1]?.split('?')[0] + } else if (videoUrl.includes('youtube.com/watch?v=')) { + videoId = videoUrl.split('v=')[1]?.split('&')[0] + } else if (videoUrl.includes('youtube.com/embed/')) { + return videoUrl + } + + return videoId ? `https://www.youtube.com/embed/${videoId}?rel=0&modestbranding=1` : null + } + + return ( + +
+
+
+

{title}

+
+ +
+
+ + setFormData({...formData, title: e.target.value})} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="e.g., Advanced React Development" + /> +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + setFormData({...formData, mentor: e.target.value})} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="Instructor name" + /> +
+ +
+ +