diff --git a/my_flask_app.py b/my_flask_app.py
index efe9112fe2ae894ece2f7e39669ebdafff58a435..25ce3523f03fe88bd42ebde543cca6fb95b0c15d 100644
--- a/my_flask_app.py
+++ b/my_flask_app.py
@@ -2,7 +2,9 @@ import os
 import secrets
 from flask import Flask, render_template, request, redirect, url_for, send_from_directory, abort
 from flask_sqlalchemy import SQLAlchemy
-from models import db, Project
+from flask_wtf import FlaskForm
+from wtforms import StringField, TextAreaField, SubmitField
+from wtforms.validators import DataRequired
 
 app = Flask(__name__, static_folder='static')
 app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
@@ -10,11 +12,17 @@ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.abspath(os.path.j
 app.config['SECRET_KEY'] = secrets.token_hex(16)
 
 db = SQLAlchemy(app)
+
 class Project(db.Model):
     id = db.Column(db.Integer, primary_key=True)
     title = db.Column(db.String(100), nullable=False)
     description = db.Column(db.Text, nullable=False)
 
+class ProjectForm(FlaskForm):
+    title = StringField('Title', validators=[DataRequired()])
+    description = TextAreaField('Description', validators=[DataRequired()])
+    submit = SubmitField('Submit')
+
 @app.route('/')
 def home():
     try:
@@ -46,23 +54,20 @@ def portfolio():
 def contact():
     return render_template('contact.html')
 
-# Updated route for adding a project without Flask-WTF form
+# Updated route for adding a project with Flask-WTF form
 @app.route('/add_project', methods=['GET', 'POST'])
 def add_project():
-    if request.method == 'POST':
-        # Retrieve form data directly from request
-        title = request.form.get('title')
-        description = request.form.get('description')
-
-        # Print or log the form data to check if it's received
-        print(f"Received form data - Title: {title}, Description: {description}")
+    form = ProjectForm()
+    if form.validate_on_submit():
+        title = form.title.data
+        description = form.description.data
 
         new_project = Project(title=title, description=description)
         db.session.add(new_project)
         db.session.commit()
         return redirect(url_for('home'))
 
-    return render_template('add_project.html')
+    return render_template('add_project.html', form=form)
 
 # Updated route for serving the 'my-cv.docx' file
 @app.route('/download_cv')
@@ -75,20 +80,17 @@ def download_cv():
 @app.route('/download_assessment/<filename>')
 def download_assessment(filename):
     try:
-        file_path = f'static/{filename}'
-        print(f"Attempting to serve file: {file_path}")
+        file_path = os.path.join('static', filename)
+        app.logger.debug(f"Attempting to serve file: {file_path}")
         return send_from_directory('static', filename, as_attachment=True)
     except FileNotFoundError:
-        print(f"File not found: {file_path}")
+        app.logger.error(f"File not found: {file_path}")
         abort(404)  # Return a 404 Not Found error
     except Exception as e:
-        print(f"Error serving assessment file: {str(e)}")
-        app.logger.exception(f"Error serving assessment file: {str(e)}")
-        abort(500) 
+        app.logger.error(f"Error serving assessment file: {str(e)}")
+        abort(500)
 
 if __name__ == '__main__':
     with app.app_context():
         db.create_all()
     app.run(debug=True, port=int(os.environ.get('PORT', 8080)))
-
-
diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/INSTALLER b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/INSTALLER
new file mode 100644
index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/METADATA b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/METADATA
new file mode 100644
index 0000000000000000000000000000000000000000..92f1ff25658838ae9639052003770899dc3851cb
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/METADATA
@@ -0,0 +1,72 @@
+Metadata-Version: 2.1
+Name: Flask-WTF
+Version: 1.2.1
+Summary: Form rendering, validation, and CSRF protection for Flask with WTForms.
+Project-URL: Documentation, https://flask-wtf.readthedocs.io/
+Project-URL: Changes, https://flask-wtf.readthedocs.io/changes/
+Project-URL: Source Code, https://github.com/wtforms/flask-wtf/
+Project-URL: Issue Tracker, https://github.com/wtforms/flask-wtf/issues/
+Project-URL: Chat, https://discord.gg/pallets
+Maintainer: WTForms
+License: Copyright 2010 WTForms
+        
+        Redistribution and use in source and binary forms, with or without
+        modification, are permitted provided that the following conditions are
+        met:
+        
+        1.  Redistributions of source code must retain the above copyright
+            notice, this list of conditions and the following disclaimer.
+        
+        2.  Redistributions in binary form must reproduce the above copyright
+            notice, this list of conditions and the following disclaimer in the
+            documentation and/or other materials provided with the distribution.
+        
+        3.  Neither the name of the copyright holder nor the names of its
+            contributors may be used to endorse or promote products derived from
+            this software without specific prior written permission.
+        
+        THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+        "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+        LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+        PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+        HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+        SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+        TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+        PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+        LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+        NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+        SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+License-File: LICENSE.rst
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
+Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
+Requires-Python: >=3.8
+Requires-Dist: flask
+Requires-Dist: itsdangerous
+Requires-Dist: wtforms
+Provides-Extra: email
+Requires-Dist: email-validator; extra == 'email'
+Description-Content-Type: text/x-rst
+
+Flask-WTF
+=========
+
+Simple integration of Flask and WTForms, including CSRF, file upload,
+and reCAPTCHA.
+
+Links
+-----
+
+-   Documentation: https://flask-wtf.readthedocs.io/
+-   Changes: https://flask-wtf.readthedocs.io/changes/
+-   PyPI Releases: https://pypi.org/project/Flask-WTF/
+-   Source Code: https://github.com/wtforms/flask-wtf/
+-   Issue Tracker: https://github.com/wtforms/flask-wtf/issues/
+-   Chat: https://discord.gg/pallets
diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/RECORD b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/RECORD
new file mode 100644
index 0000000000000000000000000000000000000000..a7814665f525f142935e652b0eafe2213194eccf
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/RECORD
@@ -0,0 +1,26 @@
+flask_wtf-1.2.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+flask_wtf-1.2.1.dist-info/METADATA,sha256=9Y5upDJ7WU2m2l4erWImF3HcVSWIZKH3TdX6klYpq4M,3373
+flask_wtf-1.2.1.dist-info/RECORD,,
+flask_wtf-1.2.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+flask_wtf-1.2.1.dist-info/WHEEL,sha256=9QBuHhg6FNW7lppboF2vKVbCGTVzsFykgRQjjlajrhA,87
+flask_wtf-1.2.1.dist-info/licenses/LICENSE.rst,sha256=1fGQNkUVeMs27u8EyZ6_fXyi5w3PBDY2UZvEIOFafGI,1475
+flask_wtf/__init__.py,sha256=x6ydw5SJzsXZgz-Y6IM_95Sy8VufRepvZH1DUIlFoTo,214
+flask_wtf/__pycache__/__init__.cpython-311.pyc,,
+flask_wtf/__pycache__/_compat.cpython-311.pyc,,
+flask_wtf/__pycache__/csrf.cpython-311.pyc,,
+flask_wtf/__pycache__/file.cpython-311.pyc,,
+flask_wtf/__pycache__/form.cpython-311.pyc,,
+flask_wtf/__pycache__/i18n.cpython-311.pyc,,
+flask_wtf/_compat.py,sha256=N3sqC9yzFWY-3MZ7QazX1sidvkO3d5yy4NR6lkp0s94,248
+flask_wtf/csrf.py,sha256=O-fjnWygxxi_FsIU2koua97ZpIhiOJVDHA57dXLpvTA,10171
+flask_wtf/file.py,sha256=AsfkYTCgtqGWySimc_NjeAxg-DtpdcthhqMLrXIDAhU,4706
+flask_wtf/form.py,sha256=TmR7xCrxin2LHp6thn7fq1OeU8aLB7xsZzvv52nH7Ss,4049
+flask_wtf/i18n.py,sha256=TyO8gqt9DocHMSaNhj0KKgxoUrPYs-G1nVW-jns0SOw,1166
+flask_wtf/recaptcha/__init__.py,sha256=m4eNGoU3Q0Wnt_wP8VvOlA0mwWuoMtAcK9pYT7sPFp8,106
+flask_wtf/recaptcha/__pycache__/__init__.cpython-311.pyc,,
+flask_wtf/recaptcha/__pycache__/fields.cpython-311.pyc,,
+flask_wtf/recaptcha/__pycache__/validators.cpython-311.pyc,,
+flask_wtf/recaptcha/__pycache__/widgets.cpython-311.pyc,,
+flask_wtf/recaptcha/fields.py,sha256=M1-RFuUKOsJAzsLm3xaaxuhX2bB9oRqS-HVSN-NpkmI,433
+flask_wtf/recaptcha/validators.py,sha256=3sd1mUQT3Y3D_WJeKwecxUGstnhh_QD-A_dEBJfkf6s,2434
+flask_wtf/recaptcha/widgets.py,sha256=J_XyxAZt3uB15diIMnkXXGII2dmsWCsVsKV3KQYn4Ns,1512
diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/REQUESTED b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/REQUESTED
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/WHEEL b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/WHEEL
new file mode 100644
index 0000000000000000000000000000000000000000..ba1a8af28bcccdacebb8c22dfda1537447a1a58a
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/WHEEL
@@ -0,0 +1,4 @@
+Wheel-Version: 1.0
+Generator: hatchling 1.18.0
+Root-Is-Purelib: true
+Tag: py3-none-any
diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/licenses/LICENSE.rst b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/licenses/LICENSE.rst
new file mode 100644
index 0000000000000000000000000000000000000000..63c3617a2d7164d30cae358c23eb3f75b5a758a1
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/licenses/LICENSE.rst
@@ -0,0 +1,28 @@
+Copyright 2010 WTForms
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1.  Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+
+2.  Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in the
+    documentation and/or other materials provided with the distribution.
+
+3.  Neither the name of the copyright holder nor the names of its
+    contributors may be used to endorse or promote products derived from
+    this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/venv/Lib/site-packages/flask_wtf/__init__.py b/venv/Lib/site-packages/flask_wtf/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..be2649e26d8dfa2cde5457f13b72715135d12b5a
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/__init__.py
@@ -0,0 +1,8 @@
+from .csrf import CSRFProtect
+from .form import FlaskForm
+from .form import Form
+from .recaptcha import Recaptcha
+from .recaptcha import RecaptchaField
+from .recaptcha import RecaptchaWidget
+
+__version__ = "1.2.1"
diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/__init__.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7c0573b1438c137de88c7976d43ddff37c7ebb80
Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/__pycache__/__init__.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/_compat.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/_compat.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..bf6047bf2e16901a5247250b9ec7f46df2d116ef
Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/__pycache__/_compat.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/csrf.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/csrf.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..9a7b10bdeeef961ea8006709ee5d3d18fdcf34cd
Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/__pycache__/csrf.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/file.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/file.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ff1baf217281277c65ce5ebaa756d3a5b1c90463
Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/__pycache__/file.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/form.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/form.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..68c6e91088694c15d2928b11f2eebd165dd1d435
Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/__pycache__/form.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/i18n.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/i18n.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2b7bd93f3fe31e810f7d0208a568d028cfbf0c82
Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/__pycache__/i18n.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/flask_wtf/_compat.py b/venv/Lib/site-packages/flask_wtf/_compat.py
new file mode 100644
index 0000000000000000000000000000000000000000..50973e063bbdbd6982fc9501221603efbc2e88f9
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/_compat.py
@@ -0,0 +1,11 @@
+import warnings
+
+
+class FlaskWTFDeprecationWarning(DeprecationWarning):
+    pass
+
+
+warnings.simplefilter("always", FlaskWTFDeprecationWarning)
+warnings.filterwarnings(
+    "ignore", category=FlaskWTFDeprecationWarning, module="wtforms|flask_wtf"
+)
diff --git a/venv/Lib/site-packages/flask_wtf/csrf.py b/venv/Lib/site-packages/flask_wtf/csrf.py
new file mode 100644
index 0000000000000000000000000000000000000000..06afa0cd4ef3670ca3357d47bbecc2baa7e18fe0
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/csrf.py
@@ -0,0 +1,329 @@
+import hashlib
+import hmac
+import logging
+import os
+from urllib.parse import urlparse
+
+from flask import Blueprint
+from flask import current_app
+from flask import g
+from flask import request
+from flask import session
+from itsdangerous import BadData
+from itsdangerous import SignatureExpired
+from itsdangerous import URLSafeTimedSerializer
+from werkzeug.exceptions import BadRequest
+from wtforms import ValidationError
+from wtforms.csrf.core import CSRF
+
+__all__ = ("generate_csrf", "validate_csrf", "CSRFProtect")
+logger = logging.getLogger(__name__)
+
+
+def generate_csrf(secret_key=None, token_key=None):
+    """Generate a CSRF token. The token is cached for a request, so multiple
+    calls to this function will generate the same token.
+
+    During testing, it might be useful to access the signed token in
+    ``g.csrf_token`` and the raw token in ``session['csrf_token']``.
+
+    :param secret_key: Used to securely sign the token. Default is
+        ``WTF_CSRF_SECRET_KEY`` or ``SECRET_KEY``.
+    :param token_key: Key where token is stored in session for comparison.
+        Default is ``WTF_CSRF_FIELD_NAME`` or ``'csrf_token'``.
+    """
+
+    secret_key = _get_config(
+        secret_key,
+        "WTF_CSRF_SECRET_KEY",
+        current_app.secret_key,
+        message="A secret key is required to use CSRF.",
+    )
+    field_name = _get_config(
+        token_key,
+        "WTF_CSRF_FIELD_NAME",
+        "csrf_token",
+        message="A field name is required to use CSRF.",
+    )
+
+    if field_name not in g:
+        s = URLSafeTimedSerializer(secret_key, salt="wtf-csrf-token")
+
+        if field_name not in session:
+            session[field_name] = hashlib.sha1(os.urandom(64)).hexdigest()
+
+        try:
+            token = s.dumps(session[field_name])
+        except TypeError:
+            session[field_name] = hashlib.sha1(os.urandom(64)).hexdigest()
+            token = s.dumps(session[field_name])
+
+        setattr(g, field_name, token)
+
+    return g.get(field_name)
+
+
+def validate_csrf(data, secret_key=None, time_limit=None, token_key=None):
+    """Check if the given data is a valid CSRF token. This compares the given
+    signed token to the one stored in the session.
+
+    :param data: The signed CSRF token to be checked.
+    :param secret_key: Used to securely sign the token. Default is
+        ``WTF_CSRF_SECRET_KEY`` or ``SECRET_KEY``.
+    :param time_limit: Number of seconds that the token is valid. Default is
+        ``WTF_CSRF_TIME_LIMIT`` or 3600 seconds (60 minutes).
+    :param token_key: Key where token is stored in session for comparison.
+        Default is ``WTF_CSRF_FIELD_NAME`` or ``'csrf_token'``.
+
+    :raises ValidationError: Contains the reason that validation failed.
+
+    .. versionchanged:: 0.14
+        Raises ``ValidationError`` with a specific error message rather than
+        returning ``True`` or ``False``.
+    """
+
+    secret_key = _get_config(
+        secret_key,
+        "WTF_CSRF_SECRET_KEY",
+        current_app.secret_key,
+        message="A secret key is required to use CSRF.",
+    )
+    field_name = _get_config(
+        token_key,
+        "WTF_CSRF_FIELD_NAME",
+        "csrf_token",
+        message="A field name is required to use CSRF.",
+    )
+    time_limit = _get_config(time_limit, "WTF_CSRF_TIME_LIMIT", 3600, required=False)
+
+    if not data:
+        raise ValidationError("The CSRF token is missing.")
+
+    if field_name not in session:
+        raise ValidationError("The CSRF session token is missing.")
+
+    s = URLSafeTimedSerializer(secret_key, salt="wtf-csrf-token")
+
+    try:
+        token = s.loads(data, max_age=time_limit)
+    except SignatureExpired as e:
+        raise ValidationError("The CSRF token has expired.") from e
+    except BadData as e:
+        raise ValidationError("The CSRF token is invalid.") from e
+
+    if not hmac.compare_digest(session[field_name], token):
+        raise ValidationError("The CSRF tokens do not match.")
+
+
+def _get_config(
+    value, config_name, default=None, required=True, message="CSRF is not configured."
+):
+    """Find config value based on provided value, Flask config, and default
+    value.
+
+    :param value: already provided config value
+    :param config_name: Flask ``config`` key
+    :param default: default value if not provided or configured
+    :param required: whether the value must not be ``None``
+    :param message: error message if required config is not found
+    :raises KeyError: if required config is not found
+    """
+
+    if value is None:
+        value = current_app.config.get(config_name, default)
+
+    if required and value is None:
+        raise RuntimeError(message)
+
+    return value
+
+
+class _FlaskFormCSRF(CSRF):
+    def setup_form(self, form):
+        self.meta = form.meta
+        return super().setup_form(form)
+
+    def generate_csrf_token(self, csrf_token_field):
+        return generate_csrf(
+            secret_key=self.meta.csrf_secret, token_key=self.meta.csrf_field_name
+        )
+
+    def validate_csrf_token(self, form, field):
+        if g.get("csrf_valid", False):
+            # already validated by CSRFProtect
+            return
+
+        try:
+            validate_csrf(
+                field.data,
+                self.meta.csrf_secret,
+                self.meta.csrf_time_limit,
+                self.meta.csrf_field_name,
+            )
+        except ValidationError as e:
+            logger.info(e.args[0])
+            raise
+
+
+class CSRFProtect:
+    """Enable CSRF protection globally for a Flask app.
+
+    ::
+
+        app = Flask(__name__)
+        csrf = CSRFProtect(app)
+
+    Checks the ``csrf_token`` field sent with forms, or the ``X-CSRFToken``
+    header sent with JavaScript requests. Render the token in templates using
+    ``{{ csrf_token() }}``.
+
+    See the :ref:`csrf` documentation.
+    """
+
+    def __init__(self, app=None):
+        self._exempt_views = set()
+        self._exempt_blueprints = set()
+
+        if app:
+            self.init_app(app)
+
+    def init_app(self, app):
+        app.extensions["csrf"] = self
+
+        app.config.setdefault("WTF_CSRF_ENABLED", True)
+        app.config.setdefault("WTF_CSRF_CHECK_DEFAULT", True)
+        app.config["WTF_CSRF_METHODS"] = set(
+            app.config.get("WTF_CSRF_METHODS", ["POST", "PUT", "PATCH", "DELETE"])
+        )
+        app.config.setdefault("WTF_CSRF_FIELD_NAME", "csrf_token")
+        app.config.setdefault("WTF_CSRF_HEADERS", ["X-CSRFToken", "X-CSRF-Token"])
+        app.config.setdefault("WTF_CSRF_TIME_LIMIT", 3600)
+        app.config.setdefault("WTF_CSRF_SSL_STRICT", True)
+
+        app.jinja_env.globals["csrf_token"] = generate_csrf
+        app.context_processor(lambda: {"csrf_token": generate_csrf})
+
+        @app.before_request
+        def csrf_protect():
+            if not app.config["WTF_CSRF_ENABLED"]:
+                return
+
+            if not app.config["WTF_CSRF_CHECK_DEFAULT"]:
+                return
+
+            if request.method not in app.config["WTF_CSRF_METHODS"]:
+                return
+
+            if not request.endpoint:
+                return
+
+            if app.blueprints.get(request.blueprint) in self._exempt_blueprints:
+                return
+
+            view = app.view_functions.get(request.endpoint)
+            dest = f"{view.__module__}.{view.__name__}"
+
+            if dest in self._exempt_views:
+                return
+
+            self.protect()
+
+    def _get_csrf_token(self):
+        # find the token in the form data
+        field_name = current_app.config["WTF_CSRF_FIELD_NAME"]
+        base_token = request.form.get(field_name)
+
+        if base_token:
+            return base_token
+
+        # if the form has a prefix, the name will be {prefix}-csrf_token
+        for key in request.form:
+            if key.endswith(field_name):
+                csrf_token = request.form[key]
+
+                if csrf_token:
+                    return csrf_token
+
+        # find the token in the headers
+        for header_name in current_app.config["WTF_CSRF_HEADERS"]:
+            csrf_token = request.headers.get(header_name)
+
+            if csrf_token:
+                return csrf_token
+
+        return None
+
+    def protect(self):
+        if request.method not in current_app.config["WTF_CSRF_METHODS"]:
+            return
+
+        try:
+            validate_csrf(self._get_csrf_token())
+        except ValidationError as e:
+            logger.info(e.args[0])
+            self._error_response(e.args[0])
+
+        if request.is_secure and current_app.config["WTF_CSRF_SSL_STRICT"]:
+            if not request.referrer:
+                self._error_response("The referrer header is missing.")
+
+            good_referrer = f"https://{request.host}/"
+
+            if not same_origin(request.referrer, good_referrer):
+                self._error_response("The referrer does not match the host.")
+
+        g.csrf_valid = True  # mark this request as CSRF valid
+
+    def exempt(self, view):
+        """Mark a view or blueprint to be excluded from CSRF protection.
+
+        ::
+
+            @app.route('/some-view', methods=['POST'])
+            @csrf.exempt
+            def some_view():
+                ...
+
+        ::
+
+            bp = Blueprint(...)
+            csrf.exempt(bp)
+
+        """
+
+        if isinstance(view, Blueprint):
+            self._exempt_blueprints.add(view)
+            return view
+
+        if isinstance(view, str):
+            view_location = view
+        else:
+            view_location = ".".join((view.__module__, view.__name__))
+
+        self._exempt_views.add(view_location)
+        return view
+
+    def _error_response(self, reason):
+        raise CSRFError(reason)
+
+
+class CSRFError(BadRequest):
+    """Raise if the client sends invalid CSRF data with the request.
+
+    Generates a 400 Bad Request response with the failure reason by default.
+    Customize the response by registering a handler with
+    :meth:`flask.Flask.errorhandler`.
+    """
+
+    description = "CSRF validation failed."
+
+
+def same_origin(current_uri, compare_uri):
+    current = urlparse(current_uri)
+    compare = urlparse(compare_uri)
+
+    return (
+        current.scheme == compare.scheme
+        and current.hostname == compare.hostname
+        and current.port == compare.port
+    )
diff --git a/venv/Lib/site-packages/flask_wtf/file.py b/venv/Lib/site-packages/flask_wtf/file.py
new file mode 100644
index 0000000000000000000000000000000000000000..a720dff8d81911df179e80512caa0056a47be410
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/file.py
@@ -0,0 +1,147 @@
+from collections import abc
+
+from werkzeug.datastructures import FileStorage
+from wtforms import FileField as _FileField
+from wtforms import MultipleFileField as _MultipleFileField
+from wtforms.validators import DataRequired
+from wtforms.validators import StopValidation
+from wtforms.validators import ValidationError
+
+
+class FileField(_FileField):
+    """Werkzeug-aware subclass of :class:`wtforms.fields.FileField`."""
+
+    def process_formdata(self, valuelist):
+        valuelist = (x for x in valuelist if isinstance(x, FileStorage) and x)
+        data = next(valuelist, None)
+
+        if data is not None:
+            self.data = data
+        else:
+            self.raw_data = ()
+
+
+class MultipleFileField(_MultipleFileField):
+    """Werkzeug-aware subclass of :class:`wtforms.fields.MultipleFileField`.
+
+    .. versionadded:: 1.2.0
+    """
+
+    def process_formdata(self, valuelist):
+        valuelist = (x for x in valuelist if isinstance(x, FileStorage) and x)
+        data = list(valuelist) or None
+
+        if data is not None:
+            self.data = data
+        else:
+            self.raw_data = ()
+
+
+class FileRequired(DataRequired):
+    """Validates that the uploaded files(s) is a Werkzeug
+    :class:`~werkzeug.datastructures.FileStorage` object.
+
+    :param message: error message
+
+    You can also use the synonym ``file_required``.
+    """
+
+    def __call__(self, form, field):
+        field_data = [field.data] if not isinstance(field.data, list) else field.data
+        if not (
+            all(isinstance(x, FileStorage) and x for x in field_data) and field_data
+        ):
+            raise StopValidation(
+                self.message or field.gettext("This field is required.")
+            )
+
+
+file_required = FileRequired
+
+
+class FileAllowed:
+    """Validates that the uploaded file(s) is allowed by a given list of
+    extensions or a Flask-Uploads :class:`~flaskext.uploads.UploadSet`.
+
+    :param upload_set: A list of extensions or an
+        :class:`~flaskext.uploads.UploadSet`
+    :param message: error message
+
+    You can also use the synonym ``file_allowed``.
+    """
+
+    def __init__(self, upload_set, message=None):
+        self.upload_set = upload_set
+        self.message = message
+
+    def __call__(self, form, field):
+        field_data = [field.data] if not isinstance(field.data, list) else field.data
+        if not (
+            all(isinstance(x, FileStorage) and x for x in field_data) and field_data
+        ):
+            return
+
+        filenames = [f.filename.lower() for f in field_data]
+
+        for filename in filenames:
+            if isinstance(self.upload_set, abc.Iterable):
+                if any(filename.endswith("." + x) for x in self.upload_set):
+                    continue
+
+                raise StopValidation(
+                    self.message
+                    or field.gettext(
+                        "File does not have an approved extension: {extensions}"
+                    ).format(extensions=", ".join(self.upload_set))
+                )
+
+            if not self.upload_set.file_allowed(field_data, filename):
+                raise StopValidation(
+                    self.message
+                    or field.gettext("File does not have an approved extension.")
+                )
+
+
+file_allowed = FileAllowed
+
+
+class FileSize:
+    """Validates that the uploaded file(s) is within a minimum and maximum
+    file size (set in bytes).
+
+    :param min_size: minimum allowed file size (in bytes). Defaults to 0 bytes.
+    :param max_size: maximum allowed file size (in bytes).
+    :param message: error message
+
+    You can also use the synonym ``file_size``.
+    """
+
+    def __init__(self, max_size, min_size=0, message=None):
+        self.min_size = min_size
+        self.max_size = max_size
+        self.message = message
+
+    def __call__(self, form, field):
+        field_data = [field.data] if not isinstance(field.data, list) else field.data
+        if not (
+            all(isinstance(x, FileStorage) and x for x in field_data) and field_data
+        ):
+            return
+
+        for f in field_data:
+            file_size = len(f.read())
+            f.seek(0)  # reset cursor position to beginning of file
+
+            if (file_size < self.min_size) or (file_size > self.max_size):
+                # the file is too small or too big => validation failure
+                raise ValidationError(
+                    self.message
+                    or field.gettext(
+                        "File must be between {min_size} and {max_size} bytes.".format(
+                            min_size=self.min_size, max_size=self.max_size
+                        )
+                    )
+                )
+
+
+file_size = FileSize
diff --git a/venv/Lib/site-packages/flask_wtf/form.py b/venv/Lib/site-packages/flask_wtf/form.py
new file mode 100644
index 0000000000000000000000000000000000000000..c7f52e022c82fe43d6674377e5df040c82d10d79
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/form.py
@@ -0,0 +1,127 @@
+from flask import current_app
+from flask import request
+from flask import session
+from markupsafe import Markup
+from werkzeug.datastructures import CombinedMultiDict
+from werkzeug.datastructures import ImmutableMultiDict
+from werkzeug.utils import cached_property
+from wtforms import Form
+from wtforms.meta import DefaultMeta
+from wtforms.widgets import HiddenInput
+
+from .csrf import _FlaskFormCSRF
+
+try:
+    from .i18n import translations
+except ImportError:
+    translations = None  # babel not installed
+
+
+SUBMIT_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
+_Auto = object()
+
+
+class FlaskForm(Form):
+    """Flask-specific subclass of WTForms :class:`~wtforms.form.Form`.
+
+    If ``formdata`` is not specified, this will use :attr:`flask.request.form`
+    and :attr:`flask.request.files`.  Explicitly pass ``formdata=None`` to
+    prevent this.
+    """
+
+    class Meta(DefaultMeta):
+        csrf_class = _FlaskFormCSRF
+        csrf_context = session  # not used, provided for custom csrf_class
+
+        @cached_property
+        def csrf(self):
+            return current_app.config.get("WTF_CSRF_ENABLED", True)
+
+        @cached_property
+        def csrf_secret(self):
+            return current_app.config.get("WTF_CSRF_SECRET_KEY", current_app.secret_key)
+
+        @cached_property
+        def csrf_field_name(self):
+            return current_app.config.get("WTF_CSRF_FIELD_NAME", "csrf_token")
+
+        @cached_property
+        def csrf_time_limit(self):
+            return current_app.config.get("WTF_CSRF_TIME_LIMIT", 3600)
+
+        def wrap_formdata(self, form, formdata):
+            if formdata is _Auto:
+                if _is_submitted():
+                    if request.files:
+                        return CombinedMultiDict((request.files, request.form))
+                    elif request.form:
+                        return request.form
+                    elif request.is_json:
+                        return ImmutableMultiDict(request.get_json())
+
+                return None
+
+            return formdata
+
+        def get_translations(self, form):
+            if not current_app.config.get("WTF_I18N_ENABLED", True):
+                return super().get_translations(form)
+
+            return translations
+
+    def __init__(self, formdata=_Auto, **kwargs):
+        super().__init__(formdata=formdata, **kwargs)
+
+    def is_submitted(self):
+        """Consider the form submitted if there is an active request and
+        the method is ``POST``, ``PUT``, ``PATCH``, or ``DELETE``.
+        """
+
+        return _is_submitted()
+
+    def validate_on_submit(self, extra_validators=None):
+        """Call :meth:`validate` only if the form is submitted.
+        This is a shortcut for ``form.is_submitted() and form.validate()``.
+        """
+        return self.is_submitted() and self.validate(extra_validators=extra_validators)
+
+    def hidden_tag(self, *fields):
+        """Render the form's hidden fields in one call.
+
+        A field is considered hidden if it uses the
+        :class:`~wtforms.widgets.HiddenInput` widget.
+
+        If ``fields`` are given, only render the given fields that
+        are hidden.  If a string is passed, render the field with that
+        name if it exists.
+
+        .. versionchanged:: 0.13
+
+           No longer wraps inputs in hidden div.
+           This is valid HTML 5.
+
+        .. versionchanged:: 0.13
+
+           Skip passed fields that aren't hidden.
+           Skip passed names that don't exist.
+        """
+
+        def hidden_fields(fields):
+            for f in fields:
+                if isinstance(f, str):
+                    f = getattr(self, f, None)
+
+                if f is None or not isinstance(f.widget, HiddenInput):
+                    continue
+
+                yield f
+
+        return Markup("\n".join(str(f) for f in hidden_fields(fields or self)))
+
+
+def _is_submitted():
+    """Consider the form submitted if there is an active request and
+    the method is ``POST``, ``PUT``, ``PATCH``, or ``DELETE``.
+    """
+
+    return bool(request) and request.method in SUBMIT_METHODS
diff --git a/venv/Lib/site-packages/flask_wtf/i18n.py b/venv/Lib/site-packages/flask_wtf/i18n.py
new file mode 100644
index 0000000000000000000000000000000000000000..1cc0e9c5a6d2fee8d18c4f46a79ed82f93d132a7
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/i18n.py
@@ -0,0 +1,47 @@
+from babel import support
+from flask import current_app
+from flask import request
+from flask_babel import get_locale
+from wtforms.i18n import messages_path
+
+__all__ = ("Translations", "translations")
+
+
+def _get_translations():
+    """Returns the correct gettext translations.
+    Copy from flask-babel with some modifications.
+    """
+
+    if not request:
+        return None
+
+    # babel should be in extensions for get_locale
+    if "babel" not in current_app.extensions:
+        return None
+
+    translations = getattr(request, "wtforms_translations", None)
+
+    if translations is None:
+        translations = support.Translations.load(
+            messages_path(), [get_locale()], domain="wtforms"
+        )
+        request.wtforms_translations = translations
+
+    return translations
+
+
+class Translations:
+    def gettext(self, string):
+        t = _get_translations()
+        return string if t is None else t.ugettext(string)
+
+    def ngettext(self, singular, plural, n):
+        t = _get_translations()
+
+        if t is None:
+            return singular if n == 1 else plural
+
+        return t.ungettext(singular, plural, n)
+
+
+translations = Translations()
diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/__init__.py b/venv/Lib/site-packages/flask_wtf/recaptcha/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3100d37e3389219d98787b585357edbe0d9bcc37
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/recaptcha/__init__.py
@@ -0,0 +1,3 @@
+from .fields import RecaptchaField
+from .validators import Recaptcha
+from .widgets import RecaptchaWidget
diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/__init__.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3f546fdcc15a0e8960fc72d690940a98e3e7e797
Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/__init__.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/fields.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/fields.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2448d610b0c6db964cd16355a47e1ca99fa9e123
Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/fields.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/validators.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/validators.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..daaff04283480f89350a9442f17337effb27f9bc
Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/validators.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/widgets.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/widgets.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c2e05915fccf962c1146aeba49c1a004f3638e79
Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/widgets.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/fields.py b/venv/Lib/site-packages/flask_wtf/recaptcha/fields.py
new file mode 100644
index 0000000000000000000000000000000000000000..e91fd092f98c01932a90ffafe68bbf98390ff2ed
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/recaptcha/fields.py
@@ -0,0 +1,17 @@
+from wtforms.fields import Field
+
+from . import widgets
+from .validators import Recaptcha
+
+__all__ = ["RecaptchaField"]
+
+
+class RecaptchaField(Field):
+    widget = widgets.RecaptchaWidget()
+
+    # error message if recaptcha validation fails
+    recaptcha_error = None
+
+    def __init__(self, label="", validators=None, **kwargs):
+        validators = validators or [Recaptcha()]
+        super().__init__(label, validators, **kwargs)
diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/validators.py b/venv/Lib/site-packages/flask_wtf/recaptcha/validators.py
new file mode 100644
index 0000000000000000000000000000000000000000..c5cafb3478cd644a199fb731cf5d2c0c440f986c
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/recaptcha/validators.py
@@ -0,0 +1,75 @@
+import json
+from urllib import request as http
+from urllib.parse import urlencode
+
+from flask import current_app
+from flask import request
+from wtforms import ValidationError
+
+RECAPTCHA_VERIFY_SERVER_DEFAULT = "https://www.google.com/recaptcha/api/siteverify"
+RECAPTCHA_ERROR_CODES = {
+    "missing-input-secret": "The secret parameter is missing.",
+    "invalid-input-secret": "The secret parameter is invalid or malformed.",
+    "missing-input-response": "The response parameter is missing.",
+    "invalid-input-response": "The response parameter is invalid or malformed.",
+}
+
+
+__all__ = ["Recaptcha"]
+
+
+class Recaptcha:
+    """Validates a ReCaptcha."""
+
+    def __init__(self, message=None):
+        if message is None:
+            message = RECAPTCHA_ERROR_CODES["missing-input-response"]
+        self.message = message
+
+    def __call__(self, form, field):
+        if current_app.testing:
+            return True
+
+        if request.is_json:
+            response = request.json.get("g-recaptcha-response", "")
+        else:
+            response = request.form.get("g-recaptcha-response", "")
+        remote_ip = request.remote_addr
+
+        if not response:
+            raise ValidationError(field.gettext(self.message))
+
+        if not self._validate_recaptcha(response, remote_ip):
+            field.recaptcha_error = "incorrect-captcha-sol"
+            raise ValidationError(field.gettext(self.message))
+
+    def _validate_recaptcha(self, response, remote_addr):
+        """Performs the actual validation."""
+        try:
+            private_key = current_app.config["RECAPTCHA_PRIVATE_KEY"]
+        except KeyError:
+            raise RuntimeError("No RECAPTCHA_PRIVATE_KEY config set") from None
+
+        verify_server = current_app.config.get("RECAPTCHA_VERIFY_SERVER")
+        if not verify_server:
+            verify_server = RECAPTCHA_VERIFY_SERVER_DEFAULT
+
+        data = urlencode(
+            {"secret": private_key, "remoteip": remote_addr, "response": response}
+        )
+
+        http_response = http.urlopen(verify_server, data.encode("utf-8"))
+
+        if http_response.code != 200:
+            return False
+
+        json_resp = json.loads(http_response.read())
+
+        if json_resp["success"]:
+            return True
+
+        for error in json_resp.get("error-codes", []):
+            if error in RECAPTCHA_ERROR_CODES:
+                raise ValidationError(RECAPTCHA_ERROR_CODES[error])
+
+        return False
diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/widgets.py b/venv/Lib/site-packages/flask_wtf/recaptcha/widgets.py
new file mode 100644
index 0000000000000000000000000000000000000000..bfae830bb17f188f2123931847f958590c77e690
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/recaptcha/widgets.py
@@ -0,0 +1,43 @@
+from urllib.parse import urlencode
+
+from flask import current_app
+from markupsafe import Markup
+
+RECAPTCHA_SCRIPT_DEFAULT = "https://www.google.com/recaptcha/api.js"
+RECAPTCHA_DIV_CLASS_DEFAULT = "g-recaptcha"
+RECAPTCHA_TEMPLATE = """
+<script src='%s' async defer></script>
+<div class="%s" %s></div>
+"""
+
+__all__ = ["RecaptchaWidget"]
+
+
+class RecaptchaWidget:
+    def recaptcha_html(self, public_key):
+        html = current_app.config.get("RECAPTCHA_HTML")
+        if html:
+            return Markup(html)
+        params = current_app.config.get("RECAPTCHA_PARAMETERS")
+        script = current_app.config.get("RECAPTCHA_SCRIPT")
+        if not script:
+            script = RECAPTCHA_SCRIPT_DEFAULT
+        if params:
+            script += "?" + urlencode(params)
+        attrs = current_app.config.get("RECAPTCHA_DATA_ATTRS", {})
+        attrs["sitekey"] = public_key
+        snippet = " ".join(f'data-{k}="{attrs[k]}"' for k in attrs)  # noqa: B028, B907
+        div_class = current_app.config.get("RECAPTCHA_DIV_CLASS")
+        if not div_class:
+            div_class = RECAPTCHA_DIV_CLASS_DEFAULT
+        return Markup(RECAPTCHA_TEMPLATE % (script, div_class, snippet))
+
+    def __call__(self, field, error=None, **kwargs):
+        """Returns the recaptcha input HTML."""
+
+        try:
+            public_key = current_app.config["RECAPTCHA_PUBLIC_KEY"]
+        except KeyError:
+            raise RuntimeError("RECAPTCHA_PUBLIC_KEY config not set") from None
+
+        return self.recaptcha_html(public_key)