This commit is contained in:
5t4l1n
2025-07-25 11:10:44 +05:30
parent 4455b39267
commit 7e6f0d0b1e
32 changed files with 2093 additions and 15 deletions
+99
View File
@@ -0,0 +1,99 @@
from flask import Blueprint, request, jsonify, current_app
import jwt
from datetime import datetime, timedelta
import uuid
bp = Blueprint('auth', __name__)
@bp.route('/nonce', methods=['POST'])
async def get_nonce():
"""Generate nonce for wallet signature"""
data = request.get_json()
wallet_address = data.get('wallet_address')
if not wallet_address:
return jsonify({"error": "Wallet address required"}), 400
web3_service = current_app.config['WEB3_SERVICE']
nonce = web3_service.generate_nonce()
# Store nonce temporarily (in production, use Redis)
message = f"Sign this message to authenticate with OpenLearnX: {nonce}"
return jsonify({
"nonce": nonce,
"message": message
})
@bp.route('/verify', methods=['POST'])
async def verify_signature():
"""Verify MetaMask signature and create session"""
data = request.get_json()
wallet_address = data.get('wallet_address')
signature = data.get('signature')
message = data.get('message')
if not all([wallet_address, signature, message]):
return jsonify({"error": "Missing required fields"}), 400
web3_service = current_app.config['WEB3_SERVICE']
mongo_service = current_app.config['MONGO_SERVICE']
# Verify signature
if not web3_service.verify_signature(wallet_address, message, signature):
return jsonify({"error": "Invalid signature"}), 401
# Create or get user
user = await mongo_service.create_user(wallet_address)
await mongo_service.update_user_login(wallet_address)
# Create JWT token
token_payload = {
'user_id': str(user['_id']),
'wallet_address': wallet_address,
'exp': datetime.utcnow() + timedelta(days=7)
}
token = jwt.encode(
token_payload,
current_app.config['SECRET_KEY'],
algorithm='HS256'
)
return jsonify({
"success": True,
"token": token,
"user": {
"id": str(user['_id']),
"wallet_address": user['wallet_address'],
"created_at": user['created_at'].isoformat(),
"total_tests": user.get('total_tests', 0),
"certificates": len(user.get('certificates', []))
}
})
@bp.route('/profile', methods=['GET'])
async def get_profile():
"""Get user profile"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return jsonify({"error": "Token required"}), 401
try:
payload = jwt.decode(
token,
current_app.config['SECRET_KEY'],
algorithms=['HS256']
)
user_id = payload['user_id']
mongo_service = current_app.config['MONGO_SERVICE']
analytics = await mongo_service.get_user_analytics(user_id)
return jsonify(analytics)
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "Invalid token"}), 401
+181
View File
@@ -0,0 +1,181 @@
from flask import Blueprint, request, jsonify, current_app
import jwt
import json
import uuid
from datetime import datetime
bp = Blueprint('certificate', __name__)
def get_user_from_token(token):
"""Extract user from JWT token"""
try:
payload = jwt.decode(
token,
current_app.config['SECRET_KEY'],
algorithms=['HS256']
)
return payload['user_id'], payload['wallet_address']
except:
return None, None
@bp.route('/mint', methods=['POST'])
async def mint_certificate():
"""Mint NFT certificate for completed test"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
user_id, wallet_address = get_user_from_token(token)
if not user_id:
return jsonify({"error": "Authentication required"}), 401
data = request.get_json()
session_id = data.get('session_id')
mongo_service = current_app.config['MONGO_SERVICE']
web3_service = current_app.config['WEB3_SERVICE']
# Get completed session
session = await mongo_service.get_test_session(session_id)
if not session or not session.get('completed'):
return jsonify({"error": "Test session not completed"}), 400
if session['user_id'] != user_id:
return jsonify({"error": "Unauthorized"}), 403
# Check if certificate already minted for this session
existing_cert = await mongo_service.certificates.find_one({"session_id": session_id})
if existing_cert:
return jsonify({"error": "Certificate already minted"}), 400
# Create certificate metadata
certificate_metadata = {
"name": f"OpenLearnX Certificate - {session['subject']}",
"description": f"Certificate of completion for {session['subject']} assessment",
"image": f"https://certificates.openlearnx.com/{session_id}.png",
"attributes": [
{"trait_type": "Subject", "value": session['subject']},
{"trait_type": "Score", "value": f"{session['score']:.1%}"},
{"trait_type": "Date", "value": session['created_at'].strftime("%Y-%m-%d")},
{"trait_type": "Questions", "value": len(session.get('answers', []))},
{"trait_type": "Difficulty", "value": session.get('current_difficulty', 2)}
],
"certificate_data": {
"student_wallet": wallet_address,
"subject": session['subject'],
"score": session['score'],
"completion_date": session.get('completed_at', datetime.utcnow()).isoformat(),
"questions_answered": len(session.get('answers', [])),
"session_id": session_id
}
}
# Upload to IPFS (simplified - in production use proper IPFS service)
ipfs_hash = f"Qm{uuid.uuid4().hex[:40]}" # Mock IPFS hash
token_uri = f"https://ipfs.io/ipfs/{ipfs_hash}"
try:
# Mint NFT (requires private key for the minting account)
private_key = current_app.config.get('MINTER_PRIVATE_KEY')
if not private_key:
return jsonify({"error": "Minting not configured"}), 500
tx_hash = web3_service.mint_certificate(
wallet_address,
token_uri,
private_key
)
if not tx_hash:
return jsonify({"error": "Minting failed"}), 500
# Get token ID from transaction (simplified)
token_id = await mongo_service.certificates.count_documents({}) + 1
# Save certificate record
cert_record = await mongo_service.create_certificate_record(
user_id=user_id,
token_id=token_id,
tx_hash=tx_hash,
ipfs_hash=ipfs_hash,
subject=session['subject'],
score=session['score']
)
# Update session with certificate info
await mongo_service.update_test_session(session_id, {
'certificate_minted': True,
'certificate_token_id': token_id,
'certificate_tx_hash': tx_hash
})
return jsonify({
"success": True,
"certificate": {
"token_id": token_id,
"transaction_hash": tx_hash,
"ipfs_hash": ipfs_hash,
"token_uri": token_uri,
"metadata": certificate_metadata
}
})
except Exception as e:
return jsonify({"error": f"Minting failed: {str(e)}"}), 500
@bp.route('/verify/<int:token_id>', methods=['GET'])
async def verify_certificate(token_id):
"""Verify certificate by token ID"""
web3_service = current_app.config['WEB3_SERVICE']
mongo_service = current_app.config['MONGO_SERVICE']
# Get certificate from blockchain
cert_details = web3_service.get_certificate_details(token_id)
if not cert_details:
return jsonify({"error": "Certificate not found"}), 404
# Get additional details from database
db_cert = await mongo_service.certificates.find_one({"token_id": token_id})
response = {
"valid": True,
"token_id": token_id,
"owner": cert_details['owner'],
"token_uri": cert_details['token_uri']
}
if db_cert:
response.update({
"subject": db_cert['subject'],
"score": db_cert['score'],
"issue_date": db_cert['created_at'].isoformat(),
"transaction_hash": db_cert['transaction_hash']
})
return jsonify(response)
@bp.route('/user/<user_id>', methods=['GET'])
async def get_user_certificates(user_id):
"""Get all certificates for a user"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
token_user_id, _ = get_user_from_token(token)
if not token_user_id or token_user_id != user_id:
return jsonify({"error": "Unauthorized"}), 403
mongo_service = current_app.config['MONGO_SERVICE']
certificates = await mongo_service.get_user_certificates(user_id)
formatted_certs = []
for cert in certificates:
formatted_certs.append({
"id": str(cert['_id']),
"token_id": cert['token_id'],
"subject": cert['subject'],
"score": cert['score'],
"created_at": cert['created_at'].isoformat(),
"transaction_hash": cert['transaction_hash'],
"verified": cert.get('verified', True)
})
return jsonify({"certificates": formatted_certs})
+211
View File
@@ -0,0 +1,211 @@
from flask import Blueprint, request, jsonify, current_app
import jwt
from datetime import datetime, timedelta
bp = Blueprint('dashboard', __name__)
def get_user_from_token(token):
"""Extract user from JWT token"""
try:
payload = jwt.decode(
token,
current_app.config['SECRET_KEY'],
algorithms=['HS256']
)
return payload['user_id']
except:
return None
@bp.route('/student/<user_id>', methods=['GET'])
async def get_student_dashboard(user_id):
"""Get comprehensive student dashboard"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
token_user_id = get_user_from_token(token)
if not token_user_id or token_user_id != user_id:
return jsonify({"error": "Unauthorized"}), 403
mongo_service = current_app.config['MONGO_SERVICE']
analytics = await mongo_service.get_user_analytics(user_id)
if not analytics:
return jsonify({"error": "User not found"}), 404
# Get recent activity
recent_sessions = await mongo_service.test_sessions.find({
"user_id": user_id
}).sort("created_at", -1).limit(5).to_list(length=5)
# Get certificates
certificates = await mongo_service.get_user_certificates(user_id)
# Calculate streaks and progress
today = datetime.utcnow().date()
week_ago = today - timedelta(days=7)
month_ago = today - timedelta(days=30)
week_sessions = [s for s in recent_sessions
if s['created_at'].date() >= week_ago]
month_sessions = [s for s in recent_sessions
if s['created_at'].date() >= month_ago]
dashboard_data = {
"user_info": {
"id": str(analytics['user']['_id']),
"wallet_address": analytics['user']['wallet_address'],
"member_since": analytics['user']['created_at'].isoformat(),
"last_login": analytics['user']['last_login'].isoformat()
},
"overview": {
"total_tests": analytics['total_tests'],
"completed_tests": analytics['completed_tests'],
"average_score": round(analytics['average_score'] * 100, 1),
"certificates_earned": analytics['certificates_earned'],
"this_week_tests": len(week_sessions),
"this_month_tests": len(month_sessions)
},
"subject_breakdown": {
subject: {
"tests_taken": data['tests'],
"average_score": round(data['avg_score'] * 100, 1),
"mastery_level": get_mastery_level(data['avg_score'])
}
for subject, data in analytics['subject_breakdown'].items()
},
"recent_activity": [
{
"id": str(session['_id']),
"subject": session['subject'],
"score": round(session.get('score', 0) * 100, 1),
"completed": session.get('completed', False),
"date": session['created_at'].isoformat(),
"questions_answered": len(session.get('answers', []))
}
for session in recent_sessions
],
"certificates": [
{
"id": str(cert['_id']),
"token_id": cert['token_id'],
"subject": cert['subject'],
"score": round(cert['score'] * 100, 1),
"earned_date": cert['created_at'].isoformat(),
"blockchain_verified": cert.get('verified', True)
}
for cert in certificates
],
"progress_chart": await get_progress_chart_data(mongo_service, user_id),
"competency_radar": get_competency_radar_data(analytics['subject_breakdown'])
}
return jsonify(dashboard_data)
@bp.route('/instructor/overview', methods=['GET'])
async def get_instructor_dashboard():
"""Get instructor dashboard with class overview"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
user_id = get_user_from_token(token)
if not user_id:
return jsonify({"error": "Unauthorized"}), 403
mongo_service = current_app.config['MONGO_SERVICE']
# Get overall platform statistics
total_users = await mongo_service.users.count_documents({})
total_tests = await mongo_service.test_sessions.count_documents({})
total_certificates = await mongo_service.certificates.count_documents({})
# Get recent activity across all users
recent_sessions = await mongo_service.test_sessions.find({}).sort(
"created_at", -1
).limit(20).to_list(length=20)
# Calculate subject popularity
subject_stats = {}
for session in recent_sessions:
subject = session.get('subject', 'Unknown')
if subject not in subject_stats:
subject_stats[subject] = {'count': 0, 'total_score': 0}
subject_stats[subject]['count'] += 1
subject_stats[subject]['total_score'] += session.get('score', 0)
for subject in subject_stats:
subject_stats[subject]['avg_score'] = (
subject_stats[subject]['total_score'] / subject_stats[subject]['count']
)
dashboard_data = {
"platform_overview": {
"total_users": total_users,
"total_tests": total_tests,
"total_certificates": total_certificates,
"active_users_today": len([s for s in recent_sessions
if s['created_at'].date() == datetime.utcnow().date()])
},
"subject_performance": {
subject: {
"total_attempts": data['count'],
"average_score": round(data['avg_score'] * 100, 1),
"difficulty_trend": "increasing" if data['avg_score'] > 0.7 else "stable"
}
for subject, data in subject_stats.items()
},
"recent_activity": [
{
"user_id": session['user_id'],
"subject": session['subject'],
"score": round(session.get('score', 0) * 100, 1),
"completed": session.get('completed', False),
"timestamp": session['created_at'].isoformat()
}
for session in recent_sessions[:10]
]
}
return jsonify(dashboard_data)
def get_mastery_level(score):
"""Determine mastery level based on score"""
if score >= 0.9:
return "Expert"
elif score >= 0.8:
return "Advanced"
elif score >= 0.7:
return "Proficient"
elif score >= 0.6:
return "Developing"
else:
return "Beginner"
async def get_progress_chart_data(mongo_service, user_id):
"""Get progress chart data for the last 30 days"""
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
sessions = await mongo_service.test_sessions.find({
"user_id": user_id,
"created_at": {"$gte": thirty_days_ago},
"completed": True
}).sort("created_at", 1).to_list(length=None)
progress_data = []
for session in sessions:
progress_data.append({
"date": session['created_at'].strftime("%Y-%m-%d"),
"score": round(session.get('score', 0) * 100, 1),
"subject": session['subject']
})
return progress_data
def get_competency_radar_data(subject_breakdown):
"""Generate radar chart data for competencies"""
radar_data = []
for subject, data in subject_breakdown.items():
radar_data.append({
"subject": subject,
"score": round(data['avg_score'] * 100, 1),
"tests": data['tests']
})
return radar_data
+201
View File
@@ -0,0 +1,201 @@
from flask import Blueprint, request, jsonify, current_app
import jwt
from datetime import datetime
import random
bp = Blueprint('test', __name__)
def get_user_from_token(token):
"""Extract user from JWT token"""
try:
payload = jwt.decode(
token,
current_app.config['SECRET_KEY'],
algorithms=['HS256']
)
return payload['user_id']
except:
return None
@bp.route('/start', methods=['POST'])
async def start_test():
"""Start a new test session"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
user_id = get_user_from_token(token)
if not user_id:
return jsonify({"error": "Authentication required"}), 401
data = request.get_json()
subject = data.get('subject', 'General')
mongo_service = current_app.config['MONGO_SERVICE']
# Create test session
session = await mongo_service.create_test_session(user_id, subject)
# Get first question
questions = await mongo_service.get_questions_by_difficulty(2, 1) # Start with medium
if not questions:
return jsonify({"error": "No questions available"}), 404
question = questions[0]
session['questions'].append(str(question['_id']))
await mongo_service.update_test_session(str(session['_id']), {
'questions': session['questions'],
'current_question': 0
})
return jsonify({
"session_id": str(session['_id']),
"question": {
"id": str(question['_id']),
"question": question['question'],
"options": question['options'],
"subject": question['subject'],
"difficulty": question['difficulty']
},
"question_number": 1,
"total_questions": 10
})
@bp.route('/answer', methods=['POST'])
async def submit_answer():
"""Submit answer and get feedback"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
user_id = get_user_from_token(token)
if not user_id:
return jsonify({"error": "Authentication required"}), 401
data = request.get_json()
session_id = data.get('session_id')
question_id = data.get('question_id')
answer = data.get('answer')
mongo_service = current_app.config['MONGO_SERVICE']
# Get session and question
session = await mongo_service.get_test_session(session_id)
question = await mongo_service.questions.find_one({"_id": question_id})
if not session or not question:
return jsonify({"error": "Invalid session or question"}), 404
# Check answer
is_correct = answer == question['correct_answer']
confidence_score = random.uniform(0.7, 0.95) if is_correct else random.uniform(0.1, 0.4)
# Update session
if 'answers' not in session:
session['answers'] = []
answer_record = {
'question_id': question_id,
'answer': answer,
'correct': is_correct,
'timestamp': datetime.utcnow()
}
session['answers'].append(answer_record)
# Calculate current score
correct_answers = sum(1 for a in session['answers'] if a['correct'])
current_score = correct_answers / len(session['answers'])
# Update difficulty for next question
current_difficulty = session.get('current_difficulty', 2)
if is_correct and confidence_score > 0.8:
current_difficulty = min(5, current_difficulty + 1)
elif not is_correct and confidence_score < 0.3:
current_difficulty = max(1, current_difficulty - 1)
await mongo_service.update_test_session(session_id, {
'answers': session['answers'],
'score': current_score,
'current_difficulty': current_difficulty
})
# Prepare response
feedback = {
"correct": is_correct,
"confidence_score": round(confidence_score, 2),
"explanation": question['explanation'],
"correct_answer": question['options'][question['correct_answer']],
"current_score": round(current_score * 100, 1),
"total_answered": len(session['answers'])
}
# Get next question if test not complete
next_question = None
if len(session['answers']) < 10: # 10 questions per test
questions = await mongo_service.get_questions_by_difficulty(current_difficulty, 1)
if questions:
next_q = questions[0]
session['questions'].append(str(next_q['_id']))
await mongo_service.update_test_session(session_id, {
'questions': session['questions']
})
next_question = {
"id": str(next_q['_id']),
"question": next_q['question'],
"options": next_q['options'],
"subject": next_q['subject'],
"difficulty": next_q['difficulty']
}
else:
# Test completed
await mongo_service.update_test_session(session_id, {
'completed': True,
'completed_at': datetime.utcnow()
})
# Update user stats
await mongo_service.users.update_one(
{"_id": user_id},
{
"$inc": {"total_tests": 1, "total_score": current_score},
"$set": {f"competency_scores.{session['subject']}": current_score}
}
)
response = {
"feedback": feedback,
"test_completed": len(session['answers']) >= 10
}
if next_question:
response['next_question'] = next_question
response['question_number'] = len(session['answers']) + 1
return jsonify(response)
@bp.route('/sessions/<user_id>', methods=['GET'])
async def get_user_sessions(user_id):
"""Get user's test sessions"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
token_user_id = get_user_from_token(token)
if not token_user_id or token_user_id != user_id:
return jsonify({"error": "Unauthorized"}), 403
mongo_service = current_app.config['MONGO_SERVICE']
sessions = await mongo_service.test_sessions.find(
{"user_id": user_id}
).sort("created_at", -1).limit(20).to_list(length=20)
# Format sessions for response
formatted_sessions = []
for session in sessions:
formatted_sessions.append({
"id": str(session['_id']),
"subject": session['subject'],
"score": session.get('score', 0),
"completed": session.get('completed', False),
"questions_answered": len(session.get('answers', [])),
"created_at": session['created_at'].isoformat()
})
return jsonify({"sessions": formatted_sessions})