From 675a9ce7e68bcc380befc7f37a88dcc2b6911745 Mon Sep 17 00:00:00 2001 From: Jerry Date: Fri, 31 Jan 2025 21:43:56 -0500 Subject: [PATCH] Add ratelimiter and fix suggestions --- app/app.py | 12 ++++- app/auth/routes.py | 6 +-- app/static/css/global.css | 19 +------ app/templates/auth/register.html | 6 +++ app/templates/base.html | 24 ++++++++- app/templates/team/create.html | 21 +++++++- app/templates/team/manage.html | 86 +++++++++++++++++++++++++------- app/templates/team/settings.html | 20 ++++++-- app/utils.py | 10 ++++ 9 files changed, 157 insertions(+), 47 deletions(-) diff --git a/app/app.py b/app/app.py index 3244f61..959e2c1 100644 --- a/app/app.py +++ b/app/app.py @@ -8,12 +8,12 @@ from flask_wtf.csrf import CSRFProtect from app.auth.auth_utils import UserManager +from app.utils import limiter csrf = CSRFProtect() mongo = PyMongo() login_manager = LoginManager() - def create_app(): app = Flask(__name__, static_folder="static", template_folder="templates") @@ -30,6 +30,9 @@ def create_app(): mongo.init_app(app) # csrf.init_app(app) + limiter.init_app(app) + limiter.storage_uri = app.config.get('MONGO_URI') + with app.app_context(): if "team_data" not in mongo.db.list_collection_names(): mongo.db.create_collection("team_data") @@ -86,10 +89,17 @@ def handle_exception(e): return jsonify({ "error": "An unexpected error occurred" }), 500 + + @app.errorhandler(429) + def rate_limit_error(e): + return jsonify({ + "error": "Rate limit exceeded" + }), 429 return app # if __name__ == "__main__": # app = create_app() + # app.run() diff --git a/app/auth/routes.py b/app/auth/routes.py index 6cb2614..77ee4e9 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -12,10 +12,9 @@ from flask_pymongo import PyMongo from gridfs import GridFS from werkzeug.utils import secure_filename - from app.auth.auth_utils import UserManager -from app.utils import (async_route, handle_route_errors, is_safe_url, - send_gridfs_file) +from app.utils import (async_route, handle_route_errors, is_safe_url, + send_gridfs_file, limiter) ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} @@ -105,6 +104,7 @@ def is_safe_url(target): @auth_bp.route("/login", methods=["GET", "POST"]) +@limiter.limit("8 per minute") @async_route @handle_route_errors async def login(): diff --git a/app/static/css/global.css b/app/static/css/global.css index 5308d39..24c5cde 100644 --- a/app/static/css/global.css +++ b/app/static/css/global.css @@ -1,22 +1,5 @@ -/* Font Face Declarations */ -@font-face { - font-family: 'MainFont'; - src: url('/static/fonts/Richardson Brand Accelerator.otf') format('opentype'); - font-display: swap; - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'SecondaryFont'; - src: url('/static/fonts/oxanium-vrb.ttf') format('truetype'); - font-display: swap; - font-weight: normal; - font-style: normal; -} - /* Global Font Settings */ -body { +*, body { font-family: 'SecondaryFont', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } diff --git a/app/templates/auth/register.html b/app/templates/auth/register.html index 5180cd8..dda3109 100644 --- a/app/templates/auth/register.html +++ b/app/templates/auth/register.html @@ -62,6 +62,10 @@

• At least 8 characters +
+ + Passwords match +
@@ -107,8 +111,10 @@

+ + + @@ -156,8 +176,8 @@ {% block content %}{% endblock %} -
-
+
+
{% endblock %} \ No newline at end of file diff --git a/app/templates/team/manage.html b/app/templates/team/manage.html index 88fb03b..1db8aa6 100644 --- a/app/templates/team/manage.html +++ b/app/templates/team/manage.html @@ -31,6 +31,7 @@

Team Management

Team {{ team.team_number }}

+
@@ -257,23 +267,55 @@

Team Assignments

{% for assignment in assignments %} - {{ assignment.title }} - {{ assignment.description }} - - {% for user_id in assignment.assigned_to %} - {% set user = user_dict.get(user_id) %} - - {{ user.username if user else "Unknown" }}{% if not loop.last %}, {% endif %} - - {% endfor %} + +
+
+

{{ assignment.title }}

+
+ Due: {{ assignment.due_date.strftime('%Y-%m-%d %I:%M %p') if assignment.due_date else 'No due date' }} + + {% if assignment.status == 'in_progress' %} + In Progress + {% else %} + {{ assignment.status|title }} + {% endif %} + +
+
+
+ Assigned to: + {% for user_id in assignment.assigned_to %} + {% set user = user_dict.get(user_id) %} + + {{ user.username if user else "Unknown" }}{% if not loop.last %}, {% endif %} + + {% endfor %} + +
+
+
+ {{ assignment.description[:100] }} + {% if assignment.description|length > 100 %} + + {% endif %} +
+ +
+
{% if assignment.due_date %} - {% if assignment.due_date is string %} - {{ assignment.due_date }} - {% else %} - {{ assignment.due_date.strftime('%Y-%m-%d %I:%M %p') }} - {% endif %} + {{ assignment.due_date.strftime('%Y-%m-%d %I:%M %p') }} {% else %} No due date {% endif %} @@ -441,5 +483,15 @@

Edit Assignment

+ {% endblock %} \ No newline at end of file diff --git a/app/templates/team/settings.html b/app/templates/team/settings.html index f1ec53d..bd74d6a 100644 --- a/app/templates/team/settings.html +++ b/app/templates/team/settings.html @@ -61,14 +61,17 @@

Team Logo

Team Description

-
+
+ autocomplete="off">{{ team.description or '' }} +
+ {{ team.description|length }}/100 characters +

Brief description of your team that will be visible to everyone. @@ -113,5 +116,14 @@

Team Description

reader.readAsDataURL(file); } } + +document.querySelector('textarea[name="description"]').addEventListener('input', function() { + const charCount = this.value.length; + document.getElementById('char-count').textContent = charCount; + + if (charCount > 100) { + this.value = this.value.substring(0, 100); + } +}); {% endblock %} \ No newline at end of file diff --git a/app/utils.py b/app/utils.py index 74c075d..db3075b 100644 --- a/app/utils.py +++ b/app/utils.py @@ -7,10 +7,14 @@ from bson import ObjectId from flask import flash, jsonify, request, send_file +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address from gridfs import GridFS from pymongo import MongoClient from pymongo.errors import ConnectionFailure, ServerSelectionTimeoutError from werkzeug.utils import secure_filename +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address # Configure logging logging.basicConfig(level=logging.INFO) @@ -98,6 +102,12 @@ def wrapper(*args, **kwargs): return error_response("An internal error has occurred.", 500) return wrapper +limiter = Limiter( + key_func=get_remote_address, + default_limits=["80 per minute"] +) + + # ============ File Handling Utilities ============ def allowed_file(filename: str) -> bool: