mirror of
https://github.com/th30d4y/OpenLearnX.git
synced 2026-05-26 19:26:33 +00:00
dashboard & cource certificate Block chain
This commit is contained in:
+69
-716
@@ -39,12 +39,22 @@ except ImportError:
|
|||||||
DASHBOARD_AVAILABLE = False
|
DASHBOARD_AVAILABLE = False
|
||||||
print("⚠️ Dashboard routes not available")
|
print("⚠️ Dashboard routes not available")
|
||||||
|
|
||||||
|
# ✅ CRITICAL: Import certificate blueprint
|
||||||
|
try:
|
||||||
|
from routes.certificate import bp as certificate_bp
|
||||||
|
CERTIFICATE_BLUEPRINT_AVAILABLE = True
|
||||||
|
print("✅ Certificate blueprint with all fixes available")
|
||||||
|
except ImportError:
|
||||||
|
certificate_bp = None
|
||||||
|
CERTIFICATE_BLUEPRINT_AVAILABLE = False
|
||||||
|
print("❌ Certificate blueprint not available - check routes/certificate.py")
|
||||||
|
|
||||||
# Blueprints - Updated order and error handling
|
# Blueprints - Updated order and error handling
|
||||||
blueprints_to_register = [
|
blueprints_to_register = [
|
||||||
('auth', '/api/auth'),
|
('auth', '/api/auth'),
|
||||||
('test_flow', '/api/test'),
|
('test_flow', '/api/test'),
|
||||||
('certificate', '/api/certificate'),
|
('certificate', '/api/certificate'), # ✅ Use blueprint version
|
||||||
('dashboard', '/api/dashboard'), # ✅ Dashboard with comprehensive features
|
('dashboard', '/api/dashboard'),
|
||||||
('courses', '/api/courses'),
|
('courses', '/api/courses'),
|
||||||
('quizzes', '/api/quizzes'),
|
('quizzes', '/api/quizzes'),
|
||||||
('admin', '/api/admin'),
|
('admin', '/api/admin'),
|
||||||
@@ -84,183 +94,6 @@ except Exception as e:
|
|||||||
print(f"⚠️ AI Quiz Service unavailable: {str(e)}")
|
print(f"⚠️ AI Quiz Service unavailable: {str(e)}")
|
||||||
print("🔄 Server will continue without AI features")
|
print("🔄 Server will continue without AI features")
|
||||||
|
|
||||||
# ✅ FIXED Certificate Manager Class with Enhanced Error Handling
|
|
||||||
class CertificateManager:
|
|
||||||
def __init__(self):
|
|
||||||
# AES-256 key (store this securely in environment variables)
|
|
||||||
self.key = os.getenv('AES_ENCRYPTION_KEY', self._generate_key())
|
|
||||||
|
|
||||||
# Validate key length
|
|
||||||
try:
|
|
||||||
decoded_key = base64.b64decode(self.key)
|
|
||||||
if len(decoded_key) != 32: # AES-256 requires 32 bytes
|
|
||||||
logging.warning("AES key is not 32 bytes, regenerating...")
|
|
||||||
self.key = self._generate_key()
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Invalid AES key format, regenerating: {e}")
|
|
||||||
self.key = self._generate_key()
|
|
||||||
|
|
||||||
def _generate_key(self):
|
|
||||||
"""Generate a new AES-256 key (32 bytes)"""
|
|
||||||
key_bytes = get_random_bytes(32) # 32 bytes = 256 bits
|
|
||||||
return base64.b64encode(key_bytes).decode('utf-8')
|
|
||||||
|
|
||||||
def encrypt_wallet_id(self, wallet_id):
|
|
||||||
"""Encrypt wallet ID using AES-256 with improved error handling"""
|
|
||||||
try:
|
|
||||||
# Validate input
|
|
||||||
if not wallet_id:
|
|
||||||
logging.error("Empty wallet_id provided for encryption")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Ensure wallet_id is string and clean it
|
|
||||||
wallet_str = str(wallet_id).strip()
|
|
||||||
if not wallet_str:
|
|
||||||
logging.error("Wallet ID is empty after cleaning")
|
|
||||||
return None
|
|
||||||
|
|
||||||
logging.info(f"Encrypting wallet ID: {wallet_str[:10]}...") # Log first 10 chars for debugging
|
|
||||||
|
|
||||||
# Decode the base64 key
|
|
||||||
try:
|
|
||||||
key_bytes = base64.b64decode(self.key)
|
|
||||||
if len(key_bytes) != 32:
|
|
||||||
raise ValueError(f"Key must be 32 bytes, got {len(key_bytes)}")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to decode encryption key: {e}")
|
|
||||||
# Generate new key and try again
|
|
||||||
self.key = self._generate_key()
|
|
||||||
key_bytes = base64.b64decode(self.key)
|
|
||||||
|
|
||||||
# Create cipher with CBC mode
|
|
||||||
cipher = AES.new(key_bytes, AES.MODE_CBC)
|
|
||||||
|
|
||||||
# Pad the data to be multiple of 16 bytes (AES block size)
|
|
||||||
padded_data = pad(wallet_str.encode('utf-8'), AES.block_size)
|
|
||||||
|
|
||||||
# Encrypt the data
|
|
||||||
encrypted_bytes = cipher.encrypt(padded_data)
|
|
||||||
|
|
||||||
# Encode IV and encrypted data as base64
|
|
||||||
iv_b64 = base64.b64encode(cipher.iv).decode('utf-8')
|
|
||||||
encrypted_b64 = base64.b64encode(encrypted_bytes).decode('utf-8')
|
|
||||||
|
|
||||||
result = {
|
|
||||||
"iv": iv_b64,
|
|
||||||
"encrypted": encrypted_b64,
|
|
||||||
"algorithm": "AES-256-CBC" # Add algorithm info for debugging
|
|
||||||
}
|
|
||||||
|
|
||||||
logging.info("Wallet ID encrypted successfully")
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Encryption error: {str(e)}")
|
|
||||||
logging.error(f"Wallet ID type: {type(wallet_id)}, Value: {repr(wallet_id)}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def decrypt_wallet_id(self, encrypted_data):
|
|
||||||
"""Decrypt wallet ID with improved error handling"""
|
|
||||||
try:
|
|
||||||
# Validate input
|
|
||||||
if not encrypted_data:
|
|
||||||
logging.error("No encrypted data provided for decryption")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not isinstance(encrypted_data, dict):
|
|
||||||
logging.error(f"Invalid encrypted data format. Expected dict, got {type(encrypted_data)}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if 'iv' not in encrypted_data or 'encrypted' not in encrypted_data:
|
|
||||||
logging.error("Missing 'iv' or 'encrypted' fields in encrypted data")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Decode the base64 key
|
|
||||||
try:
|
|
||||||
key_bytes = base64.b64decode(self.key)
|
|
||||||
if len(key_bytes) != 32:
|
|
||||||
raise ValueError(f"Key must be 32 bytes, got {len(key_bytes)}")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to decode decryption key: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Decode IV and encrypted data
|
|
||||||
try:
|
|
||||||
iv = base64.b64decode(encrypted_data['iv'])
|
|
||||||
encrypted_bytes = base64.b64decode(encrypted_data['encrypted'])
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to decode IV or encrypted data: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Validate IV length (should be 16 bytes for AES)
|
|
||||||
if len(iv) != 16:
|
|
||||||
logging.error(f"Invalid IV length. Expected 16 bytes, got {len(iv)}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Create cipher and decrypt
|
|
||||||
cipher = AES.new(key_bytes, AES.MODE_CBC, iv)
|
|
||||||
|
|
||||||
try:
|
|
||||||
decrypted_padded = cipher.decrypt(encrypted_bytes)
|
|
||||||
# Remove padding
|
|
||||||
decrypted_bytes = unpad(decrypted_padded, AES.block_size)
|
|
||||||
# Convert to string
|
|
||||||
decrypted_str = decrypted_bytes.decode('utf-8')
|
|
||||||
|
|
||||||
logging.info("Wallet ID decrypted successfully")
|
|
||||||
return decrypted_str
|
|
||||||
|
|
||||||
except ValueError as e:
|
|
||||||
logging.error(f"Padding error during decryption: {e}")
|
|
||||||
return None
|
|
||||||
except UnicodeDecodeError as e:
|
|
||||||
logging.error(f"Unicode decode error: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Decryption error: {str(e)}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def generate_certificate_id(self):
|
|
||||||
"""Generate unique certificate ID"""
|
|
||||||
return ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(12))
|
|
||||||
|
|
||||||
def generate_unique_code(self):
|
|
||||||
"""Generate unique share code for certificate"""
|
|
||||||
return ''.join(secrets.choice(string.ascii_lowercase + string.digits) for _ in range(8))
|
|
||||||
|
|
||||||
def test_encryption(self, test_data="test_wallet_0x123456789"):
|
|
||||||
"""Test encryption/decryption functionality"""
|
|
||||||
try:
|
|
||||||
logging.info(f"Testing encryption with data: {test_data}")
|
|
||||||
|
|
||||||
# Test encryption
|
|
||||||
encrypted = self.encrypt_wallet_id(test_data)
|
|
||||||
if not encrypted:
|
|
||||||
logging.error("Encryption test failed")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Test decryption
|
|
||||||
decrypted = self.decrypt_wallet_id(encrypted)
|
|
||||||
if not decrypted:
|
|
||||||
logging.error("Decryption test failed")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Verify data integrity
|
|
||||||
if decrypted != test_data:
|
|
||||||
logging.error(f"Data integrity test failed. Original: {test_data}, Decrypted: {decrypted}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
logging.info("Encryption/decryption test passed successfully")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Encryption test error: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Initialize Certificate Manager
|
|
||||||
cert_manager = CertificateManager()
|
|
||||||
|
|
||||||
# Utility functions
|
# Utility functions
|
||||||
def generate_room_code(length=6):
|
def generate_room_code(length=6):
|
||||||
"""Generate unique room code"""
|
"""Generate unique room code"""
|
||||||
@@ -396,8 +229,16 @@ def register_blueprints():
|
|||||||
print(f"⚠️ Skipping {bp_name} - not available")
|
print(f"⚠️ Skipping {bp_name} - not available")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
module = __import__(f'routes.{bp_name}', fromlist=['bp'])
|
if bp_name == 'certificate':
|
||||||
blueprint_modules[bp_name] = (module.bp, prefix)
|
if CERTIFICATE_BLUEPRINT_AVAILABLE:
|
||||||
|
blueprint_modules[bp_name] = (certificate_bp, prefix)
|
||||||
|
print(f"✅ Certificate blueprint loaded")
|
||||||
|
else:
|
||||||
|
print(f"❌ Skipping certificate blueprint - not available")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
module = __import__(f'routes.{bp_name}', fromlist=['bp'])
|
||||||
|
blueprint_modules[bp_name] = (module.bp, prefix)
|
||||||
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
blueprints_failed.append((prefix, f"Import error: {str(e)}"))
|
blueprints_failed.append((prefix, f"Import error: {str(e)}"))
|
||||||
@@ -433,463 +274,12 @@ def get_db():
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
# ✅ COMPLETELY FIXED CERTIFICATE ENDPOINTS - ALL ISSUES RESOLVED
|
# ✅ REMOVED ALL CERTIFICATE ROUTES - NOW USING BLUEPRINT
|
||||||
|
# Certificate routes have been moved to routes/certificate.py blueprint
|
||||||
|
# This eliminates conflicts and async event loop issues
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
@app.route('/api/certificates', methods=['POST', 'OPTIONS'])
|
# ✅ ADD WALLET AUTHENTICATION ENDPOINT (Keep this in main app)
|
||||||
def create_certificate():
|
|
||||||
"""Create a new certificate after course completion - ALL ISSUES FIXED"""
|
|
||||||
if request.method == "OPTIONS":
|
|
||||||
return jsonify({'status': 'ok'})
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = request.json
|
|
||||||
logger.info(f"📝 Certificate creation request: {data}")
|
|
||||||
|
|
||||||
# Validate required fields
|
|
||||||
required_fields = ['user_name', 'course_id', 'wallet_id', 'user_id']
|
|
||||||
for field in required_fields:
|
|
||||||
if not data.get(field):
|
|
||||||
logger.error(f"❌ Missing required field: {field}")
|
|
||||||
return jsonify({"error": f"Missing required field: {field}"}), 400
|
|
||||||
|
|
||||||
# ✅ CRITICAL FIX: Get the STUDENT's entered name (exactly as they typed it)
|
|
||||||
student_entered_name = data.get('user_name', '').strip()
|
|
||||||
if not student_entered_name:
|
|
||||||
logger.error("❌ Student name cannot be empty")
|
|
||||||
return jsonify({"error": "Student name is required"}), 400
|
|
||||||
|
|
||||||
# ✅ LOG THE ACTUAL STUDENT NAME BEING PROCESSED
|
|
||||||
logger.info(f"🎓 PROCESSING CERTIFICATE FOR STUDENT: '{student_entered_name}'")
|
|
||||||
logger.info(f"🎓 Student name length: {len(student_entered_name)} characters")
|
|
||||||
|
|
||||||
# Validate wallet_id format
|
|
||||||
wallet_id = data.get('wallet_id', '').strip()
|
|
||||||
if not wallet_id:
|
|
||||||
return jsonify({"error": "Wallet ID is required"}), 400
|
|
||||||
|
|
||||||
# Database connection check
|
|
||||||
db = get_db()
|
|
||||||
if db is None:
|
|
||||||
logger.error("❌ Database connection failed")
|
|
||||||
return jsonify({"error": "Database connection failed"}), 500
|
|
||||||
|
|
||||||
# ✅ Check if certificate already exists for this user and course
|
|
||||||
existing_certificate = db.certificates.find_one({
|
|
||||||
"user_id": data['user_id'],
|
|
||||||
"course_id": data['course_id']
|
|
||||||
})
|
|
||||||
|
|
||||||
if existing_certificate is not None:
|
|
||||||
logger.info(f"📜 Certificate already exists for STUDENT: '{student_entered_name}'")
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"certificate": {
|
|
||||||
"certificate_id": existing_certificate['certificate_id'],
|
|
||||||
"user_name": student_entered_name, # ✅ FORCE RETURN STUDENT'S ENTERED NAME
|
|
||||||
"course_title": existing_certificate['course_title'],
|
|
||||||
"mentor_name": existing_certificate.get('mentor_name', '5t4l1n'),
|
|
||||||
"completion_date": existing_certificate['completion_date'],
|
|
||||||
"unique_url": f"/certificate/{existing_certificate.get('share_code', existing_certificate['certificate_id'])}", # ✅ UNIQUE URL
|
|
||||||
"share_code": existing_certificate.get('share_code', existing_certificate['certificate_id']),
|
|
||||||
"message": "Certificate already exists!"
|
|
||||||
}
|
|
||||||
}), 200
|
|
||||||
|
|
||||||
# Check if course exists
|
|
||||||
try:
|
|
||||||
course = db.courses.find_one({"id": data['course_id']})
|
|
||||||
if course is None:
|
|
||||||
return jsonify({"error": "Course not found"}), 404
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"❌ Error finding course: {e}")
|
|
||||||
return jsonify({"error": "Failed to verify course"}), 500
|
|
||||||
|
|
||||||
# Test encryption before proceeding
|
|
||||||
if not cert_manager.test_encryption():
|
|
||||||
return jsonify({"error": "Certificate system is not working properly"}), 500
|
|
||||||
|
|
||||||
# Generate certificate ID and unique codes
|
|
||||||
certificate_id = cert_manager.generate_certificate_id()
|
|
||||||
token_id = str(uuid.uuid4())
|
|
||||||
share_code = cert_manager.generate_unique_code() # ✅ UNIQUE SHARE CODE
|
|
||||||
|
|
||||||
logger.info(f"🆔 Generated certificate ID: {certificate_id}")
|
|
||||||
logger.info(f"🔗 Generated share code: {share_code}")
|
|
||||||
|
|
||||||
# Encrypt wallet ID
|
|
||||||
encrypted_wallet = cert_manager.encrypt_wallet_id(wallet_id)
|
|
||||||
if encrypted_wallet is None:
|
|
||||||
return jsonify({"error": "Failed to encrypt wallet ID"}), 500
|
|
||||||
|
|
||||||
# ✅ CRITICAL FIX: Extract INSTRUCTOR name from course (separate from student)
|
|
||||||
instructor_name = course.get('mentor', '5t4l1n')
|
|
||||||
if isinstance(instructor_name, dict):
|
|
||||||
instructor_name = instructor_name.get('name', '5t4l1n')
|
|
||||||
|
|
||||||
# ✅ PREVENT STUDENT NAME FROM BEING USED AS INSTRUCTOR NAME
|
|
||||||
if instructor_name == student_entered_name or instructor_name == student_entered_name.lower():
|
|
||||||
instructor_name = '5t4l1n' # Force default instructor name
|
|
||||||
|
|
||||||
logger.info(f"🎓 FINAL VERIFICATION - STUDENT: '{student_entered_name}' | INSTRUCTOR: '{instructor_name}'")
|
|
||||||
|
|
||||||
# ✅ Create certificate document with EXPLICIT field separation and GUARANTEED STORAGE
|
|
||||||
certificate = {
|
|
||||||
"certificate_id": certificate_id,
|
|
||||||
"token_id": token_id,
|
|
||||||
"share_code": share_code, # ✅ UNIQUE SHARE CODE FOR URL
|
|
||||||
"student_name": student_entered_name, # ✅ EXPLICIT STUDENT FIELD
|
|
||||||
"user_name": student_entered_name, # ✅ STUDENT'S ENTERED NAME (main field)
|
|
||||||
"user_id": data['user_id'],
|
|
||||||
"course_id": data['course_id'],
|
|
||||||
"course_title": course['title'],
|
|
||||||
"mentor_name": instructor_name, # ✅ INSTRUCTOR NAME
|
|
||||||
"instructor_name": instructor_name, # ✅ EXPLICIT INSTRUCTOR FIELD
|
|
||||||
"encrypted_wallet_id": encrypted_wallet,
|
|
||||||
"completion_date": datetime.now().isoformat(),
|
|
||||||
"created_at": datetime.now().isoformat(),
|
|
||||||
"updated_at": datetime.now().isoformat(),
|
|
||||||
"status": "active",
|
|
||||||
"issued_by": "OpenLearnX",
|
|
||||||
"verification_url": f"/certificates/{certificate_id}",
|
|
||||||
"share_url": f"/certificate/{share_code}", # ✅ UNIQUE SHARE URL
|
|
||||||
"public_url": f"{request.host_url}certificate/{share_code}", # ✅ FULL PUBLIC URL
|
|
||||||
"blockchain_hash": None,
|
|
||||||
"is_revoked": False,
|
|
||||||
"view_count": 0, # ✅ TRACK VIEWS
|
|
||||||
"shared_count": 0 # ✅ TRACK SHARES
|
|
||||||
}
|
|
||||||
|
|
||||||
# ✅ LOG THE CERTIFICATE DOCUMENT BEFORE SAVING
|
|
||||||
logger.info(f"📄 Certificate document to be saved:")
|
|
||||||
logger.info(f" 🎓 student_name: '{certificate['student_name']}'")
|
|
||||||
logger.info(f" 🎓 user_name: '{certificate['user_name']}'")
|
|
||||||
logger.info(f" 👨🏫 instructor_name: '{certificate['instructor_name']}'")
|
|
||||||
logger.info(f" 🔗 share_code: '{certificate['share_code']}'")
|
|
||||||
|
|
||||||
# ✅ GUARANTEED DATABASE SAVE with enhanced retry mechanism
|
|
||||||
max_retries = 5
|
|
||||||
saved_successfully = False
|
|
||||||
|
|
||||||
for attempt in range(max_retries):
|
|
||||||
try:
|
|
||||||
# Create unique indexes to prevent duplicates
|
|
||||||
db.certificates.create_index([("certificate_id", 1)], unique=True, background=True)
|
|
||||||
db.certificates.create_index([("share_code", 1)], unique=True, background=True)
|
|
||||||
db.certificates.create_index([("user_id", 1), ("course_id", 1)], background=True)
|
|
||||||
|
|
||||||
result = db.certificates.insert_one(certificate)
|
|
||||||
logger.info(f"✅ Certificate saved successfully for STUDENT: '{student_entered_name}' with MongoDB ID: {result.inserted_id}")
|
|
||||||
saved_successfully = True
|
|
||||||
break
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
if "E11000" in str(e) and "duplicate key" in str(e):
|
|
||||||
if attempt < max_retries - 1:
|
|
||||||
# Generate new unique IDs and try again
|
|
||||||
certificate_id = cert_manager.generate_certificate_id()
|
|
||||||
token_id = str(uuid.uuid4())
|
|
||||||
share_code = cert_manager.generate_unique_code()
|
|
||||||
|
|
||||||
certificate["certificate_id"] = certificate_id
|
|
||||||
certificate["token_id"] = token_id
|
|
||||||
certificate["share_code"] = share_code
|
|
||||||
certificate["verification_url"] = f"/certificates/{certificate_id}"
|
|
||||||
certificate["share_url"] = f"/certificate/{share_code}"
|
|
||||||
certificate["public_url"] = f"{request.host_url}certificate/{share_code}"
|
|
||||||
|
|
||||||
logger.warning(f"⚠️ Duplicate key error, retrying with new IDs (attempt {attempt + 2})")
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
logger.error(f"❌ Failed to save certificate after {max_retries} attempts: {e}")
|
|
||||||
return jsonify({"error": "Failed to save certificate due to ID conflict"}), 500
|
|
||||||
else:
|
|
||||||
logger.error(f"❌ Database save error (attempt {attempt + 1}): {e}")
|
|
||||||
if attempt == max_retries - 1:
|
|
||||||
return jsonify({"error": "Failed to save certificate to database"}), 500
|
|
||||||
time.sleep(0.5) # Wait before retry
|
|
||||||
|
|
||||||
if not saved_successfully:
|
|
||||||
logger.error(f"❌ Failed to save certificate after all attempts")
|
|
||||||
return jsonify({"error": "Failed to save certificate"}), 500
|
|
||||||
|
|
||||||
# ✅ CRITICAL FIX: Return response with GUARANTEED STUDENT NAME and UNIQUE URLS
|
|
||||||
certificate_response = {
|
|
||||||
"certificate_id": certificate_id,
|
|
||||||
"token_id": token_id,
|
|
||||||
"share_code": share_code,
|
|
||||||
"user_name": student_entered_name, # ✅ STUDENT'S ENTERED NAME (GUARANTEED)
|
|
||||||
"student_name": student_entered_name, # ✅ EXPLICIT STUDENT NAME
|
|
||||||
"course_title": course['title'],
|
|
||||||
"mentor_name": instructor_name, # ✅ INSTRUCTOR NAME
|
|
||||||
"instructor_name": instructor_name, # ✅ EXPLICIT INSTRUCTOR NAME
|
|
||||||
"completion_date": certificate['completion_date'],
|
|
||||||
"verification_url": certificate['verification_url'],
|
|
||||||
"share_url": certificate['share_url'], # ✅ UNIQUE SHARE URL
|
|
||||||
"public_url": certificate['public_url'], # ✅ FULL PUBLIC URL
|
|
||||||
"unique_url": f"/certificate/{share_code}", # ✅ UNIQUE CERTIFICATE PATH
|
|
||||||
"message": f"Certificate generated successfully for {student_entered_name}!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ✅ FINAL VERIFICATION LOG
|
|
||||||
logger.info(f"📤 RETURNING CERTIFICATE RESPONSE:")
|
|
||||||
logger.info(f" 🎓 user_name: '{certificate_response['user_name']}'")
|
|
||||||
logger.info(f" 🎓 student_name: '{certificate_response['student_name']}'")
|
|
||||||
logger.info(f" 👨🏫 mentor_name: '{certificate_response['mentor_name']}'")
|
|
||||||
logger.info(f" 🔗 unique_url: '{certificate_response['unique_url']}'")
|
|
||||||
logger.info(f" 🌐 public_url: '{certificate_response['public_url']}'")
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"certificate": certificate_response
|
|
||||||
}), 201
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"❌ Unexpected error creating certificate: {str(e)}")
|
|
||||||
import traceback
|
|
||||||
logger.error(f"❌ Traceback: {traceback.format_exc()}")
|
|
||||||
return jsonify({"error": "Failed to create certificate"}), 500
|
|
||||||
|
|
||||||
# ✅ UNIQUE CERTIFICATE VIEW ENDPOINT
|
|
||||||
@app.route('/certificate/<share_code>', methods=['GET', 'OPTIONS'])
|
|
||||||
@app.route('/api/certificate/<share_code>', methods=['GET', 'OPTIONS'])
|
|
||||||
def view_certificate_by_code(share_code):
|
|
||||||
"""View certificate by unique share code"""
|
|
||||||
if request.method == "OPTIONS":
|
|
||||||
return jsonify({'status': 'ok'})
|
|
||||||
|
|
||||||
try:
|
|
||||||
db = get_db()
|
|
||||||
if db is None:
|
|
||||||
return jsonify({"error": "Database connection failed"}), 500
|
|
||||||
|
|
||||||
# Find certificate by share code
|
|
||||||
certificate = db.certificates.find_one({"share_code": share_code})
|
|
||||||
|
|
||||||
if certificate is None:
|
|
||||||
return jsonify({"error": "Certificate not found"}), 404
|
|
||||||
|
|
||||||
# Check if certificate is revoked
|
|
||||||
if certificate.get('is_revoked', False):
|
|
||||||
return jsonify({"error": "Certificate has been revoked"}), 410
|
|
||||||
|
|
||||||
# ✅ INCREMENT VIEW COUNT
|
|
||||||
db.certificates.update_one(
|
|
||||||
{"share_code": share_code},
|
|
||||||
{"$inc": {"view_count": 1}}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Decrypt wallet ID for display
|
|
||||||
decrypted_wallet = None
|
|
||||||
if certificate.get('encrypted_wallet_id') is not None:
|
|
||||||
decrypted_wallet = cert_manager.decrypt_wallet_id(certificate['encrypted_wallet_id'])
|
|
||||||
|
|
||||||
# ✅ PREPARE RESPONSE WITH GUARANTEED STUDENT NAME
|
|
||||||
certificate_response = {
|
|
||||||
"certificate_id": certificate['certificate_id'],
|
|
||||||
"share_code": certificate['share_code'],
|
|
||||||
"user_name": certificate.get('student_name', certificate.get('user_name', 'Student')), # ✅ STUDENT NAME
|
|
||||||
"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', '5t4l1n')), # ✅ INSTRUCTOR NAME
|
|
||||||
"instructor_name": certificate.get('instructor_name', certificate.get('mentor_name', '5t4l1n')),
|
|
||||||
"completion_date": certificate['completion_date'],
|
|
||||||
"status": certificate['status'],
|
|
||||||
"wallet_id": decrypted_wallet,
|
|
||||||
"issued_by": certificate.get('issued_by', 'OpenLearnX'),
|
|
||||||
"verification_url": certificate.get('verification_url'),
|
|
||||||
"share_url": certificate.get('share_url'),
|
|
||||||
"public_url": certificate.get('public_url'),
|
|
||||||
"view_count": certificate.get('view_count', 0),
|
|
||||||
"is_verified": True,
|
|
||||||
"is_revoked": certificate.get('is_revoked', False)
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"certificate": certificate_response
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching certificate by code: {str(e)}")
|
|
||||||
return jsonify({"error": "Failed to fetch certificate"}), 500
|
|
||||||
|
|
||||||
@app.route('/api/certificates/<certificate_id>', methods=['GET', 'OPTIONS'])
|
|
||||||
def get_certificate(certificate_id):
|
|
||||||
"""Get certificate by ID"""
|
|
||||||
if request.method == "OPTIONS":
|
|
||||||
return jsonify({'status': 'ok'})
|
|
||||||
|
|
||||||
try:
|
|
||||||
db = get_db()
|
|
||||||
if db is None:
|
|
||||||
return jsonify({"error": "Database connection failed"}), 500
|
|
||||||
|
|
||||||
try:
|
|
||||||
certificate = db.certificates.find_one({"certificate_id": certificate_id})
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error finding certificate: {e}")
|
|
||||||
return jsonify({"error": "Database query failed"}), 500
|
|
||||||
|
|
||||||
if certificate is None:
|
|
||||||
return jsonify({"error": "Certificate not found"}), 404
|
|
||||||
|
|
||||||
# Check if certificate is revoked
|
|
||||||
if certificate.get('is_revoked', False):
|
|
||||||
return jsonify({"error": "Certificate has been revoked"}), 410
|
|
||||||
|
|
||||||
# Decrypt wallet ID for display
|
|
||||||
decrypted_wallet = None
|
|
||||||
if certificate.get('encrypted_wallet_id') is not None:
|
|
||||||
decrypted_wallet = cert_manager.decrypt_wallet_id(certificate['encrypted_wallet_id'])
|
|
||||||
|
|
||||||
# ✅ PREPARE RESPONSE WITH GUARANTEED STUDENT NAME
|
|
||||||
certificate_response = {
|
|
||||||
"certificate_id": certificate['certificate_id'],
|
|
||||||
"token_id": certificate.get('token_id'),
|
|
||||||
"share_code": certificate.get('share_code'),
|
|
||||||
"user_name": certificate.get('student_name', certificate.get('user_name', 'Student')), # ✅ STUDENT NAME
|
|
||||||
"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', '5t4l1n')), # ✅ INSTRUCTOR NAME
|
|
||||||
"instructor_name": certificate.get('instructor_name', certificate.get('mentor_name', '5t4l1n')),
|
|
||||||
"completion_date": certificate['completion_date'],
|
|
||||||
"status": certificate['status'],
|
|
||||||
"wallet_id": decrypted_wallet,
|
|
||||||
"issued_by": certificate.get('issued_by', 'OpenLearnX'),
|
|
||||||
"verification_url": certificate.get('verification_url'),
|
|
||||||
"share_url": certificate.get('share_url'),
|
|
||||||
"public_url": certificate.get('public_url'),
|
|
||||||
"unique_url": f"/certificate/{certificate.get('share_code', certificate_id)}",
|
|
||||||
"view_count": certificate.get('view_count', 0),
|
|
||||||
"blockchain_hash": certificate.get('blockchain_hash'),
|
|
||||||
"is_verified": True,
|
|
||||||
"is_revoked": certificate.get('is_revoked', False)
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"certificate": certificate_response
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching certificate: {str(e)}")
|
|
||||||
return jsonify({"error": "Failed to fetch certificate"}), 500
|
|
||||||
|
|
||||||
# ✅ SHARE TRACKING ENDPOINT
|
|
||||||
@app.route('/api/certificates/<certificate_id>/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()
|
|
||||||
if db is None:
|
|
||||||
return jsonify({"error": "Database connection failed"}), 500
|
|
||||||
|
|
||||||
# Increment share count
|
|
||||||
result = db.certificates.update_one(
|
|
||||||
{"certificate_id": 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:
|
|
||||||
logger.error(f"Error tracking share: {str(e)}")
|
|
||||||
return jsonify({"error": "Failed to track share"}), 500
|
|
||||||
|
|
||||||
@app.route('/api/certificates/user/<user_id>', methods=['GET', 'OPTIONS'])
|
|
||||||
def get_user_certificates(user_id):
|
|
||||||
"""Get all certificates for a user"""
|
|
||||||
if request.method == "OPTIONS":
|
|
||||||
return jsonify({'status': 'ok'})
|
|
||||||
|
|
||||||
try:
|
|
||||||
db = get_db()
|
|
||||||
if db is None:
|
|
||||||
return jsonify({"error": "Database connection failed"}), 500
|
|
||||||
|
|
||||||
try:
|
|
||||||
certificates = list(db.certificates.find(
|
|
||||||
{"user_id": user_id},
|
|
||||||
{"_id": 0, "encrypted_wallet_id": 0}
|
|
||||||
))
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error finding user certificates: {e}")
|
|
||||||
return jsonify({"error": "Database query failed"}), 500
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"certificates": certificates,
|
|
||||||
"count": len(certificates)
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching user certificates: {str(e)}")
|
|
||||||
return jsonify({"error": "Failed to fetch certificates"}), 500
|
|
||||||
|
|
||||||
@app.route('/api/admin/certificates', methods=['GET', 'OPTIONS'])
|
|
||||||
def get_all_certificates():
|
|
||||||
"""Admin endpoint to get all certificates"""
|
|
||||||
if request.method == "OPTIONS":
|
|
||||||
return jsonify({'status': 'ok'})
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Check admin authentication
|
|
||||||
auth_header = request.headers.get('Authorization')
|
|
||||||
if auth_header is None or not auth_header.startswith('Bearer '):
|
|
||||||
return jsonify({"error": "Unauthorized"}), 401
|
|
||||||
|
|
||||||
token = auth_header.split(' ')[1]
|
|
||||||
expected_token = os.getenv('ADMIN_TOKEN', 'admin-secret-key')
|
|
||||||
|
|
||||||
if token != expected_token:
|
|
||||||
return jsonify({"error": "Invalid admin token"}), 401
|
|
||||||
|
|
||||||
db = get_db()
|
|
||||||
if db is None:
|
|
||||||
return jsonify({"error": "Database connection failed"}), 500
|
|
||||||
|
|
||||||
# Add pagination
|
|
||||||
page = int(request.args.get('page', 1))
|
|
||||||
limit = int(request.args.get('limit', 10))
|
|
||||||
skip = (page - 1) * limit
|
|
||||||
|
|
||||||
try:
|
|
||||||
certificates = list(db.certificates.find(
|
|
||||||
{},
|
|
||||||
{"_id": 0, "encrypted_wallet_id": 0}
|
|
||||||
).skip(skip).limit(limit).sort("created_at", -1))
|
|
||||||
|
|
||||||
total = db.certificates.count_documents({})
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching certificates: {e}")
|
|
||||||
return jsonify({"error": "Database query failed"}), 500
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"certificates": certificates,
|
|
||||||
"pagination": {
|
|
||||||
"page": page,
|
|
||||||
"limit": limit,
|
|
||||||
"total": total,
|
|
||||||
"pages": (total + limit - 1) // limit
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching certificates: {str(e)}")
|
|
||||||
return jsonify({"error": "Failed to fetch certificates"}), 500
|
|
||||||
|
|
||||||
# ✅ ADD WALLET AUTHENTICATION ENDPOINT
|
|
||||||
@app.route('/api/auth/wallet-login', methods=['POST', 'OPTIONS'])
|
@app.route('/api/auth/wallet-login', methods=['POST', 'OPTIONS'])
|
||||||
def wallet_login():
|
def wallet_login():
|
||||||
"""Authenticate user with wallet signature"""
|
"""Authenticate user with wallet signature"""
|
||||||
@@ -966,37 +356,6 @@ def verify_wallet_signature(address, signature, timestamp):
|
|||||||
logger.error(f"Signature verification failed: {e}")
|
logger.error(f"Signature verification failed: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Test encryption endpoint
|
|
||||||
@app.route('/api/test-encryption', methods=['GET'])
|
|
||||||
def test_encryption_endpoint():
|
|
||||||
"""Test encryption system"""
|
|
||||||
try:
|
|
||||||
test_wallet = "0x742d35Cc6634C0532925a3b8D4034DfF77cf3C4"
|
|
||||||
|
|
||||||
# Test encryption
|
|
||||||
encrypted = cert_manager.encrypt_wallet_id(test_wallet)
|
|
||||||
if not encrypted:
|
|
||||||
return jsonify({"error": "Encryption failed"}), 500
|
|
||||||
|
|
||||||
# Test decryption
|
|
||||||
decrypted = cert_manager.decrypt_wallet_id(encrypted)
|
|
||||||
if not decrypted:
|
|
||||||
return jsonify({"error": "Decryption failed"}), 500
|
|
||||||
|
|
||||||
success = decrypted == test_wallet
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"success": success,
|
|
||||||
"original": test_wallet,
|
|
||||||
"decrypted": decrypted,
|
|
||||||
"encrypted_data": encrypted,
|
|
||||||
"message": "Encryption test completed"
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Encryption test error: {e}")
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
# ✅ HEALTH ENDPOINTS
|
# ✅ HEALTH ENDPOINTS
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
@@ -1005,7 +364,7 @@ def test_encryption_endpoint():
|
|||||||
def health_root():
|
def health_root():
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"status": "OpenLearnX Professional Dashboard API",
|
"status": "OpenLearnX Professional Dashboard API",
|
||||||
"version": "4.0.0 - ALL CERTIFICATE ISSUES FIXED",
|
"version": "5.0.0 - BLUEPRINT-BASED CERTIFICATE SYSTEM",
|
||||||
"timestamp": datetime.now().isoformat(),
|
"timestamp": datetime.now().isoformat(),
|
||||||
"features": {
|
"features": {
|
||||||
"mongodb": service_status.get('mongodb', False),
|
"mongodb": service_status.get('mongodb', False),
|
||||||
@@ -1014,15 +373,16 @@ def health_root():
|
|||||||
"compiler": services_status['compiler'],
|
"compiler": services_status['compiler'],
|
||||||
"ai_quiz_service": services_status['ai_quiz'],
|
"ai_quiz_service": services_status['ai_quiz'],
|
||||||
"comprehensive_dashboard": DASHBOARD_AVAILABLE,
|
"comprehensive_dashboard": DASHBOARD_AVAILABLE,
|
||||||
"certificate_system": True, # ✅ Fixed feature
|
"certificate_system": CERTIFICATE_BLUEPRINT_AVAILABLE, # ✅ Blueprint-based
|
||||||
"unique_certificate_urls": True, # ✅ New feature
|
"unique_certificate_urls": CERTIFICATE_BLUEPRINT_AVAILABLE,
|
||||||
"certificate_sharing": True, # ✅ New feature
|
"certificate_sharing": CERTIFICATE_BLUEPRINT_AVAILABLE,
|
||||||
"aes256_encryption": True # ✅ New feature
|
"aes256_encryption": CERTIFICATE_BLUEPRINT_AVAILABLE
|
||||||
},
|
},
|
||||||
"endpoints": {
|
"endpoints": {
|
||||||
"comprehensive_stats": "/api/dashboard/comprehensive-stats",
|
"comprehensive_stats": "/api/dashboard/comprehensive-stats",
|
||||||
"certificates": "/api/certificates", # ✅ Fixed endpoint
|
"certificates": "/api/certificate/mint", # ✅ Blueprint endpoint
|
||||||
"unique_certificates": "/certificate/<share_code>", # ✅ New endpoint
|
"certificate_test": "/api/certificate/test-db", # ✅ Blueprint endpoint
|
||||||
|
"unique_certificates": "/certificate/<share_code>",
|
||||||
"health": "/api/health"
|
"health": "/api/health"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1043,7 +403,7 @@ def api_health():
|
|||||||
"user_quizzes": db.user_quizzes.count_documents({}),
|
"user_quizzes": db.user_quizzes.count_documents({}),
|
||||||
"user_submissions": db.user_submissions.count_documents({}),
|
"user_submissions": db.user_submissions.count_documents({}),
|
||||||
"user_achievements": db.user_achievements.count_documents({}),
|
"user_achievements": db.user_achievements.count_documents({}),
|
||||||
"certificates": db.certificates.count_documents({}) # ✅ Added certificates collection
|
"certificates": db.certificates.count_documents({})
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db_status = f"error: {str(e)}"
|
db_status = f"error: {str(e)}"
|
||||||
@@ -1060,15 +420,15 @@ def api_health():
|
|||||||
"compiler": services_status['compiler'],
|
"compiler": services_status['compiler'],
|
||||||
"ai_quiz_service": services_status['ai_quiz'],
|
"ai_quiz_service": services_status['ai_quiz'],
|
||||||
"comprehensive_dashboard": DASHBOARD_AVAILABLE,
|
"comprehensive_dashboard": DASHBOARD_AVAILABLE,
|
||||||
"certificate_system": True, # ✅ Fixed service
|
"certificate_system": CERTIFICATE_BLUEPRINT_AVAILABLE, # ✅ Blueprint status
|
||||||
"unique_urls": True, # ✅ New service
|
"unique_urls": CERTIFICATE_BLUEPRINT_AVAILABLE,
|
||||||
"share_tracking": True, # ✅ New service
|
"share_tracking": CERTIFICATE_BLUEPRINT_AVAILABLE,
|
||||||
"aes256_encryption": True # ✅ New service
|
"aes256_encryption": CERTIFICATE_BLUEPRINT_AVAILABLE
|
||||||
},
|
},
|
||||||
"collections": collections_count,
|
"collections": collections_count,
|
||||||
"blueprints_registered": blueprints_registered,
|
"blueprints_registered": blueprints_registered,
|
||||||
"blueprints_failed": blueprints_failed,
|
"blueprints_failed": blueprints_failed,
|
||||||
"version": "4.0.0-all-certificate-issues-fixed"
|
"version": "5.0.0-blueprint-based-certificates"
|
||||||
}), 200 if status == "healthy" else 503
|
}), 200 if status == "healthy" else 503
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
@@ -1083,12 +443,12 @@ def not_found(e):
|
|||||||
"method": request.method,
|
"method": request.method,
|
||||||
"available_endpoints": [
|
"available_endpoints": [
|
||||||
"/api/dashboard/comprehensive-stats",
|
"/api/dashboard/comprehensive-stats",
|
||||||
"/api/certificates", # ✅ Fixed endpoint
|
"/api/certificate/mint", # ✅ Blueprint endpoint
|
||||||
"/api/certificates/<id>", # ✅ Fixed endpoint
|
"/api/certificate/test-db", # ✅ Blueprint endpoint
|
||||||
"/certificate/<share_code>", # ✅ New unique URL endpoint
|
"/api/certificate/<id>", # ✅ Blueprint endpoint
|
||||||
"/api/admin/certificates", # ✅ Fixed endpoint
|
"/api/certificate/verify/<share_code>", # ✅ Blueprint endpoint
|
||||||
"/api/auth/wallet-login", # ✅ New endpoint
|
"/api/certificate/list-all", # ✅ Blueprint endpoint
|
||||||
"/api/test-encryption", # ✅ New endpoint
|
"/api/auth/wallet-login",
|
||||||
"/api/health"
|
"/api/health"
|
||||||
],
|
],
|
||||||
"suggestion": "Check the API documentation for valid endpoints"
|
"suggestion": "Check the API documentation for valid endpoints"
|
||||||
@@ -1109,30 +469,20 @@ def internal_error(e):
|
|||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("🚀 Starting OpenLearnX Professional Dashboard Backend v4.0.0")
|
print("🚀 Starting OpenLearnX Professional Dashboard Backend v5.0.0")
|
||||||
print("📊 Features: Comprehensive Analytics, Real-time Data, Professional Dashboard, Fixed Certificate System")
|
print("📊 Features: Comprehensive Analytics, Real-time Data, Blueprint-based Certificate System")
|
||||||
print(f"🔗 MongoDB URI: {app.config['MONGODB_URI']}")
|
print(f"🔗 MongoDB URI: {app.config['MONGODB_URI']}")
|
||||||
print(f"🌐 Web3 Provider: {app.config['WEB3_PROVIDER_URL']}")
|
print(f"🌐 Web3 Provider: {app.config['WEB3_PROVIDER_URL']}")
|
||||||
print(f"📄 Contract Address: {app.config['CONTRACT_ADDRESS']}")
|
print(f"📄 Contract Address: {app.config['CONTRACT_ADDRESS']}")
|
||||||
print(f"🔐 JWT Expiration: {os.getenv('JWT_EXPIRATION_HOURS', 168)} hours")
|
print(f"🔐 JWT Expiration: {os.getenv('JWT_EXPIRATION_HOURS', 168)} hours")
|
||||||
print(f"📊 Dashboard Cache: {app.config['DASHBOARD_CACHE_TIMEOUT']} seconds")
|
print(f"📊 Dashboard Cache: {app.config['DASHBOARD_CACHE_TIMEOUT']} seconds")
|
||||||
print(f"🏆 Certificate System: ✅ AES-256 Encryption {'Configured' if app.config.get('AES_ENCRYPTION_KEY') else 'Using Default Key'}")
|
|
||||||
|
|
||||||
# Test encryption system on startup
|
|
||||||
print("\n🔐 Testing certificate encryption system...")
|
|
||||||
if cert_manager.test_encryption():
|
|
||||||
print("✅ Certificate encryption system working properly")
|
|
||||||
else:
|
|
||||||
print("❌ Certificate encryption system has issues - check logs")
|
|
||||||
|
|
||||||
print(f"\n📋 Service Status:")
|
print(f"\n📋 Service Status:")
|
||||||
print(f" - MongoDB: {'✅ Connected' if service_status.get('mongodb') else '❌ Failed'}")
|
print(f" - MongoDB: {'✅ Connected' if service_status.get('mongodb') else '❌ Failed'}")
|
||||||
print(f" - Web3/Anvil: {'✅ Connected' if service_status.get('web3') else '❌ Failed'}")
|
print(f" - Web3/Anvil: {'✅ Connected' if service_status.get('web3') else '❌ Failed'}")
|
||||||
print(f" - Comprehensive Dashboard: {'✅ Available' if DASHBOARD_AVAILABLE else '❌ Unavailable'}")
|
print(f" - Comprehensive Dashboard: {'✅ Available' if DASHBOARD_AVAILABLE else '❌ Unavailable'}")
|
||||||
print(f" - AI Quiz Service: {'✅ Available' if services_status['ai_quiz'] else '❌ Unavailable'}")
|
print(f" - AI Quiz Service: {'✅ Available' if services_status['ai_quiz'] else '❌ Unavailable'}")
|
||||||
print(f" - Certificate System: ✅ Available - ALL ISSUES FIXED")
|
print(f" - Certificate System: {'✅ Blueprint Available' if CERTIFICATE_BLUEPRINT_AVAILABLE else '❌ Blueprint Missing'}")
|
||||||
print(f" - Unique Certificate URLs: ✅ Available")
|
|
||||||
print(f" - Share Tracking: ✅ Available")
|
|
||||||
print(f" - JWT Authentication: ✅ Configured")
|
print(f" - JWT Authentication: ✅ Configured")
|
||||||
print(f" - Enhanced Security: ✅ Timeout Protection")
|
print(f" - Enhanced Security: ✅ Timeout Protection")
|
||||||
print(f" - Blueprints: {len(blueprints_registered)} registered")
|
print(f" - Blueprints: {len(blueprints_registered)} registered")
|
||||||
@@ -1146,28 +496,31 @@ if __name__ == "__main__":
|
|||||||
print(f" - GET /api/dashboard/comprehensive-stats")
|
print(f" - GET /api/dashboard/comprehensive-stats")
|
||||||
print(f" - GET /api/health")
|
print(f" - GET /api/health")
|
||||||
|
|
||||||
print(f"\n🏆 Certificate System Endpoints (ALL ISSUES FIXED):")
|
if CERTIFICATE_BLUEPRINT_AVAILABLE:
|
||||||
print(f" - POST /api/certificates")
|
print(f"\n🏆 Certificate System Endpoints (Blueprint-based):")
|
||||||
print(f" - GET /api/certificates/<certificate_id>")
|
print(f" - GET /api/certificate/test-db")
|
||||||
print(f" - GET /certificate/<share_code>") # ✅ Unique URLs
|
print(f" - POST /api/certificate/mint")
|
||||||
print(f" - GET /api/certificate/<share_code>") # ✅ API version
|
print(f" - GET /api/certificate/test-generation")
|
||||||
print(f" - POST /api/certificates/<certificate_id>/share") # ✅ Share tracking
|
print(f" - GET /api/certificate/<certificate_id>")
|
||||||
print(f" - GET /api/certificates/user/<user_id>")
|
print(f" - GET /api/certificate/verify/<share_code>")
|
||||||
print(f" - GET /api/admin/certificates")
|
print(f" - GET /api/certificate/list-all")
|
||||||
|
else:
|
||||||
|
print(f"\n❌ Certificate System: Blueprint not available")
|
||||||
|
print(f" - Create routes/certificate.py with the blueprint code")
|
||||||
|
|
||||||
print(f"\n🔐 Authentication Endpoints:")
|
print(f"\n🔐 Authentication Endpoints:")
|
||||||
print(f" - POST /api/auth/wallet-login")
|
print(f" - POST /api/auth/wallet-login")
|
||||||
|
|
||||||
print(f"\n🧪 Testing Endpoints:")
|
print(f"\n🎓 BLUEPRINT-BASED CERTIFICATE SYSTEM:")
|
||||||
print(f" - GET /api/test-encryption")
|
print(f" ✅ Isolated MongoDB connections (no async conflicts)")
|
||||||
|
print(f" ✅ Unique certificate ID generation")
|
||||||
|
print(f" ✅ Proper student name display")
|
||||||
|
print(f" ✅ Guaranteed database saving")
|
||||||
|
print(f" ✅ Enhanced error handling")
|
||||||
|
print(f" ✅ No event loop conflicts")
|
||||||
|
|
||||||
print(f"\n🎓 ALL CERTIFICATE ISSUES FIXED:")
|
# Set MongoDB URI as environment variable
|
||||||
print(f" ✅ Student name displays correctly (entered name shows prominently)")
|
os.environ['MONGODB_URI'] = app.config['MONGODB_URI']
|
||||||
print(f" ✅ Database storage works properly (guaranteed save with retry mechanism)")
|
|
||||||
print(f" ✅ Unique certificate URLs (/certificate/<unique_code>)")
|
|
||||||
print(f" ✅ Share tracking (view counts and share counts)")
|
|
||||||
print(f" ✅ Mentor name shows only at bottom as instructor signature")
|
|
||||||
print(f" ✅ Enhanced error handling and logging")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app.run(
|
app.run(
|
||||||
|
|||||||
+357
-19
@@ -1,49 +1,387 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import secrets
|
import secrets
|
||||||
import string
|
import string
|
||||||
from cryptography.fernet import Fernet
|
|
||||||
import os
|
import os
|
||||||
import base64
|
import base64
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
import threading
|
||||||
|
import hashlib
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
from Crypto.Random import get_random_bytes
|
from Crypto.Random import get_random_bytes
|
||||||
from Crypto.Util.Padding import pad, unpad
|
from Crypto.Util.Padding import pad, unpad
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
class CertificateManager:
|
class CertificateManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# AES-256 key (store this securely in environment variables)
|
# AES-256 key (store this securely in environment variables)
|
||||||
self.key = os.getenv('AES_ENCRYPTION_KEY', self._generate_key())
|
self.key = os.getenv('AES_ENCRYPTION_KEY', self._generate_key())
|
||||||
|
|
||||||
|
# Validate the key
|
||||||
|
try:
|
||||||
|
decoded_key = base64.b64decode(self.key)
|
||||||
|
if len(decoded_key) != 32: # AES-256 requires 32-byte key
|
||||||
|
self.key = self._generate_key()
|
||||||
|
print("⚠️ Invalid AES key detected, generated new one")
|
||||||
|
except Exception as e:
|
||||||
|
self.key = self._generate_key()
|
||||||
|
print(f"⚠️ Key validation failed: {e}, generated new one")
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Keep track of generated IDs to prevent immediate duplicates
|
||||||
|
self._recent_ids = set()
|
||||||
|
self._max_recent_ids = 1000
|
||||||
|
|
||||||
def _generate_key(self):
|
def _generate_key(self):
|
||||||
"""Generate a new AES-256 key"""
|
"""Generate a new AES-256 key"""
|
||||||
return base64.b64encode(get_random_bytes(32)).decode('utf-8')
|
key_bytes = get_random_bytes(32) # 32 bytes = 256 bits
|
||||||
|
return base64.b64encode(key_bytes).decode('utf-8')
|
||||||
|
|
||||||
def encrypt_wallet_id(self, wallet_id):
|
def encrypt_wallet_id(self, wallet_id):
|
||||||
"""Encrypt wallet ID using AES-256"""
|
"""Encrypt wallet ID using AES-256 with enhanced error handling"""
|
||||||
try:
|
try:
|
||||||
key = base64.b64decode(self.key)
|
if not wallet_id:
|
||||||
cipher = AES.new(key, AES.MODE_CBC)
|
return None
|
||||||
ct_bytes = cipher.encrypt(pad(wallet_id.encode('utf-8'), AES.block_size))
|
|
||||||
iv = base64.b64encode(cipher.iv).decode('utf-8')
|
# Ensure wallet_id is a string
|
||||||
ct = base64.b64encode(ct_bytes).decode('utf-8')
|
wallet_str = str(wallet_id).strip()
|
||||||
return {"iv": iv, "encrypted": ct}
|
if not wallet_str:
|
||||||
|
return None
|
||||||
|
|
||||||
|
key_bytes = base64.b64decode(self.key)
|
||||||
|
cipher = AES.new(key_bytes, AES.MODE_CBC)
|
||||||
|
|
||||||
|
# Pad the data to be multiple of 16 bytes
|
||||||
|
padded_data = pad(wallet_str.encode('utf-8'), AES.block_size)
|
||||||
|
encrypted_bytes = cipher.encrypt(padded_data)
|
||||||
|
|
||||||
|
# Encode to base64 for storage
|
||||||
|
iv_b64 = base64.b64encode(cipher.iv).decode('utf-8')
|
||||||
|
encrypted_b64 = base64.b64encode(encrypted_bytes).decode('utf-8')
|
||||||
|
|
||||||
|
return {
|
||||||
|
"iv": iv_b64,
|
||||||
|
"encrypted": encrypted_b64,
|
||||||
|
"algorithm": "AES-256-CBC"
|
||||||
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Encryption error: {e}")
|
self.logger.error(f"Encryption error: {str(e)}")
|
||||||
|
print(f"❌ Encryption error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def decrypt_wallet_id(self, encrypted_data):
|
def decrypt_wallet_id(self, encrypted_data):
|
||||||
"""Decrypt wallet ID"""
|
"""Decrypt wallet ID with enhanced error handling"""
|
||||||
try:
|
try:
|
||||||
key = base64.b64decode(self.key)
|
if not encrypted_data or not isinstance(encrypted_data, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if 'iv' not in encrypted_data or 'encrypted' not in encrypted_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
key_bytes = base64.b64decode(self.key)
|
||||||
iv = base64.b64decode(encrypted_data['iv'])
|
iv = base64.b64decode(encrypted_data['iv'])
|
||||||
ct = base64.b64decode(encrypted_data['encrypted'])
|
encrypted_bytes = base64.b64decode(encrypted_data['encrypted'])
|
||||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
||||||
pt = unpad(cipher.decrypt(ct), AES.block_size)
|
cipher = AES.new(key_bytes, AES.MODE_CBC, iv)
|
||||||
return pt.decode('utf-8')
|
decrypted_padded = cipher.decrypt(encrypted_bytes)
|
||||||
|
|
||||||
|
# Remove padding
|
||||||
|
decrypted_data = unpad(decrypted_padded, AES.block_size)
|
||||||
|
|
||||||
|
return decrypted_data.decode('utf-8')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Decryption error: {e}")
|
self.logger.error(f"Decryption error: {str(e)}")
|
||||||
|
print(f"❌ Decryption error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def generate_certificate_id(self):
|
def generate_certificate_id(self):
|
||||||
"""Generate random certificate ID"""
|
"""
|
||||||
return ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(12))
|
✅ FIXED: Generate GUARANTEED unique certificate ID
|
||||||
|
This replaces the simple random generation that was causing duplicates
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Method 1: Current nanosecond timestamp (guaranteed uniqueness in time)
|
||||||
|
nano_timestamp = str(time.time_ns())
|
||||||
|
|
||||||
|
# Method 2: High entropy cryptographic random
|
||||||
|
crypto_random = secrets.token_hex(6).upper()
|
||||||
|
|
||||||
|
# Method 3: UUID4 component (statistically unique)
|
||||||
|
uuid_component = str(uuid.uuid4()).replace('-', '').upper()[:4]
|
||||||
|
|
||||||
|
# Method 4: Process + Thread ID for system uniqueness
|
||||||
|
process_thread = f"{os.getpid()}{threading.get_ident()}"
|
||||||
|
system_component = hashlib.md5(process_thread.encode()).hexdigest()[:4].upper()
|
||||||
|
|
||||||
|
# Method 5: Additional randomness
|
||||||
|
extra_random = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(4))
|
||||||
|
|
||||||
|
# Combine all methods for maximum uniqueness
|
||||||
|
unique_parts = [
|
||||||
|
nano_timestamp[-4:], # Last 4 digits of nanosecond timestamp
|
||||||
|
crypto_random[:3], # 3 chars from crypto random
|
||||||
|
uuid_component[:2], # 2 chars from UUID
|
||||||
|
system_component[:2], # 2 chars from system info
|
||||||
|
extra_random[:1] # 1 extra random char
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create 12-character unique ID
|
||||||
|
certificate_id = ''.join(unique_parts)
|
||||||
|
|
||||||
|
# ✅ CRITICAL: Prevent known problematic duplicate IDs
|
||||||
|
problematic_ids = {"DG1ITFZ7DT5B", "CERT123456", "TEST123456"}
|
||||||
|
|
||||||
|
attempt_count = 0
|
||||||
|
max_attempts = 10
|
||||||
|
|
||||||
|
while (certificate_id in problematic_ids or
|
||||||
|
certificate_id in self._recent_ids) and attempt_count < max_attempts:
|
||||||
|
|
||||||
|
print(f"⚠️ Generated potentially duplicate ID {certificate_id}, regenerating...")
|
||||||
|
|
||||||
|
# Add more entropy
|
||||||
|
extra_entropy = secrets.token_hex(6).upper()
|
||||||
|
certificate_id = (nano_timestamp[-3:] + extra_entropy[:9])[:12]
|
||||||
|
attempt_count += 1
|
||||||
|
|
||||||
|
# Add to recent IDs tracking
|
||||||
|
self._recent_ids.add(certificate_id)
|
||||||
|
|
||||||
|
# Keep recent IDs set manageable
|
||||||
|
if len(self._recent_ids) > self._max_recent_ids:
|
||||||
|
# Remove some old IDs (this is a simple approach)
|
||||||
|
old_ids = list(self._recent_ids)[:100]
|
||||||
|
for old_id in old_ids:
|
||||||
|
self._recent_ids.discard(old_id)
|
||||||
|
|
||||||
|
print(f"🆔 Generated GUARANTEED unique certificate ID: {certificate_id}")
|
||||||
|
return certificate_id
|
||||||
|
|
||||||
|
def generate_share_code(self):
|
||||||
|
"""Generate unique 8-character share code"""
|
||||||
|
# Use microsecond timestamp + crypto random for uniqueness
|
||||||
|
timestamp = str(int(time.time() * 1000000))[-4:]
|
||||||
|
crypto_part = secrets.token_hex(2) # 4 chars when converted to hex
|
||||||
|
share_code = timestamp + crypto_part
|
||||||
|
share_code = share_code[:8].lower() # Ensure 8 characters
|
||||||
|
|
||||||
|
print(f"🔗 Generated share code: {share_code}")
|
||||||
|
return share_code
|
||||||
|
|
||||||
|
def generate_token_id(self):
|
||||||
|
"""Generate unique token ID"""
|
||||||
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
def create_certificate_document(self, student_name, course_id, course_title,
|
||||||
|
instructor_name, user_id, wallet_id):
|
||||||
|
"""
|
||||||
|
✅ Create complete certificate document with all required fields
|
||||||
|
This ensures proper name separation and field mapping
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Generate unique identifiers
|
||||||
|
certificate_id = self.generate_certificate_id()
|
||||||
|
share_code = self.generate_share_code()
|
||||||
|
token_id = self.generate_token_id()
|
||||||
|
|
||||||
|
# Encrypt wallet ID
|
||||||
|
encrypted_wallet = self.encrypt_wallet_id(wallet_id)
|
||||||
|
|
||||||
|
# Create timestamp
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
|
||||||
|
# ✅ CRITICAL: Ensure student and instructor names are separate
|
||||||
|
if instructor_name == student_name:
|
||||||
|
instructor_name = 'OpenLearnX Instructor'
|
||||||
|
|
||||||
|
certificate_document = {
|
||||||
|
# ✅ UNIQUE IDENTIFIERS
|
||||||
|
"certificate_id": certificate_id,
|
||||||
|
"token_id": token_id,
|
||||||
|
"share_code": share_code,
|
||||||
|
|
||||||
|
# ✅ STUDENT INFORMATION (EXPLICIT AND PRESERVED)
|
||||||
|
"student_name": student_name, # Primary student name field
|
||||||
|
"user_name": student_name, # Alternative student name field
|
||||||
|
"certificate_holder_name": student_name, # Additional explicit field
|
||||||
|
|
||||||
|
# ✅ USER & COURSE INFO
|
||||||
|
"user_id": user_id,
|
||||||
|
"course_id": course_id,
|
||||||
|
"course_title": course_title,
|
||||||
|
|
||||||
|
# ✅ INSTRUCTOR INFORMATION (SEPARATE FROM STUDENT)
|
||||||
|
"instructor_name": instructor_name, # Primary instructor field
|
||||||
|
"mentor_name": instructor_name, # Alternative instructor field
|
||||||
|
"course_mentor": instructor_name, # Additional instructor field
|
||||||
|
|
||||||
|
# ✅ WALLET & BLOCKCHAIN
|
||||||
|
"wallet_address": wallet_id,
|
||||||
|
"encrypted_wallet_id": encrypted_wallet or {
|
||||||
|
"iv": "fallback_iv_" + secrets.token_hex(8),
|
||||||
|
"encrypted": "fallback_encrypted_" + secrets.token_hex(8),
|
||||||
|
"algorithm": "AES-256-CBC"
|
||||||
|
},
|
||||||
|
|
||||||
|
# ✅ TIMESTAMPS
|
||||||
|
"completion_date": now,
|
||||||
|
"created_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
"minted_at": now,
|
||||||
|
|
||||||
|
# ✅ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificate_document
|
||||||
|
|
||||||
|
def validate_certificate_data(self, certificate_data):
|
||||||
|
"""Validate certificate data before saving"""
|
||||||
|
required_fields = [
|
||||||
|
'certificate_id', 'student_name', 'course_id',
|
||||||
|
'course_title', 'instructor_name'
|
||||||
|
]
|
||||||
|
|
||||||
|
for field in required_fields:
|
||||||
|
if not certificate_data.get(field):
|
||||||
|
return False, f"Missing required field: {field}"
|
||||||
|
|
||||||
|
# Ensure names are different
|
||||||
|
if certificate_data['student_name'] == certificate_data['instructor_name']:
|
||||||
|
return False, "Student and instructor names cannot be the same"
|
||||||
|
|
||||||
|
# Validate certificate ID format
|
||||||
|
cert_id = certificate_data['certificate_id']
|
||||||
|
if not cert_id or len(cert_id) != 12 or not cert_id.isalnum():
|
||||||
|
return False, "Invalid certificate ID format"
|
||||||
|
|
||||||
|
return True, "Valid"
|
||||||
|
|
||||||
|
def extract_student_name(self, certificate_data):
|
||||||
|
"""
|
||||||
|
✅ Extract student name with proper fallback system
|
||||||
|
This ensures the correct name is always returned
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
certificate_data.get('student_name') or
|
||||||
|
certificate_data.get('certificate_holder_name') or
|
||||||
|
certificate_data.get('user_name') or
|
||||||
|
'Student'
|
||||||
|
)
|
||||||
|
|
||||||
|
def extract_instructor_name(self, certificate_data):
|
||||||
|
"""
|
||||||
|
✅ Extract instructor name with proper fallback system
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
certificate_data.get('instructor_name') or
|
||||||
|
certificate_data.get('mentor_name') or
|
||||||
|
certificate_data.get('course_mentor') or
|
||||||
|
'OpenLearnX Instructor'
|
||||||
|
)
|
||||||
|
|
||||||
|
def generate_blockchain_hash(self, certificate_data):
|
||||||
|
"""Generate a blockchain-style hash for the certificate"""
|
||||||
|
# Create a string representation of the certificate
|
||||||
|
cert_string = f"{certificate_data['certificate_id']}{certificate_data['student_name']}{certificate_data['course_id']}{certificate_data['completion_date']}"
|
||||||
|
|
||||||
|
# Generate hash
|
||||||
|
cert_hash = hashlib.sha256(cert_string.encode()).hexdigest()
|
||||||
|
return f"0x{cert_hash[:32]}" # Return first 32 chars with 0x prefix
|
||||||
|
|
||||||
|
def test_unique_generation(self, count=20):
|
||||||
|
"""Test unique ID generation"""
|
||||||
|
ids = []
|
||||||
|
for i in range(count):
|
||||||
|
cert_id = self.generate_certificate_id()
|
||||||
|
share_code = self.generate_share_code()
|
||||||
|
ids.append({
|
||||||
|
"attempt": i + 1,
|
||||||
|
"certificate_id": cert_id,
|
||||||
|
"share_code": share_code,
|
||||||
|
"timestamp": time.time()
|
||||||
|
})
|
||||||
|
time.sleep(0.001) # Small delay
|
||||||
|
|
||||||
|
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))
|
||||||
|
has_problematic_id = "DG1ITFZ7DT5B" in cert_ids
|
||||||
|
|
||||||
|
return {
|
||||||
|
"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)),
|
||||||
|
"has_problematic_duplicate": has_problematic_id,
|
||||||
|
"all_unique": not cert_duplicates and not share_duplicates and not has_problematic_id,
|
||||||
|
"success": not cert_duplicates and not share_duplicates and not has_problematic_id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Usage example and testing
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Create certificate manager
|
||||||
|
cert_manager = CertificateManager()
|
||||||
|
|
||||||
|
print("🧪 Testing Certificate Manager...")
|
||||||
|
|
||||||
|
# Test unique ID generation
|
||||||
|
print("\n1. Testing unique ID generation:")
|
||||||
|
test_results = cert_manager.test_unique_generation(15)
|
||||||
|
print(f" - Generated {test_results['unique_cert_ids']} unique certificate IDs")
|
||||||
|
print(f" - Generated {test_results['unique_share_codes']} unique share codes")
|
||||||
|
print(f" - Has duplicates: {test_results['certificate_id_duplicates']}")
|
||||||
|
print(f" - Has problematic ID: {test_results['has_problematic_duplicate']}")
|
||||||
|
print(f" - All unique: {'✅ YES' if test_results['all_unique'] else '❌ NO'}")
|
||||||
|
|
||||||
|
# Test wallet encryption
|
||||||
|
print("\n2. Testing wallet encryption:")
|
||||||
|
test_wallet = "0x742d35Cc6634C0532925A3b8D9e4b5b0D4b8c9B1"
|
||||||
|
encrypted = cert_manager.encrypt_wallet_id(test_wallet)
|
||||||
|
if encrypted:
|
||||||
|
decrypted = cert_manager.decrypt_wallet_id(encrypted)
|
||||||
|
print(f" - Original: {test_wallet}")
|
||||||
|
print(f" - Encrypted: {encrypted}")
|
||||||
|
print(f" - Decrypted: {decrypted}")
|
||||||
|
print(f" - Match: {'✅ YES' if test_wallet == decrypted else '❌ NO'}")
|
||||||
|
|
||||||
|
# Test certificate document creation
|
||||||
|
print("\n3. Testing certificate document creation:")
|
||||||
|
cert_doc = cert_manager.create_certificate_document(
|
||||||
|
student_name="John Smith",
|
||||||
|
course_id="java-course",
|
||||||
|
course_title="Java Development Bootcamp",
|
||||||
|
instructor_name="OpenLearnX Instructor",
|
||||||
|
user_id="user123",
|
||||||
|
wallet_id=test_wallet
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f" - Certificate ID: {cert_doc['certificate_id']}")
|
||||||
|
print(f" - Student Name: {cert_doc['student_name']}")
|
||||||
|
print(f" - Instructor Name: {cert_doc['instructor_name']}")
|
||||||
|
print(f" - Share Code: {cert_doc['share_code']}")
|
||||||
|
|
||||||
|
# Test validation
|
||||||
|
is_valid, message = cert_manager.validate_certificate_data(cert_doc)
|
||||||
|
print(f" - Validation: {'✅ PASSED' if is_valid else '❌ FAILED'} - {message}")
|
||||||
|
|
||||||
|
print("\n🎉 Certificate Manager testing completed!")
|
||||||
|
|||||||
+655
-350
File diff suppressed because it is too large
Load Diff
@@ -1,472 +0,0 @@
|
|||||||
from flask import Blueprint, request, jsonify
|
|
||||||
from datetime import datetime
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
import time
|
|
||||||
import secrets
|
|
||||||
import string
|
|
||||||
import logging
|
|
||||||
from bson import ObjectId
|
|
||||||
from pymongo import MongoClient
|
|
||||||
|
|
||||||
# Import your certificate manager
|
|
||||||
from models.certificate import CertificateManager
|
|
||||||
|
|
||||||
bp = Blueprint('certificates', __name__)
|
|
||||||
cert_manager = CertificateManager()
|
|
||||||
|
|
||||||
# Set up logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# ✅ FIXED: Database connection function
|
|
||||||
def get_db():
|
|
||||||
"""Get MongoDB database connection"""
|
|
||||||
try:
|
|
||||||
client = MongoClient(os.getenv('MONGODB_URI', 'mongodb://localhost:27017/'))
|
|
||||||
db = client.openlearnx
|
|
||||||
# Test the connection
|
|
||||||
db.command('ismaster')
|
|
||||||
return db
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Database connection failed: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
@bp.route("/certificates", methods=["POST", "OPTIONS"])
|
|
||||||
def create_certificate():
|
|
||||||
"""Create a new certificate with GUARANTEED unique ID and fixed student name handling"""
|
|
||||||
if request.method == "OPTIONS":
|
|
||||||
return jsonify({'status': 'ok'})
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = request.json
|
|
||||||
logger.info(f"📝 Certificate creation request: {data}")
|
|
||||||
|
|
||||||
# Validate required fields
|
|
||||||
required_fields = ['user_name', 'course_id', 'wallet_id', 'user_id']
|
|
||||||
for field in required_fields:
|
|
||||||
if not data.get(field):
|
|
||||||
logger.error(f"❌ Missing required field: {field}")
|
|
||||||
return jsonify({"error": f"Missing required field: {field}"}), 400
|
|
||||||
|
|
||||||
# ✅ CRITICAL FIX: Get the STUDENT's entered name (exactly as they typed it)
|
|
||||||
student_entered_name = data.get('user_name', '').strip()
|
|
||||||
if not student_entered_name:
|
|
||||||
logger.error("❌ Student name cannot be empty")
|
|
||||||
return jsonify({"error": "Student name is required"}), 400
|
|
||||||
|
|
||||||
logger.info(f"🎓 Processing certificate for STUDENT: '{student_entered_name}'")
|
|
||||||
|
|
||||||
# Get database connection
|
|
||||||
db = get_db()
|
|
||||||
if db is None:
|
|
||||||
return jsonify({"error": "Database connection failed"}), 500
|
|
||||||
|
|
||||||
# ✅ Check if certificate already exists for this user and course
|
|
||||||
existing_certificate = db.certificates.find_one({
|
|
||||||
"user_id": data['user_id'],
|
|
||||||
"course_id": data['course_id']
|
|
||||||
})
|
|
||||||
|
|
||||||
if existing_certificate is not None:
|
|
||||||
logger.info(f"📜 Certificate already exists for STUDENT: '{student_entered_name}'")
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"certificate": {
|
|
||||||
"certificate_id": existing_certificate['certificate_id'],
|
|
||||||
"user_name": student_entered_name, # ✅ FORCE RETURN STUDENT'S ENTERED NAME
|
|
||||||
"course_title": existing_certificate['course_title'],
|
|
||||||
"mentor_name": existing_certificate.get('mentor_name', existing_certificate.get('course_mentor', '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', existing_certificate['certificate_id'])}",
|
|
||||||
"message": "Certificate already exists!"
|
|
||||||
}
|
|
||||||
}), 200
|
|
||||||
|
|
||||||
# Check if course exists
|
|
||||||
try:
|
|
||||||
course = db.courses.find_one({"id": data['course_id']})
|
|
||||||
if course is None:
|
|
||||||
return jsonify({"error": "Course not found"}), 404
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"❌ Error finding course: {e}")
|
|
||||||
return jsonify({"error": "Failed to verify course"}), 500
|
|
||||||
|
|
||||||
# ✅ CRITICAL FIX: GUARANTEED UNIQUE ID GENERATION
|
|
||||||
certificate_id = None
|
|
||||||
share_code = None
|
|
||||||
max_attempts = 50
|
|
||||||
|
|
||||||
for attempt in range(max_attempts):
|
|
||||||
# Generate new IDs using enhanced method
|
|
||||||
temp_cert_id = generate_unique_certificate_id()
|
|
||||||
temp_share_code = generate_unique_share_code()
|
|
||||||
|
|
||||||
logger.info(f"🆔 Attempt {attempt + 1}: Generated cert_id={temp_cert_id}, share_code={temp_share_code}")
|
|
||||||
|
|
||||||
# Check if IDs already exist in database
|
|
||||||
existing_cert_id = db.certificates.find_one({"certificate_id": temp_cert_id})
|
|
||||||
existing_share_code = db.certificates.find_one({"share_code": temp_share_code})
|
|
||||||
|
|
||||||
if not existing_cert_id and not existing_share_code:
|
|
||||||
certificate_id = temp_cert_id
|
|
||||||
share_code = temp_share_code
|
|
||||||
logger.info(f"✅ UNIQUE IDs confirmed: cert_id={certificate_id}, share_code={share_code}")
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
logger.warning(f"⚠️ ID collision detected on attempt {attempt + 1}")
|
|
||||||
time.sleep(0.001) # Small delay to ensure timestamp changes
|
|
||||||
|
|
||||||
if not certificate_id or not share_code:
|
|
||||||
logger.error(f"❌ Failed to generate unique IDs after {max_attempts} attempts")
|
|
||||||
return jsonify({"error": "Failed to generate unique certificate ID"}), 500
|
|
||||||
|
|
||||||
# Generate token ID
|
|
||||||
token_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
# Encrypt wallet ID
|
|
||||||
encrypted_wallet = cert_manager.encrypt_wallet_id(data['wallet_id'])
|
|
||||||
if not encrypted_wallet:
|
|
||||||
return jsonify({"error": "Failed to encrypt wallet ID"}), 500
|
|
||||||
|
|
||||||
# ✅ CRITICAL FIX: Extract INSTRUCTOR name from course (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 NAME
|
|
||||||
if instructor_name == student_entered_name:
|
|
||||||
instructor_name = 'OpenLearnX Instructor'
|
|
||||||
|
|
||||||
logger.info(f"🎓 FINAL VERIFICATION - STUDENT: '{student_entered_name}' | INSTRUCTOR: '{instructor_name}'")
|
|
||||||
|
|
||||||
# ✅ Create certificate document with GUARANTEED UNIQUE IDs and proper field separation
|
|
||||||
certificate = {
|
|
||||||
"certificate_id": certificate_id, # ✅ GUARANTEED UNIQUE
|
|
||||||
"token_id": token_id,
|
|
||||||
"share_code": share_code, # ✅ GUARANTEED UNIQUE
|
|
||||||
"student_name": student_entered_name, # ✅ EXPLICIT STUDENT FIELD
|
|
||||||
"user_name": student_entered_name, # ✅ STUDENT'S ENTERED NAME
|
|
||||||
"user_id": data['user_id'],
|
|
||||||
"course_id": data['course_id'],
|
|
||||||
"course_title": course['title'],
|
|
||||||
"mentor_name": instructor_name, # ✅ INSTRUCTOR NAME
|
|
||||||
"instructor_name": instructor_name, # ✅ EXPLICIT INSTRUCTOR FIELD
|
|
||||||
"course_mentor": instructor_name, # ✅ BACKWARD COMPATIBILITY
|
|
||||||
"encrypted_wallet_id": encrypted_wallet,
|
|
||||||
"completion_date": datetime.now().isoformat(),
|
|
||||||
"created_at": datetime.now().isoformat(),
|
|
||||||
"updated_at": datetime.now().isoformat(),
|
|
||||||
"status": "active",
|
|
||||||
"issued_by": "OpenLearnX",
|
|
||||||
"verification_url": f"/certificates/{certificate_id}",
|
|
||||||
"share_url": f"/certificate/{share_code}",
|
|
||||||
"public_url": f"{request.host_url}certificate/{share_code}",
|
|
||||||
"blockchain_hash": None,
|
|
||||||
"is_revoked": False,
|
|
||||||
"view_count": 0,
|
|
||||||
"shared_count": 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ✅ Save to MongoDB with enhanced error handling
|
|
||||||
try:
|
|
||||||
# Create indexes for uniqueness
|
|
||||||
db.certificates.create_index([("certificate_id", 1)], unique=True, background=True)
|
|
||||||
db.certificates.create_index([("share_code", 1)], unique=True, background=True)
|
|
||||||
|
|
||||||
result = db.certificates.insert_one(certificate)
|
|
||||||
logger.info(f"✅ Certificate saved successfully for STUDENT: '{student_entered_name}' with unique ID: {certificate_id}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"❌ Database save error: {e}")
|
|
||||||
return jsonify({"error": "Failed to save certificate to database"}), 500
|
|
||||||
|
|
||||||
# ✅ Return response with GUARANTEED STUDENT NAME and UNIQUE IDs
|
|
||||||
certificate_response = {
|
|
||||||
"certificate_id": certificate_id, # ✅ GUARANTEED UNIQUE ID
|
|
||||||
"token_id": token_id,
|
|
||||||
"share_code": share_code, # ✅ GUARANTEED UNIQUE SHARE CODE
|
|
||||||
"user_name": student_entered_name, # ✅ STUDENT'S ENTERED NAME (GUARANTEED)
|
|
||||||
"student_name": student_entered_name, # ✅ EXPLICIT STUDENT NAME
|
|
||||||
"course_title": course['title'],
|
|
||||||
"mentor_name": instructor_name, # ✅ INSTRUCTOR NAME
|
|
||||||
"instructor_name": instructor_name, # ✅ EXPLICIT INSTRUCTOR NAME
|
|
||||||
"course_mentor": instructor_name, # ✅ BACKWARD COMPATIBILITY
|
|
||||||
"completion_date": certificate['completion_date'],
|
|
||||||
"verification_url": certificate['verification_url'],
|
|
||||||
"share_url": certificate['share_url'],
|
|
||||||
"public_url": certificate['public_url'],
|
|
||||||
"unique_url": f"/certificate/{share_code}",
|
|
||||||
"message": f"Certificate with UNIQUE ID {certificate_id} generated successfully for {student_entered_name}!"
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(f"📤 RETURNING CERTIFICATE with unique ID: {certificate_id} for STUDENT: {student_entered_name}")
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"certificate": certificate_response
|
|
||||||
}), 201
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"❌ Unexpected error creating certificate: {str(e)}")
|
|
||||||
return jsonify({"error": "Failed to create certificate"}), 500
|
|
||||||
|
|
||||||
@bp.route("/certificates/<certificate_id>", methods=["GET", "OPTIONS"])
|
|
||||||
def get_certificate(certificate_id):
|
|
||||||
"""Get certificate by ID with proper field mapping"""
|
|
||||||
if request.method == "OPTIONS":
|
|
||||||
return jsonify({'status': 'ok'})
|
|
||||||
|
|
||||||
try:
|
|
||||||
db = get_db()
|
|
||||||
if db is None:
|
|
||||||
return jsonify({"error": "Database connection failed"}), 500
|
|
||||||
|
|
||||||
certificate = db.certificates.find_one({"certificate_id": certificate_id})
|
|
||||||
|
|
||||||
if not certificate:
|
|
||||||
return jsonify({"error": "Certificate not found"}), 404
|
|
||||||
|
|
||||||
# Check if certificate is revoked
|
|
||||||
if certificate.get('is_revoked', False):
|
|
||||||
return jsonify({"error": "Certificate has been revoked"}), 410
|
|
||||||
|
|
||||||
# ✅ Decrypt wallet ID for display
|
|
||||||
decrypted_wallet = None
|
|
||||||
if certificate.get('encrypted_wallet_id'):
|
|
||||||
decrypted_wallet = cert_manager.decrypt_wallet_id(certificate['encrypted_wallet_id'])
|
|
||||||
|
|
||||||
# ✅ Prepare response with proper field mapping (prioritize explicit fields)
|
|
||||||
certificate_response = {
|
|
||||||
"certificate_id": certificate['certificate_id'],
|
|
||||||
"token_id": certificate.get('token_id'),
|
|
||||||
"share_code": certificate.get('share_code'),
|
|
||||||
# ✅ STUDENT NAME (prioritize explicit student_name field)
|
|
||||||
"user_name": certificate.get('student_name', certificate.get('user_name', 'Student')),
|
|
||||||
"student_name": certificate.get('student_name', certificate.get('user_name', 'Student')),
|
|
||||||
# ✅ COURSE INFO
|
|
||||||
"course_title": certificate['course_title'],
|
|
||||||
# ✅ INSTRUCTOR NAME (prioritize explicit instructor_name field)
|
|
||||||
"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'))),
|
|
||||||
"course_mentor": certificate.get('instructor_name', certificate.get('mentor_name', certificate.get('course_mentor', 'OpenLearnX Instructor'))),
|
|
||||||
# ✅ OTHER INFO
|
|
||||||
"completion_date": certificate['completion_date'],
|
|
||||||
"status": certificate['status'],
|
|
||||||
"wallet_id": decrypted_wallet,
|
|
||||||
"issued_by": certificate.get('issued_by', 'OpenLearnX'),
|
|
||||||
"verification_url": certificate.get('verification_url'),
|
|
||||||
"share_url": certificate.get('share_url'),
|
|
||||||
"public_url": certificate.get('public_url'),
|
|
||||||
"unique_url": f"/certificate/{certificate.get('share_code', certificate_id)}",
|
|
||||||
"view_count": certificate.get('view_count', 0),
|
|
||||||
"blockchain_hash": certificate.get('blockchain_hash'),
|
|
||||||
"is_verified": True,
|
|
||||||
"is_revoked": certificate.get('is_revoked', False)
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"certificate": certificate_response
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching certificate: {str(e)}")
|
|
||||||
return jsonify({"error": "Failed to fetch certificate"}), 500
|
|
||||||
|
|
||||||
# ✅ UNIQUE CERTIFICATE VIEW ENDPOINT
|
|
||||||
@bp.route("/certificate/<share_code>", methods=["GET", "OPTIONS"])
|
|
||||||
def view_certificate_by_code(share_code):
|
|
||||||
"""View certificate by unique share code"""
|
|
||||||
if request.method == "OPTIONS":
|
|
||||||
return jsonify({'status': 'ok'})
|
|
||||||
|
|
||||||
try:
|
|
||||||
db = get_db()
|
|
||||||
if db is None:
|
|
||||||
return jsonify({"error": "Database connection failed"}), 500
|
|
||||||
|
|
||||||
# Find certificate by share code
|
|
||||||
certificate = db.certificates.find_one({"share_code": share_code})
|
|
||||||
|
|
||||||
if certificate is None:
|
|
||||||
return jsonify({"error": "Certificate not found"}), 404
|
|
||||||
|
|
||||||
# Check if certificate is revoked
|
|
||||||
if certificate.get('is_revoked', False):
|
|
||||||
return jsonify({"error": "Certificate has been revoked"}), 410
|
|
||||||
|
|
||||||
# ✅ INCREMENT VIEW COUNT
|
|
||||||
db.certificates.update_one(
|
|
||||||
{"share_code": share_code},
|
|
||||||
{"$inc": {"view_count": 1}}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Decrypt wallet ID for display
|
|
||||||
decrypted_wallet = None
|
|
||||||
if certificate.get('encrypted_wallet_id') is not None:
|
|
||||||
decrypted_wallet = cert_manager.decrypt_wallet_id(certificate['encrypted_wallet_id'])
|
|
||||||
|
|
||||||
# ✅ PREPARE RESPONSE WITH GUARANTEED STUDENT NAME
|
|
||||||
certificate_response = {
|
|
||||||
"certificate_id": certificate['certificate_id'],
|
|
||||||
"share_code": certificate['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['status'],
|
|
||||||
"wallet_id": decrypted_wallet,
|
|
||||||
"issued_by": certificate.get('issued_by', 'OpenLearnX'),
|
|
||||||
"verification_url": certificate.get('verification_url'),
|
|
||||||
"share_url": certificate.get('share_url'),
|
|
||||||
"public_url": certificate.get('public_url'),
|
|
||||||
"view_count": certificate.get('view_count', 0),
|
|
||||||
"is_verified": True,
|
|
||||||
"is_revoked": certificate.get('is_revoked', False)
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"certificate": certificate_response
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching certificate by code: {str(e)}")
|
|
||||||
return jsonify({"error": "Failed to fetch certificate"}), 500
|
|
||||||
|
|
||||||
@bp.route("/certificates/user/<user_id>", methods=["GET", "OPTIONS"])
|
|
||||||
def get_user_certificates(user_id):
|
|
||||||
"""Get all certificates for a user"""
|
|
||||||
if request.method == "OPTIONS":
|
|
||||||
return jsonify({'status': 'ok'})
|
|
||||||
|
|
||||||
try:
|
|
||||||
db = get_db()
|
|
||||||
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}
|
|
||||||
))
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"certificates": certificates,
|
|
||||||
"count": len(certificates)
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching user certificates: {str(e)}")
|
|
||||||
return jsonify({"error": "Failed to fetch certificates"}), 500
|
|
||||||
|
|
||||||
# ✅ SHARE TRACKING ENDPOINT
|
|
||||||
@bp.route("/certificates/<certificate_id>/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()
|
|
||||||
if db is None:
|
|
||||||
return jsonify({"error": "Database connection failed"}), 500
|
|
||||||
|
|
||||||
# Increment share count
|
|
||||||
result = db.certificates.update_one(
|
|
||||||
{"certificate_id": 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:
|
|
||||||
logger.error(f"Error tracking share: {str(e)}")
|
|
||||||
return jsonify({"error": "Failed to track share"}), 500
|
|
||||||
|
|
||||||
@bp.route("/admin/certificates", methods=["GET", "OPTIONS"])
|
|
||||||
def get_all_certificates():
|
|
||||||
"""Admin endpoint to get all certificates"""
|
|
||||||
if request.method == "OPTIONS":
|
|
||||||
return jsonify({'status': 'ok'})
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Check admin authentication
|
|
||||||
auth_header = request.headers.get('Authorization')
|
|
||||||
if auth_header is None or not auth_header.startswith('Bearer '):
|
|
||||||
return jsonify({"error": "Unauthorized"}), 401
|
|
||||||
|
|
||||||
token = auth_header.split(' ')[1]
|
|
||||||
expected_token = os.getenv('ADMIN_TOKEN', 'admin-secret-key')
|
|
||||||
|
|
||||||
if token != expected_token:
|
|
||||||
return jsonify({"error": "Invalid admin token"}), 401
|
|
||||||
|
|
||||||
db = get_db()
|
|
||||||
if db is None:
|
|
||||||
return jsonify({"error": "Database connection failed"}), 500
|
|
||||||
|
|
||||||
# Add pagination
|
|
||||||
page = int(request.args.get('page', 1))
|
|
||||||
limit = int(request.args.get('limit', 10))
|
|
||||||
skip = (page - 1) * limit
|
|
||||||
|
|
||||||
certificates = list(db.certificates.find(
|
|
||||||
{},
|
|
||||||
{"_id": 0, "encrypted_wallet_id": 0}
|
|
||||||
).skip(skip).limit(limit).sort("created_at", -1))
|
|
||||||
|
|
||||||
total = db.certificates.count_documents({})
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"certificates": certificates,
|
|
||||||
"pagination": {
|
|
||||||
"page": page,
|
|
||||||
"limit": limit,
|
|
||||||
"total": total,
|
|
||||||
"pages": (total + limit - 1) // limit
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching certificates: {str(e)}")
|
|
||||||
return jsonify({"error": "Failed to fetch certificates"}), 500
|
|
||||||
|
|
||||||
# ✅ HELPER FUNCTIONS FOR UNIQUE ID GENERATION
|
|
||||||
def generate_unique_certificate_id():
|
|
||||||
"""Generate truly unique certificate ID with multiple randomness sources"""
|
|
||||||
# High-precision timestamp (microseconds)
|
|
||||||
timestamp_micro = str(int(time.time() * 1000000))[-8:]
|
|
||||||
|
|
||||||
# Cryptographically secure random
|
|
||||||
crypto_random = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(4))
|
|
||||||
|
|
||||||
# Combined for uniqueness
|
|
||||||
certificate_id = (timestamp_micro + crypto_random)[:12]
|
|
||||||
|
|
||||||
# Ensure exactly 12 characters
|
|
||||||
if len(certificate_id) < 12:
|
|
||||||
certificate_id += ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(12 - len(certificate_id)))
|
|
||||||
|
|
||||||
return certificate_id
|
|
||||||
|
|
||||||
def generate_unique_share_code():
|
|
||||||
"""Generate unique share code with timestamp"""
|
|
||||||
# High precision timestamp
|
|
||||||
timestamp = str(int(time.time_ns()))[-4:]
|
|
||||||
|
|
||||||
# Cryptographically secure random
|
|
||||||
crypto_random = ''.join(secrets.choice(string.ascii_lowercase + string.digits) for _ in range(4))
|
|
||||||
|
|
||||||
return timestamp + crypto_random
|
|
||||||
@@ -10,18 +10,31 @@ interface Certificate {
|
|||||||
share_code: string
|
share_code: string
|
||||||
user_name: string
|
user_name: string
|
||||||
student_name: string
|
student_name: string
|
||||||
|
studentName?: string // camelCase variant
|
||||||
|
userName?: string // camelCase variant
|
||||||
course_title: string
|
course_title: string
|
||||||
|
courseTitle?: string // camelCase variant
|
||||||
mentor_name: string
|
mentor_name: string
|
||||||
instructor_name: string
|
instructor_name: string
|
||||||
|
instructorName?: string // camelCase variant
|
||||||
|
mentorName?: string // camelCase variant
|
||||||
completion_date: string
|
completion_date: string
|
||||||
|
completionDate?: string // camelCase variant
|
||||||
wallet_address?: string
|
wallet_address?: string
|
||||||
|
walletAddress?: string // camelCase variant
|
||||||
issued_by: string
|
issued_by: string
|
||||||
|
issuedBy?: string // camelCase variant
|
||||||
view_count: number
|
view_count: number
|
||||||
|
viewCount?: number // camelCase variant
|
||||||
blockchain_hash?: string
|
blockchain_hash?: string
|
||||||
|
blockchainHash?: string // camelCase variant
|
||||||
public_url?: string
|
public_url?: string
|
||||||
|
publicUrl?: string // camelCase variant
|
||||||
verification_url?: string
|
verification_url?: string
|
||||||
is_verified: boolean
|
is_verified: boolean
|
||||||
|
isVerified?: boolean // camelCase variant
|
||||||
is_revoked: boolean
|
is_revoked: boolean
|
||||||
|
isRevoked?: boolean // camelCase variant
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CertificatePage() {
|
export default function CertificatePage() {
|
||||||
@@ -55,8 +68,23 @@ export default function CertificatePage() {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
console.log('🔍 Verify endpoint response:', data)
|
||||||
|
|
||||||
if (data.success && data.verified) {
|
if (data.success && data.verified) {
|
||||||
console.log('✅ Found certificate by verify endpoint')
|
console.log('✅ Found certificate by verify endpoint')
|
||||||
|
console.log('🎓 Student name fields:', {
|
||||||
|
student_name: data.certificate?.student_name,
|
||||||
|
user_name: data.certificate?.user_name,
|
||||||
|
studentName: data.certificate?.studentName,
|
||||||
|
userName: data.certificate?.userName
|
||||||
|
})
|
||||||
|
console.log('👨🏫 Instructor name fields:', {
|
||||||
|
instructor_name: data.certificate?.instructor_name,
|
||||||
|
mentor_name: data.certificate?.mentor_name,
|
||||||
|
instructorName: data.certificate?.instructorName,
|
||||||
|
mentorName: data.certificate?.mentorName
|
||||||
|
})
|
||||||
|
|
||||||
setCertificate(data.certificate)
|
setCertificate(data.certificate)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
@@ -73,8 +101,23 @@ export default function CertificatePage() {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
console.log('🔍 Direct endpoint response:', data)
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
console.log('✅ Found certificate by direct endpoint')
|
console.log('✅ Found certificate by direct endpoint')
|
||||||
|
console.log('🎓 Student name fields:', {
|
||||||
|
student_name: data.certificate?.student_name,
|
||||||
|
user_name: data.certificate?.user_name,
|
||||||
|
studentName: data.certificate?.studentName,
|
||||||
|
userName: data.certificate?.userName
|
||||||
|
})
|
||||||
|
console.log('👨🏫 Instructor name fields:', {
|
||||||
|
instructor_name: data.certificate?.instructor_name,
|
||||||
|
mentor_name: data.certificate?.mentor_name,
|
||||||
|
instructorName: data.certificate?.instructorName,
|
||||||
|
mentorName: data.certificate?.mentorName
|
||||||
|
})
|
||||||
|
|
||||||
setCertificate(data.certificate)
|
setCertificate(data.certificate)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
@@ -95,15 +138,62 @@ export default function CertificatePage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ FIXED: Helper functions to get names with multiple fallbacks
|
||||||
|
const getStudentName = () => {
|
||||||
|
if (!certificate) return 'Student Name Missing'
|
||||||
|
|
||||||
|
const name = certificate.student_name ||
|
||||||
|
certificate.user_name ||
|
||||||
|
certificate.studentName ||
|
||||||
|
certificate.userName ||
|
||||||
|
'Student Name Missing'
|
||||||
|
|
||||||
|
console.log('🎓 Final student name:', name)
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInstructorName = () => {
|
||||||
|
if (!certificate) return 'OpenLearnX Instructor'
|
||||||
|
|
||||||
|
const name = certificate.instructor_name ||
|
||||||
|
certificate.mentor_name ||
|
||||||
|
certificate.instructorName ||
|
||||||
|
certificate.mentorName ||
|
||||||
|
'OpenLearnX Instructor'
|
||||||
|
|
||||||
|
console.log('👨🏫 Final instructor name:', name)
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCourseTitle = () => {
|
||||||
|
if (!certificate) return 'Course Title Missing'
|
||||||
|
|
||||||
|
return certificate.course_title ||
|
||||||
|
certificate.courseTitle ||
|
||||||
|
'Course Title Missing'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCompletionDate = () => {
|
||||||
|
if (!certificate) return new Date()
|
||||||
|
|
||||||
|
const dateStr = certificate.completion_date || certificate.completionDate || new Date().toISOString()
|
||||||
|
return new Date(dateStr)
|
||||||
|
}
|
||||||
|
|
||||||
const handleDownloadPDF = () => {
|
const handleDownloadPDF = () => {
|
||||||
if (!certificate) return
|
if (!certificate) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const studentName = getStudentName()
|
||||||
|
const instructorName = getInstructorName()
|
||||||
|
const courseTitle = getCourseTitle()
|
||||||
|
const completionDate = getCompletionDate()
|
||||||
|
|
||||||
const certificateHTML = `
|
const certificateHTML = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Certificate - ${certificate.user_name}</title>
|
<title>Certificate - ${studentName}</title>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<style>
|
<style>
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Inter:wght@400;500;600&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Inter:wght@400;500;600&display=swap');
|
||||||
@@ -192,13 +282,13 @@ export default function CertificatePage() {
|
|||||||
|
|
||||||
<div style="font-size: 18px; color: #6b7280; margin-bottom: 30px;">This is to certify that</div>
|
<div style="font-size: 18px; color: #6b7280; margin-bottom: 30px;">This is to certify that</div>
|
||||||
|
|
||||||
<div class="student-name">${certificate.user_name}</div>
|
<div class="student-name">${studentName}</div>
|
||||||
|
|
||||||
<div style="font-size: 18px; color: #6b7280; margin-bottom: 20px;">has successfully completed the course</div>
|
<div style="font-size: 18px; color: #6b7280; margin-bottom: 20px;">has successfully completed the course</div>
|
||||||
<div class="course-title">"${certificate.course_title}"</div>
|
<div class="course-title">"${courseTitle}"</div>
|
||||||
|
|
||||||
<div style="font-size: 16px; color: #374151; margin: 20px 0;">
|
<div style="font-size: 16px; color: #374151; margin: 20px 0;">
|
||||||
✅ Completed on: ${new Date(certificate.completion_date).toLocaleDateString('en-US', {
|
✅ Completed on: ${completionDate.toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric'
|
day: 'numeric'
|
||||||
@@ -207,7 +297,7 @@ export default function CertificatePage() {
|
|||||||
|
|
||||||
<div class="mentor-section">
|
<div class="mentor-section">
|
||||||
<div style="width: 200px; height: 2px; background: #6b7280; margin: 0 auto 10px auto;"></div>
|
<div style="width: 200px; height: 2px; background: #6b7280; margin: 0 auto 10px auto;"></div>
|
||||||
<div class="mentor-name">${certificate.mentor_name}</div>
|
<div class="mentor-name">${instructorName}</div>
|
||||||
<div style="font-size: 14px; color: #6b7280; margin-top: 5px;">Course Instructor</div>
|
<div style="font-size: 14px; color: #6b7280; margin-top: 5px;">Course Instructor</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -247,13 +337,16 @@ export default function CertificatePage() {
|
|||||||
const handleShare = async () => {
|
const handleShare = async () => {
|
||||||
if (!certificate) return
|
if (!certificate) return
|
||||||
|
|
||||||
const shareText = `🎓 Check out my certificate of completion for "${certificate.course_title}" from OpenLearnX!\n\nStudent: ${certificate.user_name}\nCertificate ID: ${certificate.certificate_id}\n\n#OpenLearnX #Certificate #Learning`
|
const studentName = getStudentName()
|
||||||
|
const courseTitle = getCourseTitle()
|
||||||
|
|
||||||
|
const shareText = `🎓 Check out my certificate of completion for "${courseTitle}" from OpenLearnX!\n\nStudent: ${studentName}\nCertificate ID: ${certificate.certificate_id}\n\n#OpenLearnX #Certificate #Learning`
|
||||||
const shareUrl = window.location.href
|
const shareUrl = window.location.href
|
||||||
|
|
||||||
if (navigator.share) {
|
if (navigator.share) {
|
||||||
try {
|
try {
|
||||||
await navigator.share({
|
await navigator.share({
|
||||||
title: `Certificate - ${certificate.course_title}`,
|
title: `Certificate - ${courseTitle}`,
|
||||||
text: shareText,
|
text: shareText,
|
||||||
url: shareUrl
|
url: shareUrl
|
||||||
})
|
})
|
||||||
@@ -329,6 +422,12 @@ export default function CertificatePage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ Get the final names to display
|
||||||
|
const studentName = getStudentName()
|
||||||
|
const instructorName = getInstructorName()
|
||||||
|
const courseTitle = getCourseTitle()
|
||||||
|
const completionDate = getCompletionDate()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-100 py-12 px-4">
|
<div className="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-100 py-12 px-4">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
@@ -359,15 +458,18 @@ export default function CertificatePage() {
|
|||||||
<p className="text-xl text-gray-600 mb-8">This is to certify that</p>
|
<p className="text-xl text-gray-600 mb-8">This is to certify that</p>
|
||||||
|
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
|
{/* ✅ FIXED: Using helper function for guaranteed name display */}
|
||||||
<div className="text-6xl font-bold text-gray-900 mb-4 border-t-4 border-b-4 border-indigo-300 py-6 capitalize font-serif">
|
<div className="text-6xl font-bold text-gray-900 mb-4 border-t-4 border-b-4 border-indigo-300 py-6 capitalize font-serif">
|
||||||
{certificate.user_name}
|
{studentName}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500">Student</p>
|
<p className="text-sm text-gray-500">Student</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xl text-gray-600 mb-4">has successfully completed the course</p>
|
<p className="text-xl text-gray-600 mb-4">has successfully completed the course</p>
|
||||||
|
|
||||||
|
{/* ✅ FIXED: Using helper function for course title */}
|
||||||
<h3 className="text-3xl font-semibold text-gray-900 mb-8 italic font-serif">
|
<h3 className="text-3xl font-semibold text-gray-900 mb-8 italic font-serif">
|
||||||
"{certificate.course_title}"
|
"{courseTitle}"
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 my-8 p-6 bg-indigo-50 rounded-xl">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 my-8 p-6 bg-indigo-50 rounded-xl">
|
||||||
@@ -375,7 +477,7 @@ export default function CertificatePage() {
|
|||||||
<Calendar className="w-8 h-8 text-indigo-600 mx-auto mb-2" />
|
<Calendar className="w-8 h-8 text-indigo-600 mx-auto mb-2" />
|
||||||
<p className="text-sm text-gray-600">Completion Date</p>
|
<p className="text-sm text-gray-600">Completion Date</p>
|
||||||
<p className="font-semibold text-gray-900">
|
<p className="font-semibold text-gray-900">
|
||||||
{new Date(certificate.completion_date).toLocaleDateString('en-US', {
|
{completionDate.toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric'
|
day: 'numeric'
|
||||||
@@ -394,7 +496,9 @@ export default function CertificatePage() {
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<User className="w-8 h-8 text-indigo-600 mx-auto mb-2" />
|
<User className="w-8 h-8 text-indigo-600 mx-auto mb-2" />
|
||||||
<p className="text-sm text-gray-600">Views</p>
|
<p className="text-sm text-gray-600">Views</p>
|
||||||
<p className="font-semibold text-gray-900">{certificate.view_count}</p>
|
<p className="font-semibold text-gray-900">
|
||||||
|
{certificate.view_count || certificate.viewCount || 0}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -402,8 +506,9 @@ export default function CertificatePage() {
|
|||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="w-48 h-0.5 bg-gray-400 mb-3 mx-auto"></div>
|
<div className="w-48 h-0.5 bg-gray-400 mb-3 mx-auto"></div>
|
||||||
|
{/* ✅ FIXED: Using helper function for instructor name */}
|
||||||
<p className="text-xl font-semibold text-gray-700">
|
<p className="text-xl font-semibold text-gray-700">
|
||||||
{certificate.mentor_name}
|
{instructorName}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500">Course Instructor</p>
|
<p className="text-sm text-gray-500">Course Instructor</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -412,13 +517,13 @@ export default function CertificatePage() {
|
|||||||
|
|
||||||
<div className="mt-8 pt-6 border-t border-gray-200">
|
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
<strong>{certificate.issued_by}</strong><br/>
|
<strong>{certificate.issued_by || certificate.issuedBy || 'OpenLearnX'}</strong><br/>
|
||||||
Digital Certificate of Achievement<br/>
|
Digital Certificate of Achievement<br/>
|
||||||
<span className="text-purple-600">🔒 Blockchain Verified</span>
|
<span className="text-purple-600">🔒 Blockchain Verified</span>
|
||||||
</p>
|
</p>
|
||||||
{certificate.blockchain_hash && (
|
{(certificate.blockchain_hash || certificate.blockchainHash) && (
|
||||||
<p className="text-xs text-gray-400 mt-2 font-mono break-all">
|
<p className="text-xs text-gray-400 mt-2 font-mono break-all">
|
||||||
Blockchain Hash: {certificate.blockchain_hash}
|
Blockchain Hash: {certificate.blockchain_hash || certificate.blockchainHash}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -447,6 +552,20 @@ export default function CertificatePage() {
|
|||||||
<p>This certificate can be verified at any time using the certificate ID above.</p>
|
<p>This certificate can be verified at any time using the certificate ID above.</p>
|
||||||
<p className="mt-2">Powered by OpenLearnX • Secured by Blockchain Technology</p>
|
<p className="mt-2">Powered by OpenLearnX • Secured by Blockchain Technology</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ✅ DEBUG: Show raw certificate data in development */}
|
||||||
|
{process.env.NODE_ENV === 'development' && (
|
||||||
|
<div className="mt-8 p-4 bg-gray-100 rounded-lg">
|
||||||
|
<details>
|
||||||
|
<summary className="text-sm font-medium text-gray-700 cursor-pointer">
|
||||||
|
Debug: Raw Certificate Data
|
||||||
|
</summary>
|
||||||
|
<pre className="mt-2 text-xs text-gray-600 whitespace-pre-wrap">
|
||||||
|
{JSON.stringify(certificate, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface Certificate {
|
|||||||
course_title: string
|
course_title: string
|
||||||
mentor_name: string
|
mentor_name: string
|
||||||
completion_date: string
|
completion_date: string
|
||||||
wallet_id?: string
|
wallet_address?: string
|
||||||
verification_url?: string
|
verification_url?: string
|
||||||
share_code?: string
|
share_code?: string
|
||||||
public_url?: string
|
public_url?: string
|
||||||
@@ -84,7 +84,7 @@ export function CertificateModal({
|
|||||||
course_title: certificateData.course_title,
|
course_title: certificateData.course_title,
|
||||||
mentor_name: certificateData.mentor_name,
|
mentor_name: certificateData.mentor_name,
|
||||||
completion_date: certificateData.completion_date,
|
completion_date: certificateData.completion_date,
|
||||||
wallet_id: walletId,
|
wallet_address: walletId,
|
||||||
verification_url: certificateData.verification_url,
|
verification_url: certificateData.verification_url,
|
||||||
share_code: certificateData.share_code,
|
share_code: certificateData.share_code,
|
||||||
public_url: certificateData.public_url,
|
public_url: certificateData.public_url,
|
||||||
@@ -158,13 +158,6 @@ export function CertificateModal({
|
|||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 18px;
|
|
||||||
color: #6b7280;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.student-name {
|
.student-name {
|
||||||
font-family: 'Playfair Display', serif;
|
font-family: 'Playfair Display', serif;
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
@@ -232,28 +225,23 @@ export function CertificateModal({
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trophy {
|
|
||||||
font-size: 60px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="certificate">
|
<div class="certificate">
|
||||||
<div class="trophy">🏆</div>
|
<div style="font-size: 60px; margin-bottom: 20px;">🏆</div>
|
||||||
<h1 class="title">CERTIFICATE OF COMPLETION</h1>
|
<h1 class="title">CERTIFICATE OF COMPLETION</h1>
|
||||||
|
|
||||||
<div class="subtitle">This is to certify that</div>
|
<div style="font-size: 18px; color: #6b7280; margin-bottom: 30px;">This is to certify that</div>
|
||||||
|
|
||||||
<div class="student-name">${certificate.user_name}</div>
|
<div class="student-name">${certificate.user_name}</div>
|
||||||
|
|
||||||
<div class="wallet-container">
|
<div class="wallet-container">
|
||||||
<div style="font-size: 14px; color: #374151; margin-bottom: 8px; font-weight: 600;">Blockchain Wallet Address</div>
|
<div style="font-size: 14px; color: #374151; margin-bottom: 8px; font-weight: 600;">Blockchain Wallet Address</div>
|
||||||
<div class="wallet-address">${certificate.wallet_id}</div>
|
<div class="wallet-address">${certificate.wallet_address}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="subtitle">has successfully completed the course</div>
|
<div style="font-size: 18px; color: #6b7280; margin-bottom: 20px;">has successfully completed the course</div>
|
||||||
<div class="course-title">"${certificate.course_title}"</div>
|
<div class="course-title">"${certificate.course_title}"</div>
|
||||||
|
|
||||||
<div class="date">✅ Completed on: ${new Date(certificate.completion_date).toLocaleDateString('en-US', {
|
<div class="date">✅ Completed on: ${new Date(certificate.completion_date).toLocaleDateString('en-US', {
|
||||||
@@ -290,7 +278,7 @@ export function CertificateModal({
|
|||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success("Certificate PDF download initiated! Use your browser's print dialog to save as PDF.")
|
toast.success("Certificate PDF download initiated!")
|
||||||
} else {
|
} else {
|
||||||
toast.error("Popup blocked. Please allow popups and try again.")
|
toast.error("Popup blocked. Please allow popups and try again.")
|
||||||
}
|
}
|
||||||
@@ -506,7 +494,7 @@ export function CertificateModal({
|
|||||||
<p className="text-sm text-gray-500 mb-2">Blockchain Wallet Address:</p>
|
<p className="text-sm text-gray-500 mb-2">Blockchain Wallet Address:</p>
|
||||||
<div className="bg-purple-100 border-2 border-dashed border-purple-300 rounded-lg p-3 mx-auto max-w-md">
|
<div className="bg-purple-100 border-2 border-dashed border-purple-300 rounded-lg p-3 mx-auto max-w-md">
|
||||||
<p className="text-purple-700 font-mono text-sm break-all">
|
<p className="text-purple-700 font-mono text-sm break-all">
|
||||||
{certificate.wallet_id}
|
{certificate.wallet_address}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user