commit b4ecbeaa371d00d78af329594cb631887333875a Author: Thastertyn Date: Tue Mar 5 16:01:26 2024 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1418522 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + // Pro informace o možných atributech použijte technologii IntelliSense. + // Umístěním ukazatele myši zobrazíte popisy existujících atributů. + // Další informace najdete tady: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Flask Shop", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/main.py", + "args": [], + "cwd": "${workspaceFolder}", + "env": { + "FLASK_APP": "main.py", + "FLASK_ENV": "development" + }, + "envFile": "${workspaceFolder}/.env", + "stopOnEntry": false, + "console": "internalConsole", + "autoReload": {"enable": true} + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..85b80f5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "cSpell.words": [ + "dotenv", + "jsonify" + ] +} \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..954eefb --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,17 @@ +from flask import Flask +from flask_jwt_extended import JWTManager + +def create_app(): + app = Flask(__name__) + jwt = JWTManager(app) + + from app.api import bp, bp_errors, bp_product, bp_user + app.register_blueprint(bp) + app.register_blueprint(bp_errors) + app.register_blueprint(bp_product) + app.register_blueprint(bp_user) + + from app.config import FlaskTesting, FlaskProduction + app.config.from_object(FlaskTesting) + + return app \ No newline at end of file diff --git a/app/__pycache__/__init__.cpython-310.pyc b/app/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..c692374 Binary files /dev/null and b/app/__pycache__/__init__.cpython-310.pyc differ diff --git a/app/__pycache__/__init__.cpython-311.pyc b/app/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..995c4bc Binary files /dev/null and b/app/__pycache__/__init__.cpython-311.pyc differ diff --git a/app/__pycache__/config.cpython-310.pyc b/app/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000..32e5d24 Binary files /dev/null and b/app/__pycache__/config.cpython-310.pyc differ diff --git a/app/__pycache__/config.cpython-311.pyc b/app/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000..0137554 Binary files /dev/null and b/app/__pycache__/config.cpython-311.pyc differ diff --git a/app/__pycache__/decorators.cpython-311.pyc b/app/__pycache__/decorators.cpython-311.pyc new file mode 100644 index 0000000..05eb1c3 Binary files /dev/null and b/app/__pycache__/decorators.cpython-311.pyc differ diff --git a/app/__pycache__/extensions.cpython-310.pyc b/app/__pycache__/extensions.cpython-310.pyc new file mode 100644 index 0000000..cb508fd Binary files /dev/null and b/app/__pycache__/extensions.cpython-310.pyc differ diff --git a/app/__pycache__/extensions.cpython-311.pyc b/app/__pycache__/extensions.cpython-311.pyc new file mode 100644 index 0000000..b93580f Binary files /dev/null and b/app/__pycache__/extensions.cpython-311.pyc differ diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..152f96d --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1,8 @@ +from flask import Blueprint + +bp_errors = Blueprint('errors', __name__) +bp = Blueprint('api', __name__) +bp_product = Blueprint('product', __name__, url_prefix="/product") +bp_user = Blueprint('user', __name__, url_prefix="/user") + +from . import routes \ No newline at end of file diff --git a/app/api/__pycache__/__init__.cpython-310.pyc b/app/api/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..dd13cd7 Binary files /dev/null and b/app/api/__pycache__/__init__.cpython-310.pyc differ diff --git a/app/api/__pycache__/__init__.cpython-311.pyc b/app/api/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..69aaf01 Binary files /dev/null and b/app/api/__pycache__/__init__.cpython-311.pyc differ diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py new file mode 100644 index 0000000..9a49a8c --- /dev/null +++ b/app/api/routes/__init__.py @@ -0,0 +1 @@ +from . import main_routes,error_routes, product_routes, user_routes \ No newline at end of file diff --git a/app/api/routes/__pycache__/__init__.cpython-310.pyc b/app/api/routes/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..d8f16cc Binary files /dev/null and b/app/api/routes/__pycache__/__init__.cpython-310.pyc differ diff --git a/app/api/routes/__pycache__/__init__.cpython-311.pyc b/app/api/routes/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..f0c4c3c Binary files /dev/null and b/app/api/routes/__pycache__/__init__.cpython-311.pyc differ diff --git a/app/api/routes/__pycache__/error.cpython-311.pyc b/app/api/routes/__pycache__/error.cpython-311.pyc new file mode 100644 index 0000000..74ba1f1 Binary files /dev/null and b/app/api/routes/__pycache__/error.cpython-311.pyc differ diff --git a/app/api/routes/__pycache__/error_routes.cpython-310.pyc b/app/api/routes/__pycache__/error_routes.cpython-310.pyc new file mode 100644 index 0000000..4e400a7 Binary files /dev/null and b/app/api/routes/__pycache__/error_routes.cpython-310.pyc differ diff --git a/app/api/routes/__pycache__/error_routes.cpython-311.pyc b/app/api/routes/__pycache__/error_routes.cpython-311.pyc new file mode 100644 index 0000000..206d2ca Binary files /dev/null and b/app/api/routes/__pycache__/error_routes.cpython-311.pyc differ diff --git a/app/api/routes/__pycache__/main_routes.cpython-310.pyc b/app/api/routes/__pycache__/main_routes.cpython-310.pyc new file mode 100644 index 0000000..bc71dca Binary files /dev/null and b/app/api/routes/__pycache__/main_routes.cpython-310.pyc differ diff --git a/app/api/routes/__pycache__/main_routes.cpython-311.pyc b/app/api/routes/__pycache__/main_routes.cpython-311.pyc new file mode 100644 index 0000000..254e589 Binary files /dev/null and b/app/api/routes/__pycache__/main_routes.cpython-311.pyc differ diff --git a/app/api/routes/__pycache__/product.cpython-311.pyc b/app/api/routes/__pycache__/product.cpython-311.pyc new file mode 100644 index 0000000..2b1dd16 Binary files /dev/null and b/app/api/routes/__pycache__/product.cpython-311.pyc differ diff --git a/app/api/routes/__pycache__/product_routes.cpython-310.pyc b/app/api/routes/__pycache__/product_routes.cpython-310.pyc new file mode 100644 index 0000000..639a622 Binary files /dev/null and b/app/api/routes/__pycache__/product_routes.cpython-310.pyc differ diff --git a/app/api/routes/__pycache__/product_routes.cpython-311.pyc b/app/api/routes/__pycache__/product_routes.cpython-311.pyc new file mode 100644 index 0000000..9d57ad6 Binary files /dev/null and b/app/api/routes/__pycache__/product_routes.cpython-311.pyc differ diff --git a/app/api/routes/__pycache__/user_routes.cpython-310.pyc b/app/api/routes/__pycache__/user_routes.cpython-310.pyc new file mode 100644 index 0000000..ca238c2 Binary files /dev/null and b/app/api/routes/__pycache__/user_routes.cpython-310.pyc differ diff --git a/app/api/routes/__pycache__/user_routes.cpython-311.pyc b/app/api/routes/__pycache__/user_routes.cpython-311.pyc new file mode 100644 index 0000000..6355a94 Binary files /dev/null and b/app/api/routes/__pycache__/user_routes.cpython-311.pyc differ diff --git a/app/api/routes/error_routes.py b/app/api/routes/error_routes.py new file mode 100644 index 0000000..343d9e9 --- /dev/null +++ b/app/api/routes/error_routes.py @@ -0,0 +1,29 @@ +from .. import bp_errors + +@bp_errors.app_errorhandler(400) +def bad_request(e): + return {"Bad Request": "The request was incorrectly formatted, or contained invalid data"}, 400 + +@bp_errors.app_errorhandler(401) +def unauthorized(e): + return {"Unauthorized": "Failed to authorize the request"}, 401 + +@bp_errors.app_errorhandler(403) +def forbidden(e): + return {"Forbidden": "Forbidden from accessing this resource. Try logging in"}, 403 + +@bp_errors.app_errorhandler(404) +def not_found(e): + return {"Not Found": "The requested resource was not found"}, 404 + +@bp_errors.app_errorhandler(405) +def method_not_allowed(e): + return {"Method Not Allowed": "The method used is not allowed in current context"}, 405 + +@bp_errors.app_errorhandler(500) +def internal_error(e): + return {"Internal Server Error": "An error occured on he server"}, 500 + +@bp_errors.app_errorhandler(501) +def internal_error(e): + return {"Not Implemented": "This function has not been implemented yet. Check back soon!"}, 501 \ No newline at end of file diff --git a/app/api/routes/main_routes.py b/app/api/routes/main_routes.py new file mode 100644 index 0000000..fcb06a0 --- /dev/null +++ b/app/api/routes/main_routes.py @@ -0,0 +1,7 @@ +from flask import jsonify, abort + +from .. import bp + +@bp.route('/') +def hello(): + return jsonify({'message': 'Hello, Flask!'}) \ No newline at end of file diff --git a/app/api/routes/product_routes.py b/app/api/routes/product_routes.py new file mode 100644 index 0000000..271a23b --- /dev/null +++ b/app/api/routes/product_routes.py @@ -0,0 +1,63 @@ +from flask import jsonify, abort, request + +from app.api import bp_product + +from app.services.product_service import ProductService + +@bp_product.route('/', methods=['GET']) +def all_product_info(id: int): + result = ProductService.get_all_info(id) + + if result is not None: + return jsonify(result) + else: + abort(404) + +@bp_product.route('//name', methods=['GET']) +def get_name(id: int): + result = ProductService.get_name(id) + + if result is not None: + return jsonify({"name": result}) + else: + return abort(404) + +@bp_product.route('//manufacturer', methods=['GET']) +def get_manufacturer(id: int): + result = ProductService.get_manufacturer(id) + + if result is not None: + return jsonify({"name": result}) + else: + return abort(404) + +@bp_product.route('//price', methods=['GET']) +def get_price(id: int): + result = ProductService.get_price(id) + + if result is not None: + return jsonify({"price": result}) + else: + return abort(404) + +@bp_product.route('//image', methods=['GET']) +def get_image(id: int): + result = ProductService.get_image(id) + + if result is not None: + return jsonify({"image": result}) + else: + return abort(404) + +@bp_product.route('//image_name', methods=['GET']) +def get_image_name(id: int): + result = ProductService.get_image_name(id) + + if result is not None: + return jsonify({"image_name": result}) + else: + return abort(404) + +@bp_product.route('/create', methods=['POST']) +def create_product_listing(): + return abort(501) \ No newline at end of file diff --git a/app/api/routes/user_routes.py b/app/api/routes/user_routes.py new file mode 100644 index 0000000..b33d9b8 --- /dev/null +++ b/app/api/routes/user_routes.py @@ -0,0 +1,79 @@ +from app.api import bp_user +from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt +from flask import request, abort, jsonify +from datetime import timedelta + +from app.services.user_service import UserService +from app.extensions import jwt_redis_blocklist + +@bp_user.route('/login', methods=['POST']) +def login(): + username = request.json.get('username') + password = request.json.get('password') + + if username is None or password is None: + return abort(400) + + result, status_code = UserService.login(username, password) + + return jsonify(**result), status_code + +@bp_user.route('/logout', methods=['DELETE']) +@jwt_required() +def logout(): + jti = get_jwt()["jti"] + jwt_redis_blocklist.set(jti, "", ex=timedelta(days=1)) + + return {"Success": "Successfully logged out"}, 200 + +@bp_user.route('/create', methods=['POST']) +def create_user(): + username = request.json.get('username') + email = request.json.get('email') + password = request.json.get('password') + + if username is None or email is None or password is None: + return abort(400) + + result, status_code = UserService.create_user(username, email, password) + + return jsonify(**result), status_code + +@bp_user.route('/update/email', methods=['POST']) +@jwt_required() +def update_email(): + username = get_jwt_identity() + new_mail = request.json.get('new_email') + + if new_mail is None: + return abort(400) + + result, status_code = UserService.update_email(username, new_mail) + + return jsonify(**result), status_code + +@bp_user.route('/update/username', methods=['POST']) +@jwt_required() +def update_username(): + username = get_jwt_identity() + new_username = request.json.get('new_username') + + if new_username is None: + return abort(400) + + result, status_code = UserService.update_username(username, new_username) + + return jsonify(**result), status_code + +@bp_user.route('/update/password', methods=['POST']) +@jwt_required() +def update_password(): + username = get_jwt_identity() + new_password = request.json.get('new_password') + + if new_password is None: + return abort(400) + + result, status_code = UserService.update_password(username, new_password) + + return jsonify(**result), status_code \ No newline at end of file diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..438cda7 --- /dev/null +++ b/app/config.py @@ -0,0 +1,22 @@ +import os + +class MySqlConfig: + MYSQL_USER = os.environ.get('MYSQL_USER') + MYSQL_DATABASE = os.environ.get('MYSQL_DATABASE') + MYSQL_HOST = os.environ.get('MYSQL_HOST') + MYSQL_PORT = os.environ.get('MYSQL_PORT') + MYSQL_PASSWORD = os.environ.get('MYSQL_PASSWORD') + +class RedisConfig: + REDIS_HOST = os.environ.get('REDIS_HOST') + REDIS_PORT = os.environ.get('REDIS_PORT') + +class FlaskProduction: + DEBUG = False + JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') + SERVER_NAME = os.environ.get('HOST') + ':' + os.environ.get('PORT') + +class FlaskTesting: + DEBUG = True + JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') + SERVER_NAME = os.environ.get('HOST') + ':' + os.environ.get('PORT') \ No newline at end of file diff --git a/app/extensions.py b/app/extensions.py new file mode 100644 index 0000000..7d8db09 --- /dev/null +++ b/app/extensions.py @@ -0,0 +1,22 @@ +import mysql.connector +import redis +import os + +from app.config import RedisConfig +from app.config import MySqlConfig + +db_connection = mysql.connector.connect( + host=MySqlConfig.MYSQL_HOST, + user=MySqlConfig.MYSQL_USER, + password=MySqlConfig.MYSQL_PASSWORD, + database=MySqlConfig.MYSQL_DATABASE, +) + +db_cursor = db_connection.cursor() + +jwt_redis_blocklist = redis.StrictRedis( + host=RedisConfig.REDIS_HOST, + port=RedisConfig.REDIS_PORT, + db=0, + decode_responses=True +) diff --git a/app/jwt_utils.py b/app/jwt_utils.py new file mode 100644 index 0000000..aec66f9 --- /dev/null +++ b/app/jwt_utils.py @@ -0,0 +1,15 @@ +from app.extensions import jwt_redis_blocklist + +from flask_jwt_extended import create_access_token +from flask_jwt_extended import get_jwt +from flask_jwt_extended import jwt_required +from flask_jwt_extended import JWTManager + +@jwt.token_in_blocklist_loader +def check_if_token_is_revoked(jwt_header, jwt_payload: dict) -> bool: + jti = jwt_payload["jti"] + token_in_redis = jwt_redis_blocklist.get(jti) + + print(token_in_redis) + + return token_in_redis is not None diff --git a/app/services/__pycache__/jwt_check_service.cpython-311.pyc b/app/services/__pycache__/jwt_check_service.cpython-311.pyc new file mode 100644 index 0000000..cf0bee5 Binary files /dev/null and b/app/services/__pycache__/jwt_check_service.cpython-311.pyc differ diff --git a/app/services/__pycache__/product_service.cpython-310.pyc b/app/services/__pycache__/product_service.cpython-310.pyc new file mode 100644 index 0000000..583d184 Binary files /dev/null and b/app/services/__pycache__/product_service.cpython-310.pyc differ diff --git a/app/services/__pycache__/product_service.cpython-311.pyc b/app/services/__pycache__/product_service.cpython-311.pyc new file mode 100644 index 0000000..a473fd2 Binary files /dev/null and b/app/services/__pycache__/product_service.cpython-311.pyc differ diff --git a/app/services/__pycache__/user_service.cpython-310.pyc b/app/services/__pycache__/user_service.cpython-310.pyc new file mode 100644 index 0000000..001c894 Binary files /dev/null and b/app/services/__pycache__/user_service.cpython-310.pyc differ diff --git a/app/services/__pycache__/user_service.cpython-311.pyc b/app/services/__pycache__/user_service.cpython-311.pyc new file mode 100644 index 0000000..d593e0e Binary files /dev/null and b/app/services/__pycache__/user_service.cpython-311.pyc differ diff --git a/app/services/product_service.py b/app/services/product_service.py new file mode 100644 index 0000000..92e2a18 --- /dev/null +++ b/app/services/product_service.py @@ -0,0 +1,52 @@ +import base64 + +from ..extensions import db_cursor as cursor + +class ProductService: + + @staticmethod + def get_name(product_id: int): + cursor.execute(f"select name from product where product.product_id = {product_id}") + result = cursor.fetchone() + return result[0] + + @staticmethod + def get_manufacturer(product_id: int): + cursor.execute(f"select manufacturer from product where product.product_id = {product_id}") + result = cursor.fetchone() + return result[0] + + @staticmethod + def get_price(product_id: int): + cursor.execute(f"select price_pc from product where product.product_id = {product_id}") + result = cursor.fetchone() + return result[0] + + @staticmethod + def get_image(product_id: int): + cursor.execute(f"select image from product where product.product_id = {product_id}") + result = cursor.fetchone() + return base64.b64encode(result[0]).decode('utf-8') + + @staticmethod + def get_image_name(product_id: int): + cursor.execute(f"select image_name from product where product.product_id = {product_id}") + result = cursor.fetchone() + return result[0] + + @staticmethod + def get_all_info(product_id: int): + cursor.execute(f"select name,manufacturer,price_pc,image_name,image from product where product.product_id = {product_id}") + result = cursor.fetchone() + + return { + "name": result[0], + "manufacturer": result[1], + "price": result[2], + "image_name": result[3], + "image": base64.b64encode(result[4]).decode('utf-8') + } + + @staticmethod + def create_user(username: str, email: str, password: str): + print("asd") \ No newline at end of file diff --git a/app/services/user_service.py b/app/services/user_service.py new file mode 100644 index 0000000..cd779ba --- /dev/null +++ b/app/services/user_service.py @@ -0,0 +1,129 @@ +import bcrypt +import re +import jwt +import datetime +from typing import Tuple, Union + +from app.extensions import db_cursor, db_connection +from mysql.connector import Error + +from flask_jwt_extended import create_access_token + + +class UserService: + + @staticmethod + def create_user(username: str, email: str, password: str) -> Tuple[Union[dict, str], int]: + + if not UserService.__verify_username(username): + return {"Failed": "Failed to verify username. Try another username"}, 400 + + if not UserService.__verify_email(email): + return {"Failed": "Failed to verify email. Try another email"}, 400 + + if not UserService.__verify_password(password): + return {"Failed": "Failed to verify password. Try another (stronger) password"}, 400 + + # Role ID 1 => Normal user + # Role ID 2 => Seller + # Role ID 3 => Admin + + hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) + + try: + db_cursor.execute("select max(user_id) from user") + last_id = db_cursor.fetchone()[0] + + if last_id < 23000: + return {"Failed": "Error occured when fetching last user id"} + + new_id = last_id + 1 + + db_cursor.execute("insert into user (username, email, password, user_id, role_id) values (%s, %s, %s, %s, 1)", (username, email, hashed_password, new_id)) + db_connection.commit() + except Error as e: + print(f"Error: {e}") + return {"Failed": "Failed to insert into database. Username or email are likely in use already"}, 500 + + return {"Success": "User created successfully"}, 200 + + @staticmethod + def login(username: str, password: str) -> Tuple[Union[dict, str], int]: + + db_cursor.execute("select user_id, password, last_change from user where username = %s", (username,)) + result = db_cursor.fetchone() + + user_id = result[0] + password_hash = result[1] + last_change = result[2] + + if user_id is None: + return {"Failed": "Username not found"}, 400 + + if not bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8')): + return {"Failed": "Incorrect password"}, 401 + + expire = datetime.timedelta(days=1) + + token = create_access_token(identity=user_id, expires_delta=expire,additional_claims={"lm": last_change}) + + return {"token": token}, 200 + + @staticmethod + def update_email(user_id: str, new_email: str) -> Tuple[Union[dict, str], int]: + + if not UserService.__verify_email(new_email): + return {"Failed": "Failed to verify email. Try another email"}, 400 + + try: + db_cursor.execute("update user set email = %s where user_id = %s", (new_email, user_id)) + db_connection.commit() + except Error as e: + return {"Failed": f"Failed to update email. Email is likely in use already. Error: {e}"}, 500 + + return {"Success": "Email successfully updated"}, 200 + + @staticmethod + def update_username(user_id: str, new_username: str) -> Tuple[Union[dict, str], int]: + + if not UserService.__verify_username(new_username): + return {"Failed": "Failed to verify username. Try another one"}, 400 + + try: + db_cursor.execute("update user set username = %s where user_id = %s", (new_username, user_id)) + db_connection.commit() + except Error as e: + return {"Failed": f"Failed to update username. Username is likely in use already. Error: {e}"}, 500 + + return {"Success": "Username successfully updated"}, 200 + + @staticmethod + def update_password(user_id: str, new_password: str) -> Tuple[Union[dict, str], int]: + + if not UserService.__verify_password(new_password): + return {"Failed": "Failed to verify password. Try another (stronger) one"}, 400 + + hashed_password = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt()) + + try: + db_cursor.execute("update user set password = %s where user_id = %s", (new_username, user_id)) + db_connection.commit() + except Error as e: + return {"Failed": f"Failed to update password. Error: {e}"}, 500 + + return {"Success": "Password successfully updated"}, 200 + + @staticmethod + def __verify_email(email: str) -> bool: + email_regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" + return re.match(email_regex ,email) and len(email) <= 64 + + @staticmethod + def __verify_username(username: str) -> bool: + username_regex = r"^[a-zA-Z.-_]{1,64}$" + return re.match(username_regex, username) + + @staticmethod + def __verify_password(password: str) -> bool: + password_regex = r"^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,64}$" + return re.match(password_regex, password) diff --git a/main.py b/main.py new file mode 100644 index 0000000..a76f213 --- /dev/null +++ b/main.py @@ -0,0 +1,13 @@ +from dotenv import load_dotenv +from flask_jwt_extended import JWTManager +import os + +from app import create_app + +load_dotenv() + +app = create_app() + +if __name__ == "__main__": + print("Hello, Flask") + app.run() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7999f0f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +Flask==2.3.3 +gunicorn==20.1.0 +mysql-connector-python==8.3.0 +python-dotenv==1.0.1 +Flask-JWT-Extended==4.5.3 +PyJWT==2.8.0 \ No newline at end of file