mirror of
https://github.com/th30d4y/OpenLearnX.git
synced 2026-05-26 11:25:49 +00:00
feat: unify real activity tracking, admin monitoring, and error UX
This commit is contained in:
+941
-2
File diff suppressed because it is too large
Load Diff
+619
-1
@@ -7,6 +7,7 @@ import jwt
|
||||
import logging
|
||||
from eth_account.messages import encode_defunct
|
||||
from web3 import Web3
|
||||
from activity_logger import log_user_activity
|
||||
|
||||
bp = Blueprint('auth', __name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -131,6 +132,8 @@ def verify_signature():
|
||||
# Create new user
|
||||
user = {
|
||||
"wallet_address": wallet_address.lower(),
|
||||
"role": "student",
|
||||
"status": "active",
|
||||
"created_at": datetime.now(),
|
||||
"last_login": datetime.now(),
|
||||
"login_count": 1
|
||||
@@ -138,7 +141,31 @@ def verify_signature():
|
||||
result = db.users.insert_one(user)
|
||||
user["_id"] = str(result.inserted_id)
|
||||
logger.info(f"✅ Created new user: {wallet_address}")
|
||||
log_user_activity(
|
||||
db,
|
||||
wallet_address.lower(),
|
||||
"auth_register",
|
||||
"Account registered",
|
||||
"Created account via wallet authentication",
|
||||
{"auth_method": "wallet"},
|
||||
)
|
||||
else:
|
||||
account_status = str(user.get("status", "active")).lower().strip()
|
||||
if account_status == "banned":
|
||||
logger.warning(f"⛔ Banned wallet login blocked: {wallet_address}")
|
||||
log_user_activity(
|
||||
db,
|
||||
wallet_address.lower(),
|
||||
"account_status",
|
||||
"Login blocked",
|
||||
"Login blocked because account is banned",
|
||||
{"status": "banned"},
|
||||
)
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Your account is banned. Contact admin."
|
||||
}), 403
|
||||
|
||||
# Update existing user
|
||||
db.users.update_one(
|
||||
{"wallet_address": wallet_address.lower()},
|
||||
@@ -149,6 +176,15 @@ def verify_signature():
|
||||
)
|
||||
user["_id"] = str(user["_id"])
|
||||
logger.info(f"✅ Updated existing user: {wallet_address}")
|
||||
|
||||
log_user_activity(
|
||||
db,
|
||||
wallet_address.lower(),
|
||||
"auth_login",
|
||||
"Login successful",
|
||||
"Wallet login completed successfully",
|
||||
{"auth_method": "wallet"},
|
||||
)
|
||||
|
||||
# Generate JWT token
|
||||
token_payload = {
|
||||
@@ -164,6 +200,12 @@ def verify_signature():
|
||||
user_response = {
|
||||
"id": user["wallet_address"],
|
||||
"wallet_address": user["wallet_address"],
|
||||
"email": user.get("email", ""),
|
||||
"name": user.get("name", ""),
|
||||
"bio": user.get("bio", ""),
|
||||
"avatar": user.get("avatar", ""),
|
||||
"role": user.get("role", "student"),
|
||||
"status": user.get("status", "active"),
|
||||
"created_at": user["created_at"].isoformat() if isinstance(user["created_at"], datetime) else str(user["created_at"]),
|
||||
"last_login": user["last_login"].isoformat() if isinstance(user["last_login"], datetime) else str(user["last_login"])
|
||||
}
|
||||
@@ -182,4 +224,580 @@ def verify_signature():
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
}), 500
|
||||
|
||||
@bp.route('/register', methods=['POST', 'OPTIONS'])
|
||||
def register():
|
||||
"""Register a new user with email and password"""
|
||||
if request.method == "OPTIONS":
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
email = data.get('email', '').strip().lower()
|
||||
password = data.get('password', '')
|
||||
username = data.get('username', '').strip()
|
||||
|
||||
if not email or not password:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Email and password are required"
|
||||
}), 400
|
||||
|
||||
if len(password) < 6:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Password must be at least 6 characters"
|
||||
}), 400
|
||||
|
||||
# Check if user already exists
|
||||
existing_user = db.users.find_one({"email": email})
|
||||
if existing_user:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Email already registered"
|
||||
}), 409
|
||||
|
||||
# Hash password using simple approach for development
|
||||
# TODO: Use werkzeug.security.generate_password_hash for production
|
||||
import hashlib
|
||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
# Create new user
|
||||
user = {
|
||||
"email": email,
|
||||
"username": username or email.split("@")[0],
|
||||
"password_hash": password_hash,
|
||||
"name": "",
|
||||
"bio": "",
|
||||
"avatar": "",
|
||||
"role": "student",
|
||||
"status": "active",
|
||||
"created_at": datetime.now(),
|
||||
"last_login": datetime.now(),
|
||||
"login_count": 1,
|
||||
"auth_method": "email"
|
||||
}
|
||||
|
||||
result = db.users.insert_one(user)
|
||||
user["_id"] = str(result.inserted_id)
|
||||
|
||||
# Generate JWT token
|
||||
token_payload = {
|
||||
"user_id": str(result.inserted_id),
|
||||
"email": email,
|
||||
"iat": datetime.utcnow(),
|
||||
"exp": datetime.utcnow() + timedelta(days=7)
|
||||
}
|
||||
|
||||
token = jwt.encode(token_payload, JWT_SECRET, algorithm="HS256")
|
||||
|
||||
user_response = {
|
||||
"id": str(result.inserted_id),
|
||||
"email": email,
|
||||
"username": username or email.split("@")[0],
|
||||
"name": "",
|
||||
"bio": "",
|
||||
"avatar": "",
|
||||
"role": "student",
|
||||
"status": "active",
|
||||
"created_at": user["created_at"].isoformat(),
|
||||
"last_login": user["last_login"].isoformat()
|
||||
}
|
||||
|
||||
log_user_activity(
|
||||
db,
|
||||
str(result.inserted_id),
|
||||
"auth_register",
|
||||
"Account registered",
|
||||
"Created account with email and password",
|
||||
{"auth_method": "email"},
|
||||
)
|
||||
|
||||
logger.info(f"✅ New user registered: {email}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"token": token,
|
||||
"user": user_response,
|
||||
"message": "Registration successful"
|
||||
}), 201
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error during registration: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
|
||||
@bp.route('/login', methods=['POST', 'OPTIONS'])
|
||||
def login():
|
||||
"""Login with email and password"""
|
||||
if request.method == "OPTIONS":
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
email = data.get('email', '').strip().lower()
|
||||
password = data.get('password', '')
|
||||
|
||||
if not email or not password:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Email and password are required"
|
||||
}), 400
|
||||
|
||||
# Find user by email
|
||||
user = db.users.find_one({"email": email})
|
||||
if not user:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Invalid email or password"
|
||||
}), 401
|
||||
|
||||
account_status = str(user.get("status", "active")).lower().strip()
|
||||
if account_status == "banned":
|
||||
logger.warning(f"⛔ Banned email login blocked: {email}")
|
||||
log_user_activity(
|
||||
db,
|
||||
str(user.get("_id")),
|
||||
"account_status",
|
||||
"Login blocked",
|
||||
"Login blocked because account is banned",
|
||||
{"status": "banned", "email": email},
|
||||
)
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Your account is banned. Contact admin."
|
||||
}), 403
|
||||
|
||||
if account_status == "suspended":
|
||||
log_user_activity(
|
||||
db,
|
||||
str(user.get("_id")),
|
||||
"account_status",
|
||||
"Login attempted while suspended",
|
||||
"User logged in while account status is suspended",
|
||||
{"status": "suspended", "email": email},
|
||||
)
|
||||
|
||||
# Verify password
|
||||
import hashlib
|
||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
if password_hash != user.get('password_hash'):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Invalid email or password"
|
||||
}), 401
|
||||
|
||||
# Update last login
|
||||
db.users.update_one(
|
||||
{"_id": user["_id"]},
|
||||
{
|
||||
"$set": {"last_login": datetime.now()},
|
||||
"$inc": {"login_count": 1}
|
||||
}
|
||||
)
|
||||
|
||||
# Generate JWT token
|
||||
token_payload = {
|
||||
"user_id": str(user["_id"]),
|
||||
"email": email,
|
||||
"iat": datetime.utcnow(),
|
||||
"exp": datetime.utcnow() + timedelta(days=7)
|
||||
}
|
||||
|
||||
token = jwt.encode(token_payload, JWT_SECRET, algorithm="HS256")
|
||||
|
||||
user_response = {
|
||||
"id": str(user["_id"]),
|
||||
"email": email,
|
||||
"username": user.get('username', ''),
|
||||
"name": user.get('name', ''),
|
||||
"bio": user.get('bio', ''),
|
||||
"avatar": user.get('avatar', ''),
|
||||
"role": user.get('role', 'student'),
|
||||
"status": user.get('status', 'active'),
|
||||
"created_at": user["created_at"].isoformat() if isinstance(user["created_at"], datetime) else str(user["created_at"]),
|
||||
"last_login": user["last_login"].isoformat() if isinstance(user["last_login"], datetime) else str(user["last_login"])
|
||||
}
|
||||
|
||||
log_user_activity(
|
||||
db,
|
||||
str(user.get("_id")),
|
||||
"auth_login",
|
||||
"Login successful",
|
||||
"Email login completed successfully",
|
||||
{"auth_method": "email", "email": email},
|
||||
)
|
||||
|
||||
logger.info(f"✅ User logged in: {email}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"token": token,
|
||||
"user": user_response,
|
||||
"message": "Login successful"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error during login: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
|
||||
@bp.route('/profile/update', methods=['POST', 'OPTIONS'])
|
||||
def update_profile():
|
||||
"""Update user profile (name, bio, avatar)"""
|
||||
if request.method == "OPTIONS":
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
try:
|
||||
# Get token from header
|
||||
auth_header = request.headers.get('Authorization', '')
|
||||
if not auth_header.startswith('Bearer '):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Authorization header required"
|
||||
}), 401
|
||||
|
||||
token = auth_header.split('Bearer ')[1]
|
||||
|
||||
# Verify and decode token
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
||||
user_id = payload.get('user_id')
|
||||
except jwt.InvalidTokenError:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Invalid token"
|
||||
}), 401
|
||||
|
||||
data = request.get_json()
|
||||
name = data.get('name', '').strip()
|
||||
bio = data.get('bio', '').strip()
|
||||
avatar = data.get('avatar', '').strip()
|
||||
|
||||
# Update user profile
|
||||
from bson.objectid import ObjectId
|
||||
result = db.users.update_one(
|
||||
{"_id": ObjectId(user_id)},
|
||||
{
|
||||
"$set": {
|
||||
"name": name,
|
||||
"bio": bio,
|
||||
"avatar": avatar,
|
||||
"updated_at": datetime.now()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if result.matched_count == 0:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "User not found"
|
||||
}), 404
|
||||
|
||||
# Get updated user
|
||||
user = db.users.find_one({"_id": ObjectId(user_id)})
|
||||
|
||||
user_response = {
|
||||
"id": str(user["_id"]),
|
||||
"email": user.get('email', ''),
|
||||
"username": user.get('username', ''),
|
||||
"name": user.get('name', ''),
|
||||
"bio": user.get('bio', ''),
|
||||
"avatar": user.get('avatar', ''),
|
||||
"role": user.get('role', 'student'),
|
||||
"status": user.get('status', 'active'),
|
||||
"created_at": user["created_at"].isoformat() if isinstance(user["created_at"], datetime) else str(user["created_at"]),
|
||||
"last_login": user["last_login"].isoformat() if isinstance(user["last_login"], datetime) else str(user["last_login"])
|
||||
}
|
||||
|
||||
logger.info(f"✅ Profile updated for user: {user_id}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"user": user_response,
|
||||
"message": "Profile updated successfully"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error updating profile: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
@bp.route('/metamask/add-email', methods=['POST', 'OPTIONS'])
|
||||
def add_metamask_email():
|
||||
"""Store contact email for MetaMask wallet"""
|
||||
if request.method == "OPTIONS":
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
try:
|
||||
# Get token from header
|
||||
auth_header = request.headers.get('Authorization', '')
|
||||
if not auth_header.startswith('Bearer '):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Authorization header required"
|
||||
}), 401
|
||||
|
||||
token = auth_header.split('Bearer ')[1]
|
||||
|
||||
# Verify and decode token
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
||||
wallet_address = payload.get('wallet_address')
|
||||
if not wallet_address:
|
||||
wallet_address = payload.get('user_id')
|
||||
except jwt.InvalidTokenError:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Invalid token"
|
||||
}), 401
|
||||
|
||||
data = request.get_json()
|
||||
email = data.get('email', '').strip().lower()
|
||||
name = data.get('name', '').strip()
|
||||
|
||||
if not email:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Email is required"
|
||||
}), 400
|
||||
|
||||
# Validate email format
|
||||
import re
|
||||
if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Invalid email format"
|
||||
}), 400
|
||||
|
||||
# Check if email already used by different wallet
|
||||
existing_user = db.users.find_one({"email": email, "wallet_address": {"$ne": wallet_address.lower()}})
|
||||
if existing_user:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Email already associated with another wallet"
|
||||
}), 409
|
||||
|
||||
# Update user with email and name
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
# Try updating by wallet address first (for new users)
|
||||
result = db.users.update_one(
|
||||
{"wallet_address": wallet_address.lower()},
|
||||
{
|
||||
"$set": {
|
||||
"email": email,
|
||||
"name": name or "",
|
||||
"updated_at": datetime.now()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if result.matched_count == 0:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "User not found"
|
||||
}), 404
|
||||
|
||||
# Get updated user
|
||||
user = db.users.find_one({"wallet_address": wallet_address.lower()})
|
||||
|
||||
user_response = {
|
||||
"id": str(user.get("_id", wallet_address)),
|
||||
"wallet_address": user.get("wallet_address", wallet_address),
|
||||
"email": user.get("email", ""),
|
||||
"name": user.get("name", ""),
|
||||
"bio": user.get("bio", ""),
|
||||
"avatar": user.get("avatar", ""),
|
||||
"role": user.get("role", "student"),
|
||||
"status": user.get("status", "active"),
|
||||
"created_at": user.get("created_at", datetime.now()).isoformat() if isinstance(user.get("created_at"), datetime) else str(user.get("created_at", datetime.now())),
|
||||
"last_login": user.get("last_login", datetime.now()).isoformat() if isinstance(user.get("last_login"), datetime) else str(user.get("last_login", datetime.now()))
|
||||
}
|
||||
|
||||
logger.info(f"✅ Email added for MetaMask wallet: {wallet_address}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"user": user_response,
|
||||
"message": "Email saved successfully"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error saving MetaMask email: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@bp.route('/verify-token', methods=['POST', 'OPTIONS'])
|
||||
def verify_token():
|
||||
"""Validate JWT token and return the latest user payload."""
|
||||
if request.method == "OPTIONS":
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
try:
|
||||
auth_header = request.headers.get('Authorization', '')
|
||||
if not auth_header.startswith('Bearer '):
|
||||
return jsonify({"valid": False, "error": "Authorization header required"}), 401
|
||||
|
||||
token = auth_header.split('Bearer ')[1]
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
||||
except jwt.InvalidTokenError:
|
||||
return jsonify({"valid": False, "error": "Invalid token"}), 401
|
||||
|
||||
user = None
|
||||
wallet_address = payload.get('wallet_address')
|
||||
email = payload.get('email')
|
||||
user_id = payload.get('user_id')
|
||||
|
||||
if wallet_address:
|
||||
user = db.users.find_one({"wallet_address": str(wallet_address).lower()})
|
||||
elif email:
|
||||
user = db.users.find_one({"email": str(email).lower()})
|
||||
elif user_id:
|
||||
try:
|
||||
from bson.objectid import ObjectId
|
||||
user = db.users.find_one({"_id": ObjectId(user_id)})
|
||||
except Exception:
|
||||
user = None
|
||||
|
||||
if not user:
|
||||
return jsonify({"valid": False, "error": "User not found"}), 404
|
||||
|
||||
status = str(user.get("status", "active")).lower().strip()
|
||||
if status == "banned":
|
||||
return jsonify({"valid": False, "error": "Account is banned"}), 403
|
||||
|
||||
user_response = {
|
||||
"id": str(user.get("_id", user.get("wallet_address", ""))),
|
||||
"wallet_address": user.get("wallet_address", ""),
|
||||
"email": user.get("email", ""),
|
||||
"username": user.get("username", ""),
|
||||
"name": user.get("name", ""),
|
||||
"bio": user.get("bio", ""),
|
||||
"avatar": user.get("avatar", ""),
|
||||
"role": user.get("role", "student"),
|
||||
"status": user.get("status", "active"),
|
||||
"created_at": user.get("created_at", datetime.now()).isoformat() if isinstance(user.get("created_at"), datetime) else str(user.get("created_at", datetime.now())),
|
||||
"last_login": user.get("last_login", datetime.now()).isoformat() if isinstance(user.get("last_login"), datetime) else str(user.get("last_login", datetime.now())),
|
||||
}
|
||||
|
||||
return jsonify({"valid": True, "user": user_response})
|
||||
except Exception as e:
|
||||
logger.error(f"❌ verify-token error: {str(e)}")
|
||||
return jsonify({"valid": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/me', methods=['GET', 'OPTIONS'])
|
||||
def get_me():
|
||||
"""Return authenticated user profile for current token."""
|
||||
if request.method == "OPTIONS":
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
verify_resp = verify_token()
|
||||
try:
|
||||
body, status = verify_resp
|
||||
if status != 200:
|
||||
return body, status
|
||||
data = body.get_json()
|
||||
return jsonify({"success": True, "user": data.get("user", {})})
|
||||
except Exception:
|
||||
return verify_resp
|
||||
|
||||
@bp.route('/upload-image', methods=['POST', 'OPTIONS'])
|
||||
def upload_image():
|
||||
"""Upload and convert image (PNG/JPG only) to base64"""
|
||||
if request.method == "OPTIONS":
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
try:
|
||||
# Get token from header
|
||||
auth_header = request.headers.get('Authorization', '')
|
||||
if not auth_header.startswith('Bearer '):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Authorization header required"
|
||||
}), 401
|
||||
|
||||
token = auth_header.split('Bearer ')[1]
|
||||
|
||||
# Verify and decode token
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
||||
user_id = payload.get('user_id')
|
||||
except jwt.InvalidTokenError:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Invalid token"
|
||||
}), 401
|
||||
|
||||
# Check if file is in request
|
||||
if 'file' not in request.files:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "No file provided"
|
||||
}), 400
|
||||
|
||||
file = request.files['file']
|
||||
|
||||
if file.filename == '':
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "No file selected"
|
||||
}), 400
|
||||
|
||||
# Validate file type - only PNG and JPG
|
||||
allowed_extensions = {'png', 'jpg', 'jpeg'}
|
||||
file_ext = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else ''
|
||||
|
||||
if file_ext not in allowed_extensions:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Only PNG and JPG formats are allowed"
|
||||
}), 400
|
||||
|
||||
# Validate file size (max 5MB)
|
||||
file.seek(0, 2) # Seek to end
|
||||
file_size = file.tell()
|
||||
file.seek(0) # Seek back to start
|
||||
|
||||
max_size = 5 * 1024 * 1024 # 5MB
|
||||
if file_size > max_size:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "File size must be less than 5MB"
|
||||
}), 400
|
||||
|
||||
# Read file and convert to base64
|
||||
import base64
|
||||
file_data = file.read()
|
||||
base64_image = base64.b64encode(file_data).decode('utf-8')
|
||||
|
||||
# Create data URL for the image
|
||||
mime_type = f"image/{file_ext if file_ext != 'jpg' else 'jpeg'}"
|
||||
data_url = f"data:{mime_type};base64,{base64_image}"
|
||||
|
||||
logger.info(f"✅ Image uploaded for user: {user_id}, size: {file_size} bytes")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"image": data_url,
|
||||
"size": file_size,
|
||||
"message": "Image uploaded successfully"
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error uploading image: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
|
||||
@@ -8,9 +8,16 @@ import uuid
|
||||
from datetime import datetime
|
||||
import docker
|
||||
import psutil
|
||||
from pymongo import MongoClient
|
||||
from activity_logger import log_user_activity, resolve_user_identity
|
||||
|
||||
bp = Blueprint('coding', __name__)
|
||||
|
||||
# MongoDB connection
|
||||
mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/')
|
||||
client = MongoClient(mongo_uri)
|
||||
db = client.openlearnx
|
||||
|
||||
def secure_execution_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
@@ -34,6 +41,20 @@ def start_coding_session():
|
||||
session['start_time'] = datetime.now().isoformat()
|
||||
session['course_id'] = course_id
|
||||
session['lesson_id'] = lesson_id
|
||||
|
||||
identity = resolve_user_identity(request, db)
|
||||
log_user_activity(
|
||||
db,
|
||||
identity.get("user_id"),
|
||||
"coding",
|
||||
"Started coding session",
|
||||
"Entered secure coding session",
|
||||
{
|
||||
"session_id": session_id,
|
||||
"course_id": course_id,
|
||||
"lesson_id": lesson_id,
|
||||
},
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
@@ -92,6 +113,36 @@ def submit_coding_test():
|
||||
code,
|
||||
test_result
|
||||
)
|
||||
|
||||
identity = resolve_user_identity(request, db)
|
||||
resolved_user_id = identity.get("user_id")
|
||||
if resolved_user_id:
|
||||
db.user_submissions.insert_one({
|
||||
"user_id": resolved_user_id,
|
||||
"session_id": session.get('coding_session_id'),
|
||||
"course_id": session.get('course_id'),
|
||||
"problem_id": problem_id,
|
||||
"score": test_result.get('score', 0),
|
||||
"points_earned": int(test_result.get('score', 0)),
|
||||
"submitted_at": datetime.now(),
|
||||
"status": "submitted",
|
||||
})
|
||||
|
||||
log_user_activity(
|
||||
db,
|
||||
resolved_user_id,
|
||||
"coding",
|
||||
"Submitted coding solution",
|
||||
f"Submitted coding test for problem '{problem_id}'",
|
||||
{
|
||||
"submission_id": submission_id,
|
||||
"problem_id": problem_id,
|
||||
"score": test_result.get('score', 0),
|
||||
"passed": test_result.get('passed', 0),
|
||||
"total": test_result.get('total', 0),
|
||||
},
|
||||
points_earned=int(test_result.get('score', 0)),
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
@@ -184,11 +235,6 @@ def get_run_command(language, filename):
|
||||
|
||||
def log_coding_attempt(session_id, code, language):
|
||||
"""Log all coding attempts for monitoring"""
|
||||
from pymongo import MongoClient
|
||||
|
||||
client = MongoClient(os.getenv('MONGODB_URI', 'mongodb://localhost:27017/'))
|
||||
db = client.openlearnx
|
||||
|
||||
db.coding_logs.insert_one({
|
||||
"session_id": session_id,
|
||||
"code": code,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from flask import Blueprint, jsonify, current_app
|
||||
from flask import Blueprint, jsonify, current_app, request
|
||||
from pymongo import MongoClient
|
||||
import os
|
||||
from datetime import datetime
|
||||
from activity_logger import log_user_activity, resolve_user_identity
|
||||
|
||||
bp = Blueprint('courses', __name__)
|
||||
|
||||
@@ -68,6 +70,38 @@ def get_lesson(course_id, lesson_id):
|
||||
def mark_lesson_complete(course_id, lesson_id):
|
||||
"""Mark a lesson as completed for the user"""
|
||||
try:
|
||||
identity = resolve_user_identity(request, db)
|
||||
user_id = identity.get("user_id")
|
||||
|
||||
if user_id:
|
||||
course = db.courses.find_one({"id": course_id}, {"title": 1}) or {}
|
||||
lesson = db.lessons.find_one({"id": lesson_id, "course_id": course_id}, {"title": 1}) or {}
|
||||
|
||||
db.user_courses.update_one(
|
||||
{"user_id": user_id, "course_id": course_id},
|
||||
{
|
||||
"$set": {
|
||||
"user_id": user_id,
|
||||
"course_id": course_id,
|
||||
"last_activity_at": datetime.utcnow(),
|
||||
"completed_at": datetime.utcnow(),
|
||||
"completed": True,
|
||||
},
|
||||
"$addToSet": {"lessons_completed": lesson_id},
|
||||
},
|
||||
upsert=True,
|
||||
)
|
||||
|
||||
log_user_activity(
|
||||
db,
|
||||
user_id,
|
||||
"course",
|
||||
"Lesson completed",
|
||||
f"Completed lesson '{lesson.get('title', lesson_id)}' in course '{course.get('title', course_id)}'",
|
||||
{"course_id": course_id, "lesson_id": lesson_id},
|
||||
points_earned=10,
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"Lesson {lesson_id} marked as complete",
|
||||
@@ -76,6 +110,66 @@ def mark_lesson_complete(course_id, lesson_id):
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@bp.route("/<course_id>/activity", methods=["POST"])
|
||||
def log_course_activity(course_id):
|
||||
"""Log course interactions like view/start for real dashboard activity."""
|
||||
try:
|
||||
identity = resolve_user_identity(request, db)
|
||||
user_id = identity.get("user_id")
|
||||
if not user_id:
|
||||
return jsonify({"success": False, "error": "Authentication required"}), 401
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
action = str(data.get("action") or "view").strip().lower()
|
||||
lesson_id = str(data.get("lesson_id") or "").strip()
|
||||
|
||||
course = db.courses.find_one({"id": course_id}, {"title": 1}) or {}
|
||||
lesson_title = lesson_id
|
||||
if lesson_id:
|
||||
lesson = db.lessons.find_one({"id": lesson_id, "course_id": course_id}, {"title": 1}) or {}
|
||||
lesson_title = lesson.get("title", lesson_id)
|
||||
|
||||
if action == "start":
|
||||
title = "Course started"
|
||||
description = f"Started course '{course.get('title', course_id)}'"
|
||||
elif action == "lesson_view":
|
||||
title = "Lesson viewed"
|
||||
description = f"Viewed lesson '{lesson_title}' in course '{course.get('title', course_id)}'"
|
||||
else:
|
||||
title = "Course viewed"
|
||||
description = f"Opened course '{course.get('title', course_id)}'"
|
||||
|
||||
log_user_activity(
|
||||
db,
|
||||
user_id,
|
||||
"course",
|
||||
title,
|
||||
description,
|
||||
{"course_id": course_id, "lesson_id": lesson_id, "action": action},
|
||||
)
|
||||
|
||||
db.user_courses.update_one(
|
||||
{"user_id": user_id, "course_id": course_id},
|
||||
{
|
||||
"$set": {
|
||||
"user_id": user_id,
|
||||
"course_id": course_id,
|
||||
"last_activity_at": datetime.utcnow(),
|
||||
},
|
||||
"$setOnInsert": {
|
||||
"started_at": datetime.utcnow(),
|
||||
"completed": False,
|
||||
"lessons_completed": [],
|
||||
},
|
||||
},
|
||||
upsert=True,
|
||||
)
|
||||
|
||||
return jsonify({"success": True})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@bp.route("/<course_id>/progress", methods=["GET"])
|
||||
def get_course_progress(course_id):
|
||||
"""Get user's progress in a specific course"""
|
||||
|
||||
+160
-4
@@ -1,5 +1,5 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pymongo import MongoClient
|
||||
import os
|
||||
from bson import ObjectId
|
||||
@@ -188,8 +188,11 @@ def get_comprehensive_stats():
|
||||
"courses_completed": courses_completed,
|
||||
"coding_problems_solved": coding_problems_solved,
|
||||
"quiz_accuracy": quiz_accuracy,
|
||||
"coding_streak": coding_streak,
|
||||
"longest_streak": max(longest_streak, coding_streak),
|
||||
"streak_data": {
|
||||
"current_streak": coding_streak,
|
||||
"best_streak": max(longest_streak, coding_streak),
|
||||
"last_active_date": datetime.now().isoformat()
|
||||
},
|
||||
"total_courses": len(courses),
|
||||
"total_quizzes": len(quizzes),
|
||||
"global_rank": calculate_real_global_rank(user_stats, user_id) if user_stats else 0,
|
||||
@@ -270,9 +273,143 @@ def get_recent_activity():
|
||||
}), 401
|
||||
|
||||
logger.info(f"📋 Fetching REAL activity for wallet: {user_id}")
|
||||
|
||||
identity_candidates = {str(user_id)}
|
||||
if wallet_address:
|
||||
identity_candidates.add(str(wallet_address).lower())
|
||||
|
||||
# Resolve user identity aliases to avoid missing activity across auth methods.
|
||||
user_doc = None
|
||||
try:
|
||||
maybe_oid = ObjectId(str(user_id))
|
||||
user_doc = db.users.find_one({"_id": maybe_oid})
|
||||
except Exception:
|
||||
user_doc = db.users.find_one({"wallet_address": str(user_id).lower()}) or db.users.find_one({"email": str(user_id).lower()})
|
||||
|
||||
if user_doc:
|
||||
if user_doc.get("_id"):
|
||||
identity_candidates.add(str(user_doc.get("_id")))
|
||||
if user_doc.get("wallet_address"):
|
||||
identity_candidates.add(str(user_doc.get("wallet_address")).lower())
|
||||
if user_doc.get("email"):
|
||||
identity_candidates.add(str(user_doc.get("email")).lower())
|
||||
|
||||
logger.info(f"📋 Recent activity identity candidates: {sorted(identity_candidates)}")
|
||||
|
||||
activities = []
|
||||
|
||||
def parse_datetime(value):
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
candidate = value.replace("Z", "+00:00")
|
||||
try:
|
||||
return datetime.fromisoformat(candidate)
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
def to_utc_display(value):
|
||||
dt = parse_datetime(value) or datetime.now(timezone.utc)
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
else:
|
||||
dt = dt.astimezone(timezone.utc)
|
||||
return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
# Primary source: explicit user activity event log.
|
||||
event_docs = list(
|
||||
db.user_activity_events.find({"user_id": {"$in": list(identity_candidates)}}).sort("occurred_at", -1).limit(150)
|
||||
)
|
||||
for item in event_docs:
|
||||
occurred_at = item.get("occurred_at") or item.get("completed_at") or datetime.now(timezone.utc)
|
||||
activities.append({
|
||||
"id": str(item.get("_id", uuid.uuid4())),
|
||||
"type": item.get("type", "activity"),
|
||||
"title": item.get("title", "User Activity"),
|
||||
"description": item.get("description", "Activity recorded"),
|
||||
"completed_at": parse_datetime(occurred_at).isoformat() if parse_datetime(occurred_at) else datetime.now(timezone.utc).isoformat(),
|
||||
"timestamp_utc": item.get("timestamp_utc") or to_utc_display(occurred_at),
|
||||
"points_earned": int(item.get("points_earned", 0) or 0),
|
||||
"success_rate": item.get("success_rate", 0),
|
||||
"difficulty": item.get("difficulty", ""),
|
||||
"blockchain_verified": item.get("blockchain_verified", False)
|
||||
})
|
||||
|
||||
# Include admin/account status events from security logs as real activity fallback.
|
||||
admin_status_logs = list(
|
||||
db.security_logs.find({
|
||||
"event_type": "admin_user_status",
|
||||
"metadata.user_id": {"$in": list(identity_candidates)}
|
||||
}).sort("timestamp", -1).limit(50)
|
||||
)
|
||||
for item in admin_status_logs:
|
||||
occurred_at = item.get("timestamp", datetime.now(timezone.utc))
|
||||
metadata = item.get("metadata") or {}
|
||||
new_status = metadata.get("status") or metadata.get("new_status") or "updated"
|
||||
activities.append({
|
||||
"id": str(item.get("_id", uuid.uuid4())),
|
||||
"type": "account_status",
|
||||
"title": f"Account status changed to {new_status}",
|
||||
"description": f"Admin changed your account status to {new_status}",
|
||||
"completed_at": parse_datetime(occurred_at).isoformat() if parse_datetime(occurred_at) else datetime.now(timezone.utc).isoformat(),
|
||||
"timestamp_utc": to_utc_display(occurred_at),
|
||||
"points_earned": 0,
|
||||
"success_rate": 0,
|
||||
"difficulty": "",
|
||||
"blockchain_verified": False,
|
||||
})
|
||||
|
||||
# Fallback source: authenticated request audit logs for this user.
|
||||
audit_logs = list(
|
||||
db.security_logs.find({
|
||||
"$or": [
|
||||
{"metadata.auth_user_id": {"$in": list(identity_candidates)}},
|
||||
{"metadata.auth_wallet_address": {"$in": list(identity_candidates)}},
|
||||
{"metadata.auth_email": {"$in": list(identity_candidates)}},
|
||||
]
|
||||
}).sort("timestamp", -1).limit(150)
|
||||
)
|
||||
for item in audit_logs:
|
||||
path = str(item.get("path") or "")
|
||||
method = str(item.get("method") or "")
|
||||
ts = item.get("timestamp", datetime.now(timezone.utc))
|
||||
if not any(segment in path for segment in ["/api/quizzes", "/api/exam", "/api/coding", "/api/courses", "/api/auth"]):
|
||||
continue
|
||||
|
||||
log_type = "activity"
|
||||
title = f"{method} {path}"
|
||||
description = f"API activity on {path}"
|
||||
if "/api/quizzes" in path:
|
||||
log_type = "quiz"
|
||||
title = "Quiz activity"
|
||||
description = f"{method} {path}"
|
||||
elif "/api/exam" in path or "/api/coding" in path:
|
||||
log_type = "coding"
|
||||
title = "Coding activity"
|
||||
description = f"{method} {path}"
|
||||
elif "/api/courses" in path:
|
||||
log_type = "course"
|
||||
title = "Course activity"
|
||||
description = f"{method} {path}"
|
||||
elif "/api/auth" in path:
|
||||
log_type = "auth_login"
|
||||
title = "Authentication activity"
|
||||
description = f"{method} {path}"
|
||||
|
||||
activities.append({
|
||||
"id": str(item.get("_id", uuid.uuid4())),
|
||||
"type": log_type,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"completed_at": parse_datetime(ts).isoformat() if parse_datetime(ts) else datetime.now(timezone.utc).isoformat(),
|
||||
"timestamp_utc": to_utc_display(ts),
|
||||
"points_earned": 0,
|
||||
"success_rate": 0,
|
||||
"difficulty": "",
|
||||
"blockchain_verified": False,
|
||||
})
|
||||
|
||||
# ✅ ONLY REAL ACTIVITY SOURCES
|
||||
activity_sources = [
|
||||
(db.user_courses, "course", "Course Activity", "completed_at"),
|
||||
@@ -285,7 +422,7 @@ def get_recent_activity():
|
||||
try:
|
||||
# Get ONLY real MongoDB data
|
||||
recent_items = list(collection.find(
|
||||
{"user_id": user_id}
|
||||
{"user_id": {"$in": list(identity_candidates)}}
|
||||
).sort(date_field, -1).limit(20))
|
||||
|
||||
for item in recent_items:
|
||||
@@ -305,6 +442,7 @@ def get_recent_activity():
|
||||
"title": item.get('title', item.get('name', default_title)),
|
||||
"description": format_real_activity_description(item, activity_type),
|
||||
"completed_at": completed_at.isoformat(),
|
||||
"timestamp_utc": to_utc_display(completed_at),
|
||||
"points_earned": item.get('points', item.get('points_earned', 0)),
|
||||
"success_rate": item.get('score', item.get('completion_percentage', 0)),
|
||||
"difficulty": item.get('difficulty', ''),
|
||||
@@ -314,8 +452,26 @@ def get_recent_activity():
|
||||
logger.warning(f"⚠️ Failed to fetch {activity_type} activities: {e}")
|
||||
continue
|
||||
|
||||
# Exclude known placeholder/demo activity content from older seeded data.
|
||||
fake_markers = {
|
||||
"completed react fundamentals",
|
||||
"scored 95% on javascript quiz",
|
||||
"7-day learning streak achieved",
|
||||
"moved up 5 positions in leaderboard",
|
||||
}
|
||||
|
||||
filtered_activities = []
|
||||
for entry in activities:
|
||||
entry_text = f"{entry.get('title', '')} {entry.get('description', '')}".strip().lower()
|
||||
if any(marker in entry_text for marker in fake_markers):
|
||||
continue
|
||||
filtered_activities.append(entry)
|
||||
|
||||
activities = filtered_activities
|
||||
|
||||
# Sort by completion date
|
||||
activities.sort(key=lambda x: x['completed_at'], reverse=True)
|
||||
activities = activities[:100]
|
||||
|
||||
logger.info(f"✅ Found {len(activities)} REAL activities for wallet {user_id}")
|
||||
return jsonify({
|
||||
|
||||
@@ -5,6 +5,7 @@ import string
|
||||
from datetime import datetime, timedelta
|
||||
from pymongo import MongoClient
|
||||
import os
|
||||
from activity_logger import log_user_activity, resolve_user_identity
|
||||
|
||||
bp = Blueprint('exam', __name__)
|
||||
|
||||
@@ -255,6 +256,21 @@ def join_exam():
|
||||
session['session_id'] = participant['session_id']
|
||||
|
||||
print(f"✅ Participant {student_name} joined exam {exam_code}")
|
||||
|
||||
identity = resolve_user_identity(request, db)
|
||||
log_user_activity(
|
||||
db,
|
||||
identity.get("user_id"),
|
||||
"exam",
|
||||
"Joined coding exam",
|
||||
f"Joined exam '{exam.get('title', exam_code)}' as {student_name}",
|
||||
{
|
||||
"exam_code": exam_code,
|
||||
"exam_title": exam.get("title"),
|
||||
"student_name": student_name,
|
||||
"session_id": participant.get("session_id"),
|
||||
},
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
|
||||
@@ -3,6 +3,7 @@ from datetime import datetime
|
||||
import uuid
|
||||
import random
|
||||
import string
|
||||
from activity_logger import log_user_activity, resolve_user_identity
|
||||
|
||||
bp = Blueprint('quizzes', __name__)
|
||||
|
||||
@@ -233,6 +234,24 @@ def join_room():
|
||||
|
||||
print(f"✅ User joined room: {username} -> {room_code}")
|
||||
|
||||
identity = resolve_user_identity(request, db)
|
||||
resolved_user_id = identity.get("user_id") or data.get("user_id") or data.get("wallet_address")
|
||||
if isinstance(resolved_user_id, str):
|
||||
resolved_user_id = resolved_user_id.strip().lower() if resolved_user_id.startswith("0x") else resolved_user_id.strip()
|
||||
log_user_activity(
|
||||
db,
|
||||
resolved_user_id,
|
||||
"quiz",
|
||||
"Joined quiz room",
|
||||
f"Joined quiz room '{room.get('title', room_code)}' as {username}",
|
||||
{
|
||||
"room_code": room_code,
|
||||
"room_title": room.get("title"),
|
||||
"username": username,
|
||||
"session_id": participant_session.get("session_id"),
|
||||
},
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"Successfully joined quiz room '{room.get('title')}'",
|
||||
@@ -423,6 +442,50 @@ def submit_answer(session_id):
|
||||
"updated_at": datetime.now()
|
||||
}}
|
||||
)
|
||||
|
||||
identity = resolve_user_identity(request, db)
|
||||
resolved_user_id = identity.get("user_id") or data.get("user_id") or data.get("wallet_address")
|
||||
if isinstance(resolved_user_id, str):
|
||||
resolved_user_id = resolved_user_id.strip().lower() if resolved_user_id.startswith("0x") else resolved_user_id.strip()
|
||||
if resolved_user_id:
|
||||
db.user_quizzes.update_one(
|
||||
{
|
||||
"user_id": resolved_user_id,
|
||||
"session_id": session_id,
|
||||
"question_id": question_data.get("question_id"),
|
||||
},
|
||||
{
|
||||
"$set": {
|
||||
"user_id": resolved_user_id,
|
||||
"session_id": session_id,
|
||||
"room_code": room.get("room_code"),
|
||||
"room_title": room.get("title"),
|
||||
"question_id": question_data.get("question_id"),
|
||||
"score": participant.get("score", 0),
|
||||
"completed_at": datetime.now(),
|
||||
"is_correct": is_correct,
|
||||
"difficulty": current_difficulty,
|
||||
"username": participant.get("username"),
|
||||
}
|
||||
},
|
||||
upsert=True,
|
||||
)
|
||||
|
||||
log_user_activity(
|
||||
db,
|
||||
resolved_user_id,
|
||||
"quiz",
|
||||
"Answered quiz question",
|
||||
f"Answered a {current_difficulty} question in '{room.get('title', 'Quiz Room')}'",
|
||||
{
|
||||
"session_id": session_id,
|
||||
"room_code": room.get("room_code"),
|
||||
"room_title": room.get("title"),
|
||||
"is_correct": is_correct,
|
||||
"difficulty": current_difficulty,
|
||||
},
|
||||
points_earned=question_data.get('points', 10) if is_correct else 0,
|
||||
)
|
||||
|
||||
# Get AI prediction for comparison (if available)
|
||||
ai_feedback = None
|
||||
@@ -479,6 +542,68 @@ def submit_answer(session_id):
|
||||
# ✅ AI QUESTION GENERATION - IMPROVED VERSION
|
||||
# ===================================================================
|
||||
|
||||
@bp.route('/generate-ai', methods=['POST', 'OPTIONS'])
|
||||
def generate_ai_quiz():
|
||||
"""Generate a traditional quiz directly using AI"""
|
||||
if request.method == "OPTIONS":
|
||||
response = jsonify({'status': 'ok'})
|
||||
response.headers.add("Access-Control-Allow-Origin", "*")
|
||||
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
|
||||
response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS")
|
||||
return response
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
topic = data.get('topic', 'General')
|
||||
difficulty = data.get('difficulty', 'medium')
|
||||
num_questions = int(data.get('num_questions', 5))
|
||||
|
||||
print(f"🤖 AI Quiz Generation: topic={topic}, difficulty={difficulty}, questions={num_questions}")
|
||||
|
||||
ai_service = get_ai_service()
|
||||
if not ai_service:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "AI service not available"
|
||||
}), 503
|
||||
|
||||
# Generate questions using AI service
|
||||
generated_data = ai_service.generate_quiz(
|
||||
topic=topic,
|
||||
difficulty=difficulty,
|
||||
num_questions=num_questions
|
||||
)
|
||||
|
||||
# Save to database
|
||||
db = get_db()
|
||||
quiz_result = db.quizzes.insert_one({
|
||||
"id": str(uuid.uuid4()),
|
||||
"title": generated_data.get('title', f"AI Quiz - {topic}"),
|
||||
"description": generated_data.get('description', ''),
|
||||
"difficulty": difficulty,
|
||||
"questions": generated_data.get('questions', []),
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"total_points": len(generated_data.get('questions', [])) * 10,
|
||||
"generated_by": "AI",
|
||||
"topic": topic
|
||||
})
|
||||
|
||||
# Get the saved quiz
|
||||
saved_quiz = db.quizzes.find_one({"_id": quiz_result.inserted_id})
|
||||
saved_quiz['_id'] = str(saved_quiz['_id'])
|
||||
|
||||
print(f"✅ Quiz generated with {len(saved_quiz.get('questions', []))} questions")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"quiz": saved_quiz,
|
||||
"message": f"Generated {len(saved_quiz.get('questions', []))} AI questions on topic: {topic}"
|
||||
}), 201
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ AI generation error: {str(e)}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@bp.route('/room/<room_code>/generate-ai-questions', methods=['POST', 'OPTIONS'])
|
||||
def generate_ai_questions(room_code):
|
||||
"""Generate AI questions for the quiz room - IMPROVED VERSION"""
|
||||
@@ -1038,5 +1163,104 @@ def get_quiz_by_id(quiz_id):
|
||||
"quiz": quiz
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/<quiz_id>/submit', methods=['POST', 'OPTIONS'])
|
||||
def submit_traditional_quiz(quiz_id):
|
||||
"""Submit traditional quiz answers, store result, and log user activity."""
|
||||
if request.method == "OPTIONS":
|
||||
response = jsonify({'status': 'ok'})
|
||||
response.headers.add("Access-Control-Allow-Origin", "*")
|
||||
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
|
||||
response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS")
|
||||
return response
|
||||
|
||||
try:
|
||||
db = get_db()
|
||||
data = request.get_json() or {}
|
||||
answers = data.get('answers') or {}
|
||||
participant_name = (data.get('participant_name') or 'User').strip()
|
||||
|
||||
quiz = db.quizzes.find_one({"id": quiz_id})
|
||||
if not quiz:
|
||||
return jsonify({"success": False, "error": "Quiz not found"}), 404
|
||||
|
||||
questions = quiz.get('questions', [])
|
||||
total_questions = len(questions)
|
||||
correct_answers = 0
|
||||
total_points = 0
|
||||
ai_feedback = []
|
||||
|
||||
for idx, question in enumerate(questions):
|
||||
question_id = question.get('id') or question.get('question_id') or f"q_{idx}"
|
||||
expected = str(question.get('correct_answer', '')).strip().lower()
|
||||
user_answer = str(answers.get(question_id, '')).strip()
|
||||
is_correct = user_answer.lower() == expected if expected else False
|
||||
points = int(question.get('points', 10) or 10)
|
||||
|
||||
if is_correct:
|
||||
correct_answers += 1
|
||||
total_points += points
|
||||
|
||||
ai_feedback.append({
|
||||
"question": question.get('question_text', question.get('question', f"Question {idx + 1}")),
|
||||
"user_answer": user_answer,
|
||||
"is_correct": is_correct,
|
||||
"correct_answer": question.get('correct_answer', ''),
|
||||
"ai_feedback": {
|
||||
"feedback": "Correct" if is_correct else "Review this concept and try again"
|
||||
}
|
||||
})
|
||||
|
||||
score = round((correct_answers / total_questions) * 100, 1) if total_questions > 0 else 0
|
||||
|
||||
identity = resolve_user_identity(request, db)
|
||||
resolved_user_id = identity.get("user_id") or data.get("user_id") or data.get("wallet_address")
|
||||
if isinstance(resolved_user_id, str):
|
||||
resolved_user_id = resolved_user_id.strip().lower() if resolved_user_id.startswith("0x") else resolved_user_id.strip()
|
||||
|
||||
if resolved_user_id:
|
||||
db.user_quizzes.insert_one({
|
||||
"user_id": resolved_user_id,
|
||||
"quiz_id": quiz_id,
|
||||
"title": quiz.get('title', 'Quiz Submission'),
|
||||
"topic": quiz.get('topic', 'General'),
|
||||
"participant_name": participant_name,
|
||||
"score": score,
|
||||
"correct_answers": correct_answers,
|
||||
"total_questions": total_questions,
|
||||
"points": total_points,
|
||||
"completed_at": datetime.now(),
|
||||
"answers": answers,
|
||||
})
|
||||
|
||||
log_user_activity(
|
||||
db,
|
||||
resolved_user_id,
|
||||
"quiz",
|
||||
"Completed quiz",
|
||||
f"Completed quiz '{quiz.get('title', quiz_id)}' with score {score}%",
|
||||
{
|
||||
"quiz_id": quiz_id,
|
||||
"quiz_title": quiz.get('title', 'Quiz'),
|
||||
"score": score,
|
||||
"correct_answers": correct_answers,
|
||||
"total_questions": total_questions,
|
||||
},
|
||||
points_earned=total_points,
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"results": {
|
||||
"score": score,
|
||||
"correct_answers": correct_answers,
|
||||
"total_questions": total_questions,
|
||||
"total_points": total_points,
|
||||
"ai_feedback": ai_feedback,
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
Reference in New Issue
Block a user