From 8016d489bd3040da86f64ba03ef5f4f144f55248 Mon Sep 17 00:00:00 2001 From: Kushal Dotel Date: Sun, 12 Jan 2025 09:42:00 +0545 Subject: [PATCH 1/4] feat: admin dashboard and stats --- .gitignore | 1 + backend/app.py | 4 +- backend/blueprints/admin/__init__.py | 140 +++++++++++++++++++++++++ backend/blueprints/profile/__init__.py | 7 +- 4 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 backend/blueprints/admin/__init__.py diff --git a/.gitignore b/.gitignore index 6399970..6b089f9 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ env/ .venv/ __pypackages__/ +**.env # Node.js node_modules/ diff --git a/backend/app.py b/backend/app.py index aa9c5cb..64476de 100644 --- a/backend/app.py +++ b/backend/app.py @@ -15,6 +15,7 @@ from utils.utils import random_string_generator, hash_string from blueprints.profile import profile as profileBlueprint from blueprints.session import session as sessionBlueprint +from blueprints.admin import admin as adminBlueprint app = Flask(__name__) # Set configuration directly on the app instance @@ -26,6 +27,7 @@ db.init_app(app) app.register_blueprint(profileBlueprint, url_prefix='/api/profile') app.register_blueprint(sessionBlueprint,url_prefix='/api/session') +app.register_blueprint(adminBlueprint,url_prefix='/api/admin') @app.route('/media/') def send_file(filename): @@ -53,7 +55,7 @@ def seed_data(): joinedDate=datetime.utcnow(), lastOnline=datetime.utcnow(), bio=f"This is user{i}'s bio.", role=int(UserRole.USER), isActivated=True, sessions=[], user_badges=[], - enrollments=[], quizzes=[], quiz_attempts=[], chats=[], notifications=[]) + enrollments=[], quizzes=[], quiz_attempts=[], chats=[], notifications=[],publications=[]) for i in range(1, 6) ] db.session.add_all(users) db.session.commit() diff --git a/backend/blueprints/admin/__init__.py b/backend/blueprints/admin/__init__.py new file mode 100644 index 0000000..9e2641a --- /dev/null +++ b/backend/blueprints/admin/__init__.py @@ -0,0 +1,140 @@ +from utils .auth import auth_required, requires_role +from flask import Blueprint, jsonify, g +from db.model import User, Course, Enrollment, Chat, db +from sqlalchemy import select, func, desc, and_ +from datetime import datetime, timedelta +from constants import UserRole + +admin = Blueprint('admin', __name__) + +@admin.route('/stats/users', methods=['GET']) +@auth_required() +@requires_role([UserRole.ADMIN]) +def get_user_stats(): + """ + Get total users and authors count. + Only accessible by admin users. + """ + try: + # Get total users + total_users = db.session.execute( + select(func.count()).select_from(User) + ).scalar() + + # Get authors (users who have created courses) + distinct_authors_count = db.session.execute( + select(func.count(func.distinct(Course.authorID))) + ).scalar() + + return jsonify({ + 'stats': { + 'totalUsers': total_users, + 'totalAuthors': distinct_authors_count + } + }), 200 + + except Exception as e: + return jsonify({'message': f'An error occurred: {str(e)}'}), 500 + +@admin.route('/stats/enrollments', methods=['GET']) +@auth_required() +@requires_role([UserRole.ADMIN]) +def get_enrollment_stats(): + """ + Get course enrollment and discussion statistics. + Only accessible by admin users. + """ + try: + # Get enrollment and user counts + enrollment_stats = db.session.execute( + select( + func.count(Enrollment.id).label('total_enrollments'), + func.count(func.distinct(Enrollment.userID)).label('enrolled_users') + ) + .select_from(Enrollment) + ).first() + + # Get course-wise enrollment counts + course_stats = db.session.execute( + select( + Course.name, + func.count(Enrollment.id).label('enrollment_count') + ) + .join(Course, Course.id == Enrollment.courseID) + .group_by(Course.id) + .order_by(desc('enrollment_count')) + ).all() + + return jsonify({ + 'stats': { + 'totalEnrollments': enrollment_stats.total_enrollments, + 'totalEnrolledUsers': enrollment_stats.enrolled_users, + 'courseEnrollments': [{ + 'courseName': stat.name, + 'enrollmentCount': stat.enrollment_count + } for stat in course_stats] + } + }), 200 + + except Exception as e: + return jsonify({'message': f'An error occurred: {str(e)}'}), 500 + +@admin.route('/stats/discussions', methods=['GET']) +@auth_required() +@requires_role([UserRole.ADMIN]) +def get_discussion_stats(): + """ + Get chat room activity statistics. + Only accessible by admin users. + """ + try: + # Get activity for last 24 hours + twenty_four_hours_ago = datetime.now() - timedelta(hours=24) + + # Get active rooms and their stats + active_rooms = db.session.execute( + select( + Course.name, + func.count(Chat.id).label('message_count'), + func.count(func.distinct(Chat.userID)).label('active_users') + ) + .join(Course, Course.id == Chat.courseID) + .where(Chat.chatDate >= twenty_four_hours_ago) + .group_by(Course.id) + .order_by(desc('message_count')) + ).all() + + # Get total active rooms + total_active_rooms = len(active_rooms) + + # Get most active room + most_active_room = None + if active_rooms: + most_active = active_rooms[0] + most_active_room = { + 'name': most_active.name, + 'messageCount': most_active.message_count, + 'activeUsers': most_active.active_users + } + + # Get total active users across all rooms + total_active_users = db.session.execute( + select(func.count(func.distinct(Chat.userID))) + .where(Chat.chatDate >= twenty_four_hours_ago) + ).scalar() + + return jsonify({ + 'stats': { + 'totalActiveRooms': total_active_rooms, + 'totalActiveUsers': total_active_users, + 'mostActiveRoom': most_active_room, + 'activeRooms': [{ + 'roomName': room.name, + 'messageCount': room.message_count, + 'activeUsers': room.active_users + } for room in active_rooms] + } + }), 200 + + except Exception as e: + return jsonify({'message': f'An error occurred: {str(e)}'}), 500 diff --git a/backend/blueprints/profile/__init__.py b/backend/blueprints/profile/__init__.py index 2d2dee0..597aab3 100644 --- a/backend/blueprints/profile/__init__.py +++ b/backend/blueprints/profile/__init__.py @@ -260,4 +260,9 @@ def change_password(): user.hash_password = generate_password_hash(new_password) db.session.commit() - return jsonify({"message": "Password updated successfully"}), 200 \ No newline at end of file + return jsonify({"message": "Password updated successfully"}), 200 + + +# @profile.route('/hello') +# @auth_required() +# @requires_role([UserRole.ADMIN]) \ No newline at end of file From c51d14f1b025218ab714086a6a487cba952745a3 Mon Sep 17 00:00:00 2001 From: Kushal Dotel Date: Sun, 12 Jan 2025 09:49:57 +0545 Subject: [PATCH 2/4] feat: add CORS policy --- backend/app.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/app.py b/backend/app.py index 64476de..a91a98c 100644 --- a/backend/app.py +++ b/backend/app.py @@ -11,6 +11,7 @@ import uuid from datetime import datetime from constants import UserRole from utils.utils import random_string_generator, hash_string +from flask_cors import CORS from blueprints.profile import profile as profileBlueprint @@ -18,6 +19,13 @@ from blueprints.session import session as sessionBlueprint from blueprints.admin import admin as adminBlueprint app = Flask(__name__) + +# Enable CORS for all routes +CORS(app) +# Enable CORS for specific routes +# CORS(app, resources={ +# r"/api/*": {"origins": "*"} # Allows CORS for all `/api/` routes +# }) # Set configuration directly on the app instance app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif'} From 7852c8c559cc17b827b9e0f350ffb616976ed846 Mon Sep 17 00:00:00 2001 From: Kushal Dotel Date: Sun, 12 Jan 2025 11:50:10 +0545 Subject: [PATCH 3/4] send message to the courses --- backend/app.py | 2 + backend/blueprints/chat/__init__.py | 64 ++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/backend/app.py b/backend/app.py index a91a98c..410a7a6 100644 --- a/backend/app.py +++ b/backend/app.py @@ -17,6 +17,7 @@ from flask_cors import CORS from blueprints.profile import profile as profileBlueprint from blueprints.session import session as sessionBlueprint from blueprints.admin import admin as adminBlueprint +from blueprints.chat import chat as chatBlueprint app = Flask(__name__) @@ -36,6 +37,7 @@ db.init_app(app) app.register_blueprint(profileBlueprint, url_prefix='/api/profile') app.register_blueprint(sessionBlueprint,url_prefix='/api/session') app.register_blueprint(adminBlueprint,url_prefix='/api/admin') +app.register_blueprint(chatBlueprint,url_prefix='/api/chat') @app.route('/media/') def send_file(filename): diff --git a/backend/blueprints/chat/__init__.py b/backend/blueprints/chat/__init__.py index 23434f5..10e8648 100644 --- a/backend/blueprints/chat/__init__.py +++ b/backend/blueprints/chat/__init__.py @@ -1,9 +1,13 @@ -from flask import Blueprint, request, jsonify +from flask import Blueprint, request, jsonify,g from uuid import UUID -from db.model import db, User, Course, Enrollment +from db.model import db, User, Course, Enrollment,Chat +from utils.auth import auth_required +import requests chat = Blueprint('chat', __name__) +SPAM_DETECTION_URL = "http://localhost:5000/test-spam" + @chat.route('/course//users', methods=['POST']) def get_users_assigned_to_course(course_id: UUID): """ @@ -64,3 +68,59 @@ def join_chat(course_id: UUID): return jsonify({"message": f"User {user.username} is enrolled in the course and can join the chat."}), 200 except Exception as e: return jsonify({"error": f"An error occurred: {str(e)}"}), 500 + + +@chat.route("/send-message", methods=["POST"]) +@auth_required() +def create_chat(): + """ + Create a chat message for a specific course. + """ + current_user = g.current_user # Fetch the logged-in user + data = request.get_json() + + # Validate the payload + course_id = data.get("course_id") + message = data.get("message") + + if not course_id or not message: + return jsonify({"error": "Course ID and message are required."}), 400 + + try: + # Call the spam detection service + spam_response = requests.post(SPAM_DETECTION_URL, json={"test_message": message}) + if spam_response.status_code != 200: + return jsonify({"error": "Failed to check message for spam."}), 500 + + spam_score = int(spam_response.json().get("spam_score", 0)) + + if spam_score > 6: + return jsonify({"error": "This message contains suspicious links or is vulnerable."}), 400 + + # Verify the course exists + course = db.session.query(Course).filter_by(id=course_id).first() + if not course: + return jsonify({"error": "Invalid course ID."}), 404 + + # Check if the user is enrolled in the course + is_enrolled = db.session.query(Course).join(Enrollment).filter( + Enrollment.userID == current_user.id, + Enrollment.courseID == course_id + ).first() + + if not is_enrolled: + return jsonify({"error": "You are not enrolled in this course."}), 403 + + # Create and save the chat message + new_chat = Chat( + textContent=message, + userID=current_user.id, + courseID=course_id, + ) + db.session.add(new_chat) + db.session.commit() + + return jsonify({"message": "Chat sent successfully.", "chat_id": str(new_chat.id)}), 201 + + except Exception as e: + return jsonify({"error": f"An error occurred: {str(e)}"}), 500 \ No newline at end of file From a6d03971eb14ec079375e16b22e57351d9a4274a Mon Sep 17 00:00:00 2001 From: Kushal Dotel Date: Sun, 12 Jan 2025 14:18:56 +0545 Subject: [PATCH 4/4] fix:chat --- backend/blueprints/chat/__init__.py | 57 ++++++++++++++++++++++++++ backend/blueprints/profile/__init__.py | 10 ++--- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/backend/blueprints/chat/__init__.py b/backend/blueprints/chat/__init__.py index 10e8648..d9ea1b7 100644 --- a/backend/blueprints/chat/__init__.py +++ b/backend/blueprints/chat/__init__.py @@ -3,6 +3,7 @@ from uuid import UUID from db.model import db, User, Course, Enrollment,Chat from utils.auth import auth_required import requests +from sqlalchemy import desc chat = Blueprint('chat', __name__) @@ -122,5 +123,61 @@ def create_chat(): return jsonify({"message": "Chat sent successfully.", "chat_id": str(new_chat.id)}), 201 + except Exception as e: + return jsonify({"error": f"An error occurred: {str(e)}"}), 500 + +@chat.route("/get", methods=["GET"]) +@auth_required() +def get_chat_history(): + """ + Fetch chat history for a course. + """ + current_user = g.current_user # Logged-in user + course_id = request.args.get("course_id") + limit = int(request.args.get("limit", 20)) # Default to 20 messages + offset = int(request.args.get("offset", 0)) # Default to no offset (latest chats) + + if not course_id: + return jsonify({"error": "Course ID is required."}), 400 + + try: + # Verify the course exists + course = db.session.query(Course).filter_by(id=course_id).first() + if not course: + return jsonify({"error": "Invalid course ID."}), 404 + + # Check if the user is enrolled in the course + is_enrolled = db.session.query(Course).join(Enrollment).filter( + Enrollment.userID == current_user.id, + Enrollment.courseID == course_id + ).first() + + if not is_enrolled: + return jsonify({"error": "You are not enrolled in this course."}), 403 + + # Fetch the latest chat messages with limit and offset + chats = ( + db.session.query(Chat) + .filter_by(courseID=course_id) + .order_by(desc(Chat.chatDate)) + .offset(offset) + .limit(limit) + .all() + ) + + # Format the chat messages + chat_history = [ + { + "id": str(chat.id), + "textContent": chat.textContent, + "userID": str(chat.userID), + "chatDate": chat.chatDate.isoformat(), + "is_mine": chat.userID == current_user.id, + } + for chat in chats + ] + + return jsonify({"chats": chat_history}), 200 + except Exception as e: return jsonify({"error": f"An error occurred: {str(e)}"}), 500 \ No newline at end of file diff --git a/backend/blueprints/profile/__init__.py b/backend/blueprints/profile/__init__.py index 597aab3..e7082d9 100644 --- a/backend/blueprints/profile/__init__.py +++ b/backend/blueprints/profile/__init__.py @@ -113,17 +113,17 @@ def manage_profile(): if request.method == 'GET': try: - profile_url = url_for('send_file', filename=current_user.pfpFilename, _external=True) + profile_picture = url_for('send_file', filename=current_user.pfpFilename, _external=True) except: - profile_url = "" + profile_picture = "" try: # Construct the user profile data profile_data = { "id": str(current_user.id), "email": current_user.email, - "first_name": current_user.firstName, - "last_name": current_user.lastName, + "firstName": current_user.firstName, + "lastName": current_user.lastName, "username": current_user.username, "dob": current_user.dob.isoformat() if current_user.dob else None, "joined_date": current_user.joinedDate.isoformat(), @@ -131,7 +131,7 @@ def manage_profile(): "bio": current_user.bio, "role": current_user.role, "pfp_filename": current_user.pfpFilename, - "profile_url": profile_url, + "profile_picture": profile_picture, } return jsonify({"profile": profile_data}), 200