dashboard & cource certificate Block chain

This commit is contained in:
5t4l1n
2025-07-29 20:43:52 +05:30
parent 6376105b1d
commit a3520a3d67
6 changed files with 1224 additions and 1593 deletions
File diff suppressed because it is too large Load Diff
-472
View File
@@ -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