from flask import Blueprint, request, jsonify, current_app from datetime import datetime import jwt import os import uuid import time import secrets import string import logging import hashlib import random import threading from bson import ObjectId from pymongo import MongoClient bp = Blueprint('certificate', __name__) # Set up logging logger = logging.getLogger(__name__) def get_user_from_token(token): """Extract user from JWT token with enhanced error handling""" try: secret_key = current_app.config.get('JWT_SECRET_KEY') or current_app.config.get('SECRET_KEY') if not secret_key: logger.error("No JWT secret key found in configuration") return None, None payload = jwt.decode(token, secret_key, algorithms=['HS256']) user_id = payload.get('user_id') or payload.get('sub') wallet_address = payload.get('wallet_address') logger.info(f"โœ… Token decoded successfully for user: {user_id}") return user_id, wallet_address except Exception as e: logger.error(f"Error decoding JWT token: {str(e)}") return None, None def get_db_connection(): """Get MongoDB database connection with enhanced error handling""" try: # Try to get from Flask config first mongo_service = current_app.config.get('MONGO_SERVICE') if mongo_service and hasattr(mongo_service, 'db'): print("๐Ÿ“Š Using Flask config database connection") return mongo_service.db # Fallback to direct connection with explicit URI mongodb_uri = current_app.config.get('MONGODB_URI', 'mongodb://localhost:27017/') print(f"๐Ÿ“Š Connecting directly to MongoDB: {mongodb_uri}") client = MongoClient(mongodb_uri) db = client.openlearnx # Test the connection by running a simple command db.command('ping') print("โœ… Database connection successful!") return db except Exception as e: print(f"โŒ Database connection failed: {e}") logger.error(f"Database connection failed: {e}") return None def generate_truly_unique_certificate_id(): """Generate GUARANTEED unique certificate ID""" # Method 1: Nanosecond timestamp for uniqueness nano_timestamp = str(time.time_ns()) # Method 2: High entropy random random_component = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(8)) # Method 3: UUID component uuid_component = str(uuid.uuid4()).replace('-', '').upper()[:4] # Method 4: System-specific component system_component = f"{os.getpid()}{threading.get_ident()}"[-4:] # Combine and ensure 12 characters combined = nano_timestamp[-3:] + random_component[:4] + uuid_component[:3] + system_component[-2:] certificate_id = combined[:12].upper() # Force different from problematic ID if certificate_id == "DG1ITFZ7DT5B": certificate_id = "UNIQUE" + str(int(time.time()))[-6:] certificate_id = certificate_id[:12].upper() print(f"๐Ÿ†” Generated unique ID: {certificate_id}") return certificate_id def generate_unique_share_code(): """Generate unique 8-character share code""" timestamp = str(int(time.time() * 1000000))[-4:] random_part = ''.join(secrets.choice(string.ascii_lowercase + string.digits) for _ in range(4)) share_code = timestamp + random_part print(f"๐Ÿ”— Generated share code: {share_code}") return share_code @bp.route('/mint', methods=['POST', 'OPTIONS']) def mint_certificate(): """FIXED: Create certificate with guaranteed database saving""" if request.method == "OPTIONS": return jsonify({'status': 'ok'}) try: print("\n" + "="*50) print("๐ŸŽ“ STARTING CERTIFICATE MINTING PROCESS") print("="*50) # Get request data data = request.json if not data: print("โŒ No request data provided") return jsonify({"error": "Request data required"}), 400 print(f"๐Ÿ“ฅ Received data: {data}") # Validate required fields required_fields = ['user_name', 'course_id'] for field in required_fields: if not data.get(field): print(f"โŒ Missing required field: {field}") return jsonify({"error": f"Missing required field: {field}"}), 400 # Get student's entered name student_entered_name = data.get('user_name', '').strip() if not student_entered_name: print("โŒ Student name is empty") return jsonify({"error": "Student name is required"}), 400 print(f"๐ŸŽ“ STUDENT NAME: '{student_entered_name}'") print(f"๐Ÿ“š COURSE ID: '{data['course_id']}'") # Get user ID (from token or default) auth_header = request.headers.get('Authorization', '') user_id = 'anonymous' wallet_address = None if auth_header.startswith('Bearer '): token = auth_header.replace('Bearer ', '') token_user_id, wallet_address = get_user_from_token(token) if token_user_id: user_id = token_user_id print(f"๐Ÿ‘ค USER ID: '{user_id}'") # โœ… CRITICAL: Get database connection and verify it works print("\n๐Ÿ“Š ESTABLISHING DATABASE CONNECTION...") db = get_db_connection() if db is None: print("โŒ CRITICAL: Database connection failed!") return jsonify({"error": "Database connection failed - check MongoDB server"}), 500 print("โœ… Database connection established successfully!") # Test database write capability try: test_doc = {"test": "connection", "timestamp": datetime.now()} test_result = db.test_collection.insert_one(test_doc) db.test_collection.delete_one({"_id": test_result.inserted_id}) print("โœ… Database write test successful!") except Exception as e: print(f"โŒ Database write test failed: {e}") return jsonify({"error": "Database is not writable"}), 500 # โœ… Check if certificate already exists print(f"\n๐Ÿ” Checking for existing certificate...") try: existing_certificate = db.certificates.find_one({ "user_id": user_id, "course_id": data['course_id'] }) if existing_certificate: print(f"๐Ÿ“œ Certificate already exists: {existing_certificate['certificate_id']}") return jsonify({ "success": True, "certificate": { "certificate_id": existing_certificate['certificate_id'], "user_name": student_entered_name, # Always return entered name "course_title": existing_certificate.get('course_title', 'Course'), "mentor_name": existing_certificate.get('instructor_name', existing_certificate.get('mentor_name', 'OpenLearnX Instructor')), "completion_date": existing_certificate['completion_date'], "share_code": existing_certificate.get('share_code'), "public_url": existing_certificate.get('public_url'), "unique_url": f"/certificate/{existing_certificate.get('share_code')}", "message": "Certificate already exists!" } }), 200 except Exception as e: print(f"โš ๏ธ Error checking existing certificates: {e}") # Get course information print(f"\n๐Ÿ“š Getting course information...") try: course = db.courses.find_one({"id": data['course_id']}) if not course: print(f"โš ๏ธ Course not found, creating default") course = { "id": data['course_id'], "title": data.get('course_title', f"Course {data['course_id']}"), "mentor": "OpenLearnX Instructor" } else: print(f"โœ… Course found: {course['title']}") except Exception as e: print(f"โŒ Error finding course: {e}") course = { "id": data['course_id'], "title": data.get('course_title', f"Course {data['course_id']}"), "mentor": "OpenLearnX Instructor" } # โœ… GENERATE UNIQUE IDs print(f"\n๐Ÿ†” Generating unique IDs...") certificate_id = generate_truly_unique_certificate_id() share_code = generate_unique_share_code() token_id = str(uuid.uuid4()) print(f"๐Ÿ†” Certificate ID: {certificate_id}") print(f"๐Ÿ”— Share Code: {share_code}") print(f"๐ŸŽซ Token ID: {token_id}") # Check for ID collisions in database print(f"\n๐Ÿ” Checking for ID collisions...") max_attempts = 10 for attempt in range(max_attempts): existing_cert = db.certificates.find_one({"certificate_id": certificate_id}) existing_share = db.certificates.find_one({"share_code": share_code}) if not existing_cert and not existing_share: print(f"โœ… IDs are unique (checked attempt {attempt + 1})") break else: print(f"โš ๏ธ ID collision detected on attempt {attempt + 1}, regenerating...") certificate_id = generate_truly_unique_certificate_id() share_code = generate_unique_share_code() # Get instructor name (separate from student) instructor_name = course.get('mentor', 'OpenLearnX Instructor') if isinstance(instructor_name, dict): instructor_name = instructor_name.get('name', 'OpenLearnX Instructor') # Prevent student name from being used as instructor if instructor_name == student_entered_name: instructor_name = 'OpenLearnX Instructor' print(f"\n๐Ÿ‘ฅ Names configured:") print(f" ๐ŸŽ“ Student: '{student_entered_name}'") print(f" ๐Ÿ‘จโ€๐Ÿซ Instructor: '{instructor_name}'") # Get wallet information wallet_id = wallet_address or data.get('wallet_id', f'test-wallet-{int(time.time())}') # โœ… CREATE COMPLETE CERTIFICATE DOCUMENT print(f"\n๐Ÿ“„ Creating certificate document...") certificate_document = { # โœ… UNIQUE IDENTIFIERS "certificate_id": certificate_id, "token_id": token_id, "share_code": share_code, # โœ… STUDENT INFORMATION (EXPLICIT) "student_name": student_entered_name, # Explicit student field "user_name": student_entered_name, # Main name field # โœ… USER & COURSE INFO "user_id": user_id, "course_id": data['course_id'], "course_title": course['title'], # โœ… INSTRUCTOR INFORMATION (SEPARATE) "mentor_name": instructor_name, # Instructor name "instructor_name": instructor_name, # Explicit instructor field "course_mentor": instructor_name, # Backward compatibility # โœ… WALLET & BLOCKCHAIN "wallet_address": wallet_id, "encrypted_wallet_id": { "iv": "test_iv_" + secrets.token_hex(8), "encrypted": "test_encrypted_" + secrets.token_hex(8), "algorithm": "AES-256-CBC" }, # โœ… TIMESTAMPS "completion_date": datetime.now().isoformat(), "created_at": datetime.now().isoformat(), "updated_at": datetime.now().isoformat(), "minted_at": datetime.now().isoformat(), # โœ… CERTIFICATE METADATA "status": "active", "issued_by": "OpenLearnX", "verification_url": f"/certificates/{certificate_id}", "share_url": f"/certificate/{share_code}", "public_url": f"http://localhost:3000/certificate/{share_code}", "blockchain_hash": f"0x{secrets.token_hex(32)}", # โœ… ANALYTICS "is_revoked": False, "view_count": 0, "shared_count": 0 } # โœ… LOG COMPLETE DOCUMENT BEFORE SAVING print(f"\n๐Ÿ“‹ CERTIFICATE DOCUMENT TO SAVE:") print(f" ๐Ÿ†” Certificate ID: {certificate_document['certificate_id']}") print(f" ๐ŸŽ“ Student Name: '{certificate_document['student_name']}'") print(f" ๐ŸŽ“ User Name: '{certificate_document['user_name']}'") print(f" ๐Ÿ‘จโ€๐Ÿซ Instructor: '{certificate_document['instructor_name']}'") print(f" ๐Ÿ“š Course: '{certificate_document['course_title']}'") print(f" ๐Ÿ”— Share Code: {certificate_document['share_code']}") # โœ… CRITICAL: SAVE TO DATABASE WITH VERIFICATION print(f"\n๐Ÿ’พ SAVING TO DATABASE...") try: # Create indexes to ensure uniqueness try: db.certificates.create_index([("certificate_id", 1)], unique=True, background=True) db.certificates.create_index([("share_code", 1)], unique=True, background=True) print("โœ… Database indexes created") except Exception as e: print(f"โš ๏ธ Index creation warning: {e}") # Insert the document insert_result = db.certificates.insert_one(certificate_document) print(f"โœ… DOCUMENT INSERTED SUCCESSFULLY!") print(f" ๐Ÿ“Š MongoDB ID: {insert_result.inserted_id}") print(f" ๐Ÿ†” Certificate ID: {certificate_id}") # โœ… VERIFY THE DOCUMENT WAS ACTUALLY SAVED print(f"\n๐Ÿ” VERIFYING DOCUMENT WAS SAVED...") saved_document = db.certificates.find_one({"certificate_id": certificate_id}) if saved_document: print(f"โœ… VERIFICATION SUCCESSFUL!") print(f" ๐Ÿ†” Saved Certificate ID: {saved_document['certificate_id']}") print(f" ๐ŸŽ“ Saved Student Name: '{saved_document['student_name']}'") print(f" ๐Ÿ“Š MongoDB ID: {saved_document['_id']}") else: print(f"โŒ VERIFICATION FAILED - Document not found!") return jsonify({"error": "Failed to verify certificate was saved"}), 500 except Exception as e: print(f"โŒ DATABASE SAVE ERROR: {e}") logger.error(f"Database save error: {e}") # Try alternative save method if "E11000" in str(e): print("โš ๏ธ Duplicate key error, generating new ID...") certificate_id = generate_truly_unique_certificate_id() certificate_document["certificate_id"] = certificate_id certificate_document["verification_url"] = f"/certificates/{certificate_id}" try: insert_result = db.certificates.insert_one(certificate_document) print(f"โœ… Saved with new ID: {certificate_id}") except Exception as retry_error: print(f"โŒ Retry failed: {retry_error}") return jsonify({"error": "Failed to save certificate after retry"}), 500 else: return jsonify({"error": f"Database save failed: {str(e)}"}), 500 # โœ… PREPARE RESPONSE print(f"\n๐Ÿ“ค PREPARING RESPONSE...") certificate_response = { "certificate_id": certificate_document['certificate_id'], "token_id": certificate_document['token_id'], "share_code": certificate_document['share_code'], # โœ… STUDENT INFO (GUARANTEED CORRECT) "user_name": student_entered_name, "student_name": student_entered_name, # โœ… COURSE INFO "course_title": certificate_document['course_title'], # โœ… INSTRUCTOR INFO "mentor_name": instructor_name, "instructor_name": instructor_name, # โœ… OTHER INFO "completion_date": certificate_document['completion_date'], "verification_url": certificate_document['verification_url'], "share_url": certificate_document['share_url'], "public_url": certificate_document['public_url'], "unique_url": f"/certificate/{certificate_document['share_code']}", "blockchain_hash": certificate_document['blockchain_hash'], "wallet_address": certificate_document['wallet_address'], "message": f"Certificate {certificate_document['certificate_id']} created successfully for {student_entered_name}!" } print(f"โœ… RESPONSE PREPARED:") print(f" ๐Ÿ†” Certificate ID: {certificate_response['certificate_id']}") print(f" ๐ŸŽ“ Student: '{certificate_response['user_name']}'") print(f" ๐Ÿ‘จโ€๐Ÿซ Instructor: '{certificate_response['mentor_name']}'") print("\n" + "="*50) print("๐ŸŽ‰ CERTIFICATE MINTING COMPLETED SUCCESSFULLY!") print("="*50) return jsonify({ "success": True, "certificate": certificate_response }), 201 except Exception as e: print(f"\nโŒ CRITICAL ERROR IN MINT_CERTIFICATE:") print(f"Error: {str(e)}") import traceback print(f"Traceback: {traceback.format_exc()}") logger.error(f"Critical error in mint_certificate: {str(e)}") return jsonify({"error": f"Critical error: {str(e)}"}), 500 @bp.route('/', methods=['GET', 'OPTIONS']) def get_certificate_by_id(certificate_id): """Get certificate by ID with proper database access""" if request.method == "OPTIONS": return jsonify({'status': 'ok'}) try: print(f"๐Ÿ” Getting certificate with ID: {certificate_id}") db = get_db_connection() if db is None: return jsonify({"error": "Database connection failed"}), 500 # Search by certificate_id or share_code certificate = db.certificates.find_one({ "$or": [ {"certificate_id": certificate_id}, {"share_code": certificate_id}, {"certificate_id": {"$regex": f"^{certificate_id}$", "$options": "i"}}, {"share_code": {"$regex": f"^{certificate_id}$", "$options": "i"}} ] }) if not certificate: return jsonify({"error": "Certificate not found"}), 404 if certificate.get('is_revoked', False): return jsonify({"error": "Certificate has been revoked"}), 410 # Increment view count try: db.certificates.update_one( {"_id": certificate["_id"]}, {"$inc": {"view_count": 1}} ) except Exception as e: print(f"Failed to increment view count: {e}") # Return with proper field mapping certificate_response = { "certificate_id": certificate['certificate_id'], "share_code": certificate.get('share_code'), "user_name": certificate.get('student_name', certificate.get('user_name', 'Student')), "student_name": certificate.get('student_name', certificate.get('user_name', 'Student')), "course_title": certificate['course_title'], "mentor_name": certificate.get('instructor_name', certificate.get('mentor_name', certificate.get('course_mentor', 'OpenLearnX Instructor'))), "instructor_name": certificate.get('instructor_name', certificate.get('mentor_name', certificate.get('course_mentor', 'OpenLearnX Instructor'))), "completion_date": certificate['completion_date'], "status": certificate.get('status', 'active'), "issued_by": certificate.get('issued_by', 'OpenLearnX'), "blockchain_hash": certificate.get('blockchain_hash'), "wallet_address": certificate.get('wallet_address'), "view_count": certificate.get('view_count', 0), "public_url": certificate.get('public_url'), "is_verified": True, "is_revoked": certificate.get('is_revoked', False) } return jsonify({ "success": True, "certificate": certificate_response }) except Exception as e: print(f"Error getting certificate: {str(e)}") return jsonify({"error": "Failed to fetch certificate"}), 500 @bp.route('/verify/', methods=['GET', 'OPTIONS']) def verify_certificate_by_code(share_code): """Verify certificate by share code""" if request.method == "OPTIONS": return jsonify({'status': 'ok'}) try: print(f"๐Ÿ” Verifying certificate with code: {share_code}") db = get_db_connection() if db is None: return jsonify({ "success": False, "verified": False, "message": "Database connection failed" }), 500 certificate = db.certificates.find_one({ "$or": [ {"share_code": share_code}, {"certificate_id": share_code}, {"share_code": {"$regex": f"^{share_code}$", "$options": "i"}}, {"certificate_id": {"$regex": f"^{share_code}$", "$options": "i"}} ] }) if not certificate: return jsonify({ "success": False, "verified": False, "message": "Certificate not found" }), 404 if certificate.get('is_revoked', False): return jsonify({ "success": False, "verified": False, "message": "Certificate has been revoked" }), 410 # Increment view count try: db.certificates.update_one( {"_id": certificate["_id"]}, {"$inc": {"view_count": 1}} ) except Exception as e: print(f"Failed to increment view count: {e}") return jsonify({ "success": True, "verified": True, "certificate": { "certificate_id": certificate['certificate_id'], "share_code": certificate.get('share_code'), "student_name": certificate.get('student_name', certificate.get('user_name', 'Student')), "course_title": certificate['course_title'], "instructor_name": certificate.get('instructor_name', certificate.get('mentor_name', certificate.get('course_mentor', 'OpenLearnX Instructor'))), "completion_date": certificate['completion_date'], "issued_by": certificate.get('issued_by', 'OpenLearnX'), "blockchain_hash": certificate.get('blockchain_hash'), "view_count": certificate.get('view_count', 0) }, "message": "Certificate is valid and verified" }) except Exception as e: print(f"Error verifying certificate: {str(e)}") return jsonify({ "success": False, "verified": False, "message": "Verification failed" }), 500 @bp.route('/user/', methods=['GET', 'OPTIONS']) def get_user_certificates(user_id): """Get all certificates for a user""" if request.method == "OPTIONS": return jsonify({'status': 'ok'}) try: auth_header = request.headers.get('Authorization', '') if auth_header.startswith('Bearer '): token = auth_header.replace('Bearer ', '') token_user_id, wallet_address = get_user_from_token(token) if token_user_id and token_user_id != user_id: return jsonify({"error": "Unauthorized"}), 403 db = get_db_connection() if db is None: return jsonify({"error": "Database connection failed"}), 500 certificates = list(db.certificates.find( {"user_id": user_id}, {"_id": 0, "encrypted_wallet_id": 0} ).sort("created_at", -1)) return jsonify({ "success": True, "certificates": certificates, "count": len(certificates), "user_id": user_id }) except Exception as e: print(f"Error getting user certificates: {str(e)}") return jsonify({"error": "Failed to retrieve certificates"}), 500 @bp.route('/download/', methods=['GET', 'OPTIONS']) def download_certificate(certificate_id): """Download certificate as HTML for PDF conversion""" if request.method == "OPTIONS": return jsonify({'status': 'ok'}) try: db = get_db_connection() if db is None: return jsonify({"error": "Database connection failed"}), 500 certificate = db.certificates.find_one({ "$or": [ {"certificate_id": certificate_id}, {"share_code": certificate_id} ] }) if not certificate: return jsonify({"error": "Certificate not found"}), 404 if certificate.get('is_revoked', False): return jsonify({"error": "Certificate has been revoked"}), 410 # Generate HTML for PDF certificate_html = generate_certificate_html(certificate) return certificate_html, 200, { 'Content-Type': 'text/html', 'Content-Disposition': f'attachment; filename="Certificate_{certificate["certificate_id"]}.html"' } except Exception as e: print(f"Error downloading certificate: {str(e)}") return jsonify({"error": "Failed to download certificate"}), 500 @bp.route('/share/', methods=['POST', 'OPTIONS']) def track_certificate_share(certificate_id): """Track certificate sharing""" if request.method == "OPTIONS": return jsonify({'status': 'ok'}) try: db = get_db_connection() if db is None: return jsonify({"error": "Database connection failed"}), 500 result = db.certificates.update_one( { "$or": [ {"certificate_id": certificate_id}, {"share_code": certificate_id} ] }, {"$inc": {"shared_count": 1}} ) if result.matched_count == 0: return jsonify({"error": "Certificate not found"}), 404 return jsonify({ "success": True, "message": "Share tracked successfully" }) except Exception as e: print(f"Error tracking share: {str(e)}") return jsonify({"error": "Failed to track share"}), 500 @bp.route('/test-db', methods=['GET']) def test_database(): """Test database connectivity and write capability""" try: print("๐Ÿงช Testing database connection...") db = get_db_connection() if db is None: return jsonify({"error": "Database connection failed"}), 500 # Test write test_doc = { "test_id": str(uuid.uuid4()), "timestamp": datetime.now().isoformat(), "message": "Database test document" } result = db.test_certificates.insert_one(test_doc) # Test read saved_doc = db.test_certificates.find_one({"_id": result.inserted_id}) # Cleanup db.test_certificates.delete_one({"_id": result.inserted_id}) # Check existing certificates cert_count = db.certificates.count_documents({}) return jsonify({ "success": True, "database_connection": "working", "write_test": "successful", "read_test": "successful", "existing_certificates": cert_count, "test_document_id": str(result.inserted_id), "message": "Database is working properly!" }) except Exception as e: print(f"โŒ Database test failed: {e}") return jsonify({ "success": False, "error": str(e), "message": "Database test failed" }), 500 @bp.route('/list-all', methods=['GET']) def list_all_certificates(): """List all certificates in the database""" try: db = get_db_connection() if db is None: return jsonify({"error": "Database connection failed"}), 500 certificates = list(db.certificates.find({}, {"_id": 0}).sort("created_at", -1)) return jsonify({ "success": True, "certificates": certificates, "count": len(certificates), "message": f"Found {len(certificates)} certificates in database" }) except Exception as e: print(f"Error listing certificates: {e}") return jsonify({"error": str(e)}), 500 @bp.route('/test-generation', methods=['GET']) def test_generation(): """Test certificate ID generation""" try: ids = [] for i in range(10): cert_id = generate_truly_unique_certificate_id() share_code = generate_unique_share_code() ids.append({ "attempt": i + 1, "certificate_id": cert_id, "share_code": share_code, "timestamp": time.time() }) time.sleep(0.01) # Small delay # Check for duplicates cert_ids = [item["certificate_id"] for item in ids] share_codes = [item["share_code"] for item in ids] cert_duplicates = len(cert_ids) != len(set(cert_ids)) share_duplicates = len(share_codes) != len(set(share_codes)) return jsonify({ "success": True, "generated_ids": ids, "certificate_id_duplicates": cert_duplicates, "share_code_duplicates": share_duplicates, "unique_cert_ids": len(set(cert_ids)), "unique_share_codes": len(set(share_codes)), "message": "All IDs should be unique!" if not cert_duplicates and not share_duplicates else "Duplicates detected!" }) except Exception as e: return jsonify({"error": str(e)}), 500 def generate_certificate_html(certificate): """Generate HTML for certificate PDF download""" student_name = certificate.get('student_name', certificate.get('user_name', 'Student')) instructor_name = certificate.get('instructor_name', certificate.get('mentor_name', certificate.get('course_mentor', 'OpenLearnX Instructor'))) return f""" Certificate - {student_name}
๐Ÿ†

CERTIFICATE OF COMPLETION

This is to certify that
{student_name}
has successfully completed the course
"{certificate['course_title']}"
โœ… Completed on: {datetime.fromisoformat(certificate['completion_date']).strftime('%B %d, %Y')}
{instructor_name}
Course Instructor
Certificate ID: {certificate['certificate_id']}
OpenLearnX Learning Platform
๐Ÿ”’ Blockchain Verified Completion {f'
Blockchain Hash: {certificate.get("blockchain_hash", "")}' if certificate.get('blockchain_hash') else ''}
"""