From 4e9fa011fafb3cbf6172bd098e60251ae94444bf Mon Sep 17 00:00:00 2001 From: white <122345776@umail.ucc.ie> Date: Mon, 27 Jan 2025 12:49:42 +0000 Subject: [PATCH] Update: remove unused imports, added better comments and refactored for all blueprints --- web_server/blueprints/__init__.py | 28 +++++++------ web_server/blueprints/authentication.py | 55 ++++++++++++++++++------- web_server/blueprints/chat.py | 21 ++++++---- web_server/blueprints/main.py | 15 ------- web_server/blueprints/streams.py | 18 ++++---- web_server/blueprints/stripe.py | 2 +- web_server/blueprints/user.py | 16 +++---- web_server/blueprints/utils.py | 15 ++++--- 8 files changed, 96 insertions(+), 74 deletions(-) delete mode 100644 web_server/blueprints/main.py diff --git a/web_server/blueprints/__init__.py b/web_server/blueprints/__init__.py index cb90558..2508522 100644 --- a/web_server/blueprints/__init__.py +++ b/web_server/blueprints/__init__.py @@ -1,16 +1,27 @@ from flask import Flask -# from flask_wtf.csrf import CSRFProtect, generate_csrf from flask_session import Session -from blueprints.utils import logged_in_user from flask_cors import CORS -import os +from blueprints.utils import logged_in_user +# from flask_wtf.csrf import CSRFProtect, generate_csrf + +from blueprints.authentication import auth_bp +from blueprints.stripe import stripe_bp +from blueprints.user import user_bp +from blueprints.streams import stream_bp +from blueprints.chat import chat_bp, socketio + +from os import getenv # csrf = CSRFProtect() - def create_app(): + """ + Set up the flask app by registering all the blueprints and configuring + the settings. Also create a CSRF token to prevent Cross-site Request Forgery. + And setup web sockets to be used throughout the project. + """ app = Flask(__name__) - app.config["SECRET_KEY"] = os.getenv("FLASK_SECRET_KEY") + app.config["SECRET_KEY"] = getenv("FLASK_SECRET_KEY") app.config["SESSION_PERMANENT"] = False app.config["SESSION_TYPE"] = "filesystem" #! ↓↓↓ For development purposes only - Allow cross-origin requests for the frontend @@ -25,16 +36,9 @@ def create_app(): # return jsonify({'csrf_token': generate_csrf()}), 200 with app.app_context(): - from blueprints.authentication import auth_bp - from blueprints.main import main_bp - from blueprints.stripe import stripe_bp - from blueprints.user import user_bp - from blueprints.streams import stream_bp - from blueprints.chat import chat_bp, socketio # Registering Blueprints app.register_blueprint(auth_bp) - app.register_blueprint(main_bp) app.register_blueprint(stripe_bp) app.register_blueprint(user_bp) app.register_blueprint(stream_bp) diff --git a/web_server/blueprints/authentication.py b/web_server/blueprints/authentication.py index 40caec0..312027b 100644 --- a/web_server/blueprints/authentication.py +++ b/web_server/blueprints/authentication.py @@ -10,17 +10,20 @@ auth_bp = Blueprint("auth", __name__) @auth_bp.route("/signup", methods=["POST"]) @cross_origin(supports_credentials=True) def signup(): + """ + Route that allows a user to sign up by providing a `username`, `email` and `password`. + """ + # ensure a JSON request is made to contact this route if not request.is_json: return jsonify({"message": "Expected JSON data"}), 400 + # Extract data from request via JSON data = request.get_json() - - # Extract data from request username = data.get('username') email = data.get('email') password = data.get('password') - # Basic server-side validation + # Validation - ensure all fields exist, users cannot have an empty field if not all([username, email, password]): fields = ["username", "email", "password"] for x in fields: @@ -32,7 +35,7 @@ def signup(): "message": "Missing required fields" }), 400 - # Sanitize the inputs + # Sanitize the inputs - helps to prevent SQL injection try: username = sanitizer(username, "username") email = sanitizer(email, "email") @@ -49,7 +52,7 @@ def signup(): cursor = db.create_connection() try: - # Check for duplicate email/username + # Check for duplicate email/username, no two users can have the same dup_email = cursor.execute( "SELECT * FROM users WHERE email = ?", (email,) @@ -74,7 +77,7 @@ def signup(): "message": "Username already taken" }), 400 - # Create new user + # Create new user once input is validated cursor.execute( """INSERT INTO users (username, password, email, num_followers, bio) @@ -89,7 +92,7 @@ def signup(): ) db.commit_data() - # Create session for new user + # Create session for new user, to avoid them having unnecessary state info session.clear() session["username"] = username @@ -112,27 +115,43 @@ def signup(): @auth_bp.route("/login", methods=["POST"]) @cross_origin(supports_credentials=True) def login(): + """ + Login to the web app with existing credentials. + """ + + # ensure a JSON request is made to contact this route if not request.is_json: return jsonify({"message": "Expected JSON data"}), 400 + # Extract data from request via JSON data = request.get_json() - - # Extract data from request username = data.get('username') password = data.get('password') - # Basic server-side validation + # Validation - ensure all fields exist, users cannot have an empty field if not all([username, password]): return jsonify({ "logged_in": False, "message": "Missing required fields" }), 400 - + + # Sanitize the inputs - helps to prevent SQL injection + try: + username = sanitizer(username, "username") + password = sanitizer(password, "password") + except ValueError as e: + return jsonify({ + "account_created": False, + "error_fields": [username, password], + "message": "Invalid input received" + }), 400 + + # Create a connection to the database db = Database() cursor = db.create_connection() try: - # Check if user exists + # Check if user exists, only existing users can be logged in user = cursor.execute( "SELECT * FROM users WHERE username = ?", (username,) @@ -145,7 +164,7 @@ def login(): "message": "Invalid username or password" }), 401 - # Verify password + # Verify password matches the password associated with that user if not check_password_hash(user["password"], password): return jsonify({ "logged_in": False, @@ -153,10 +172,11 @@ def login(): "message": "Invalid username or password" }), 401 - # Set up session + # Set up session to avoid having unncessary state information session.clear() session["username"] = username + # User has been logged in, let frontend know that return jsonify({ "logged_in": True, "message": "Login successful", @@ -176,6 +196,11 @@ def login(): @auth_bp.route("/logout") @login_required -def logout(): +def logout() -> dict: + """ + Log out and clear the users session. + + Can only be accessed by a logged in user. + """ session.clear() return {"logged_in": False} diff --git a/web_server/blueprints/chat.py b/web_server/blueprints/chat.py index a792b78..2fa4b63 100644 --- a/web_server/blueprints/chat.py +++ b/web_server/blueprints/chat.py @@ -1,5 +1,4 @@ -from flask import Blueprint, request, jsonify, session -from blueprints.utils import login_required +from flask import Blueprint, jsonify, session from database.database import Database from flask_socketio import SocketIO, emit, join_room, leave_room from datetime import datetime @@ -11,11 +10,14 @@ socketio = SocketIO() # TODO: Add a route that deletes all chat logs when the stream is finished @socketio.on("connect") -def handle_connection(): - print("Client Connected") +def handle_connection() -> None: + """ + Accept the connection from the frontend. + """ + print("Client Connected") # Confirmation connect has been made @socketio.on("join") -def handle_join(data): +def handle_join(data) -> None: """ Allow a user to join the chat of the stream they are watching. """ @@ -25,7 +27,7 @@ def handle_join(data): emit("status", {"message": f"Welcome to the chat, stream_id: {stream_id}"}, room=stream_id) @socketio.on("leave") -def handle_leave(data): +def handle_leave(data) -> None: """ Handle what happens when a user leaves the stream they are watching in regards to the chat. """ @@ -35,7 +37,7 @@ def handle_leave(data): emit("status", {"message": f"user left room {stream_id}"}, room=stream_id) @chat_bp.route("/chat/") -def get_past_chat(stream_id): +def get_past_chat(stream_id: int): """ Returns a JSON object to be passed to the server. @@ -56,7 +58,7 @@ def get_past_chat(stream_id): FROM chat WHERE stream_id = ? ORDER BY time_sent DESC - LIMIT 50 + LIMIT 1 ) ORDER BY time_sent ASC;""", (stream_id,)).fetchall() db.close_connection() @@ -68,7 +70,7 @@ def get_past_chat(stream_id): return jsonify({"chat_history": chat_history}), 200 @socketio.on("send_message") -def send_chat(data): +def send_chat(data) -> None: """ Using WebSockets to send a chat message to the specified chat """ @@ -92,6 +94,7 @@ def send_chat(data): db.commit_data() db.close_connection() + # Send the chat message to the client so it can be displayed emit("new_message", { "chatter_id":chatter_id, "message":message, diff --git a/web_server/blueprints/main.py b/web_server/blueprints/main.py deleted file mode 100644 index cab308f..0000000 --- a/web_server/blueprints/main.py +++ /dev/null @@ -1,15 +0,0 @@ -from flask import Blueprint, render_template, session, jsonify - -main_bp = Blueprint("app", __name__) - -# temp, showcasing HLS - - -@main_bp.route('/hls1/') -def hls(stream_id): - stream_url = f"http://127.0.0.1:8080/hls/{stream_id}/index.m3u8" - return render_template("video.html", video_url=stream_url) -# -------------------------------------------------------- - - -# TODO Route for saving uploaded thumbnails to database, serving these images to the frontend upon request: →→→ @main_bp.route('/images/') \n def serve_image(filename): ←←← diff --git a/web_server/blueprints/streams.py b/web_server/blueprints/streams.py index 3c27838..f5fe021 100644 --- a/web_server/blueprints/streams.py +++ b/web_server/blueprints/streams.py @@ -4,8 +4,8 @@ from utils.user_utils import get_user_id stream_bp = Blueprint("stream", __name__) -@stream_bp.route('/get_streams', methods=['GET']) -def get_sample_streams(): +@stream_bp.route('/get_streams') +def get_sample_streams() -> list[dict]: """ Returns a list of (sample) streams live right now """ @@ -55,8 +55,8 @@ def get_sample_streams(): return streams -@stream_bp.route('/get_recommended_streams', methods=['GET']) -def get_recommended_streams(): +@stream_bp.route('/get_recommended_streams') +def get_recommended_streams() -> list[dict]: """ Queries DB to get a list of recommended streams using an algorithm """ @@ -83,8 +83,8 @@ def get_recommended_streams(): }] -@stream_bp.route('/get_categories', methods=['GET']) -def get_categories(): +@stream_bp.route('/get_categories') +def get_categories() -> list[dict]: """ Returns a list of (sample) categories being watched right now """ @@ -122,8 +122,8 @@ def get_categories(): ] -@stream_bp.route('/get_followed_categories', methods=['GET']) -def get_followed_categories(): +@stream_bp.route('/get_followed_categories') +def get_followed_categories() -> list | list[dict]: """ Queries DB to get a list of followed categories Hmm.. @@ -134,7 +134,7 @@ def get_followed_categories(): return get_categories() -@stream_bp.route('/get_streamer_data/', methods=['GET']) +@stream_bp.route('/get_streamer_data/') def get_streamer_data(streamer_username): """ Returns a given streamer's data diff --git a/web_server/blueprints/stripe.py b/web_server/blueprints/stripe.py index 831590e..d55c8b1 100644 --- a/web_server/blueprints/stripe.py +++ b/web_server/blueprints/stripe.py @@ -30,7 +30,7 @@ def create_checkout_session(): return jsonify(clientSecret=session.client_secret) -@stripe_bp.route('/session-status', methods=['GET']) # check for payment status +@stripe_bp.route('/session-status') # check for payment status def session_status(): """ Used to query payment status diff --git a/web_server/blueprints/user.py b/web_server/blueprints/user.py index 08c7532..6d6d0ac 100644 --- a/web_server/blueprints/user.py +++ b/web_server/blueprints/user.py @@ -3,8 +3,8 @@ from utils.user_utils import is_subscribed, is_following, subscription_expiratio user_bp = Blueprint("user", __name__) -@user_bp.route('/is_subscribed//', methods=['GET']) -def user_subscribed(user_id, streamer_id): +@user_bp.route('/is_subscribed//') +def user_subscribed(user_id: int, streamer_id: int): """ Checks to see if user is subscribed to a streamer """ @@ -12,8 +12,8 @@ def user_subscribed(user_id, streamer_id): return jsonify({"subscribed": True}) return jsonify({"subscribed": False}) -@user_bp.route('/is_following//', methods=['GET']) -def user_following(user_id, streamer_id): +@user_bp.route('/is_following//') +def user_following(user_id: int, streamer_id: int): """ Checks to see if user is following a streamer """ @@ -22,8 +22,8 @@ def user_following(user_id, streamer_id): return jsonify({"following": False}) -@user_bp.route('/subscription_remaining//', methods=['GET']) -def user_subscription_expiration(user_id, streamer_id): +@user_bp.route('/subscription_remaining//') +def user_subscription_expiration(user_id: int, streamer_id: int): """ Returns remaining time until subscription expiration """ @@ -31,7 +31,7 @@ def user_subscription_expiration(user_id, streamer_id): return jsonify({"remaining_time": remaining_time}) -@user_bp.route('/get_login_status', methods=['GET']) +@user_bp.route('/get_login_status') def get_login_status(): """ Returns whether the user is logged in or not @@ -39,7 +39,7 @@ def get_login_status(): return jsonify(session.get("username") is not None) @user_bp.route('/authenticate_user') -def authenticate_user(): +def authenticate_user() -> dict: """ Authenticates the user """ diff --git a/web_server/blueprints/utils.py b/web_server/blueprints/utils.py index a496a8d..b808fdb 100644 --- a/web_server/blueprints/utils.py +++ b/web_server/blueprints/utils.py @@ -3,11 +3,16 @@ from functools import wraps from re import match def logged_in_user(): + """ + Validator to make sure a user is logged in. + """ g.user = session.get("username", None) g.admin = session.get("username", None) def login_required(view): - """add at start of routes where users need to be logged in to access""" + """ + Add at start of routes where users need to be logged in to access. + """ @wraps(view) def wrapped_view(*args, **kwargs): if g.user is None: @@ -16,7 +21,9 @@ def login_required(view): return wrapped_view def admin_required(view): - """add at start of routes where admins need to be logged in to access""" + """ + Add at start of routes where admins need to be logged in to access. + """ @wraps(view) def wrapped_view(*args, **kwargs): if g.admin != "admin": @@ -24,8 +31,6 @@ def admin_required(view): return view(*args, **kwargs) return wrapped_view -import re - def sanitizer(user_input: str, input_type="username") -> str: """ Sanitizes user input based on the specified input type. @@ -58,7 +63,7 @@ def sanitizer(user_input: str, input_type="username") -> str: r = rules.get(input_type) if not r or \ not (r["min_length"] <= len(sanitised_input) <= r["max_length"]) or \ - not re.match(r["pattern"], sanitised_input): + not match(r["pattern"], sanitised_input): raise ValueError("Unaccepted character or length in input") return sanitised_input