Skip to content

Commit

Permalink
Add ratelimiter and fix suggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
cherriae committed Feb 1, 2025
1 parent 3a1bd0b commit 675a9ce
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 47 deletions.
12 changes: 11 additions & 1 deletion app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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")
Expand Down Expand Up @@ -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()
6 changes: 3 additions & 3 deletions app/auth/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'}

Expand Down Expand Up @@ -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():
Expand Down
19 changes: 1 addition & 18 deletions app/static/css/global.css
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down
6 changes: 6 additions & 0 deletions app/templates/auth/register.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ <h2 class="text-center text-4xl font-bold tracking-tight text-gray-900 sm:text-5
<span class="check-icon unmet w-4 h-4 flex items-center justify-center rounded-full border border-gray-300"></span>
At least 8 characters
</div>
<div class="requirement-item flex items-center gap-2" data-requirement="match">
<span class="check-icon unmet w-4 h-4 flex items-center justify-center rounded-full border border-gray-300"></span>
Passwords match
</div>
</div>
</div>

Expand Down Expand Up @@ -107,8 +111,10 @@ <h2 class="text-center text-4xl font-bold tracking-tight text-gray-900 sm:text-5
const strengthBar = document.querySelector('.strength-bar');

function checkPasswordStrength(password) {
const confirmPassword = document.getElementById('confirm_password').value;
const checks = {
length: password.length >= 8,
match: password === confirmPassword && confirmPassword.length > 0
};

Object.entries(checks).forEach(([requirement, isPassing]) => {
Expand Down
24 changes: 22 additions & 2 deletions app/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@
<title>{% block title %}Castle{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/@popperjs/core@2"></script>

<style>
/* 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;
}
</style>

<link rel="preload" href="{{ url_for('static', filename='fonts/Richardson Brand Accelerator.otf') }}" as="font" type="font/otf" crossorigin>
<link rel="preload" href="{{ url_for('static', filename='fonts/oxanium-vrb.ttf') }}" as="font" type="font/ttf" crossorigin>
<link rel="stylesheet" href="{{ url_for('static', filename='css/global.css') }}">
Expand Down Expand Up @@ -156,8 +176,8 @@
</div>
{% block content %}{% endblock %}
<!-- Floating decoration elements -->
<div class="absolute top-20 right-10 w-20 h-20 bg-blue-500 rounded-full opacity-20 animate-float z-0"></div>
<div class="absolute bottom-10 left-10 w-16 h-16 bg-blue-600 rounded-full opacity-25 animate-float delay-200 -z-10"></div>
<div class="absolute top-20 right-10 w-20 h-20 bg-blue-500 rounded-full opacity-20 animate-float z-10"></div>
<div class="absolute bottom-10 left-10 w-16 h-16 bg-blue-600 rounded-full opacity-25 animate-float delay-200 z-10"></div>
</div>
<script>
const menuButton = document.querySelector('.mobile-menu-button');
Expand Down
21 changes: 19 additions & 2 deletions app/templates/team/create.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ <h1 class="text-3xl font-bold text-gray-900">Create Your Team</h1>
{{ form.team_number(
autocomplete="off",
class="w-full px-4 py-3 rounded-lg border " + (" border-red-500 ring-1 ring-red-500" if form.team_number.errors else "border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"),
placeholder="Enter your team number"
placeholder="Enter your team number",
oninput="this.value = this.value.replace(/[^0-9]/g, '')",
pattern="[0-9]*"
) }}
{% if form.team_number.errors %}
{% for error in form.team_number.errors %}
Expand Down Expand Up @@ -50,8 +52,12 @@ <h1 class="text-3xl font-bold text-gray-900">Create Your Team</h1>
{{ form.description(
autocomplete="off",
class="w-full px-4 py-3 rounded-lg border h-32 resize-none " + (" border-red-500 ring-1 ring-red-500" if form.description.errors else "border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"),
placeholder="Tell us about your team..."
placeholder="Tell us about your team...",
maxlength="100"
) }}
<div class="mt-1 text-sm text-gray-500">
<span id="char-count">0</span>/100 characters
</div>
{% if form.description.errors %}
{% for error in form.description.errors %}
<p class="mt-1 text-sm text-red-600">{{ error }}</p>
Expand Down Expand Up @@ -85,4 +91,15 @@ <h1 class="text-3xl font-bold text-gray-900">Create Your Team</h1>
</div>
</div>
</div>

<script>
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);
}
});
</script>
{% endblock %}
86 changes: 69 additions & 17 deletions app/templates/team/manage.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ <h1 class="text-2xl font-bold text-center sm:text-left">Team Management</h1>
<div class="flex flex-col md:flex-row md:justify-between md:items-start">
<h2 class="text-2xl font-semibold text-center md:text-left mb-4 md:mb-0">Team {{ team.team_number }}</h2>


<div class="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-4 items-center md:items-start">
{% if team.is_admin(current_user.id) %}
<a href="{{ url_for('team.team_settings', team_number=team.team_number) }}"
Expand Down Expand Up @@ -70,13 +71,22 @@ <h2 class="text-2xl font-semibold text-center md:text-left mb-4 md:mb-0">Team {{
{% endif %}

<div class="mt-4">
<p class="text-gray-700">
<div class="text-gray-700 break-words overflow-hidden w-full">
{% if team.description %}
{{ team.description }}
<div class="description-preview whitespace-normal break-all">
{{ team.description[:70] }}
{% if team.description|length > 70 %}
<button onclick="toggleDescription(this)" class="text-blue-600 hover:text-blue-800 ml-1 inline-block">See more</button>
{% endif %}
</div>
<div class="description-full hidden whitespace-normal break-all">
{{ team.description }}
<button onclick="toggleDescription(this)" class="text-blue-600 hover:text-blue-800 ml-1 inline-block">See less</button>
</div>
{% else %}
No team description available.
{% endif %}
</p>
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -257,23 +267,55 @@ <h2 class="text-xl font-semibold">Team Assignments</h2>
{% for assignment in assignments %}
<tr class="assignment-row {% if current_user.get_id() in assignment.assigned_to %}bg-blue-50{% endif %} hover:bg-gray-50"
data-assignment-id="{{ assignment.id }}">
<td class="px-6 py-4 whitespace-nowrap">{{ assignment.title }}</td>
<td class="px-6 py-4 whitespace-normal">{{ assignment.description }}</td>
<td class="px-6 py-4 whitespace-nowrap">
{% for user_id in assignment.assigned_to %}
{% set user = user_dict.get(user_id) %}
<span class="{% if user_id == current_user.get_id() %}font-bold text-blue-600{% endif %}">
{{ user.username if user else "Unknown" }}{% if not loop.last %}, {% endif %}
</span>
{% endfor %}
<td class="px-6 py-4">
<div class="flex flex-col">
<div class="flex items-center justify-between mb-2">
<h3 class="font-semibold truncate max-w-xs">{{ assignment.title }}</h3>
<div class="flex items-center space-x-4">
<span class="text-sm text-gray-500">Due: {{ assignment.due_date.strftime('%Y-%m-%d %I:%M %p') if assignment.due_date else 'No due date' }}</span>
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full status-badge
{% if assignment.status == 'completed' %}
bg-green-100 text-green-800
{% elif assignment.status == 'in_progress' %}
bg-yellow-100 text-yellow-800
{% else %}
bg-gray-100 text-gray-800
{% endif %}">
{% if assignment.status == 'in_progress' %}
In Progress
{% else %}
{{ assignment.status|title }}
{% endif %}
</span>
</div>
</div>
<div class="text-sm text-gray-500 mb-2">
<span>Assigned to:
{% for user_id in assignment.assigned_to %}
{% set user = user_dict.get(user_id) %}
<span class="{% if user_id == current_user.get_id() %}font-bold text-blue-600{% endif %}">
{{ user.username if user else "Unknown" }}{% if not loop.last %}, {% endif %}
</span>
{% endfor %}
</span>
</div>
<div class="text-sm text-gray-700 overflow-hidden">
<div class="description-preview">
{{ assignment.description[:100] }}
{% if assignment.description|length > 100 %}
<button onclick="toggleDescription(this)" class="text-blue-600 hover:text-blue-800 ml-1">See more</button>
{% endif %}
</div>
<div class="description-full hidden">
{{ assignment.description }}
<button onclick="toggleDescription(this)" class="text-blue-600 hover:text-blue-800 ml-1">See less</button>
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{% 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 %}
Expand Down Expand Up @@ -441,5 +483,15 @@ <h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">Edit Assignment</h3
</div>
</div>
<script src="{{ url_for('static', filename='js/team.manage.js') }}"></script>
<script>
function toggleDescription(button) {
const container = button.closest('.text-gray-700');
const preview = container.querySelector('.description-preview');
const full = container.querySelector('.description-full');

preview.classList.toggle('hidden');
full.classList.toggle('hidden');
}
</script>

{% endblock %}
20 changes: 16 additions & 4 deletions app/templates/team/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,17 @@ <h3 class="text-lg font-medium text-gray-900 mb-4">Team Logo</h3>
<h3 class="text-lg font-medium text-gray-900 mb-4">Team Description</h3>
<div>
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
<div class="mt-1">
<div class="mt-1 relative">
<textarea id="description"
name="description"
rows="4"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
maxlength="500"
class="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm transition-colors"
placeholder="Describe your team..."
autocomplete="off"
>{{ team.description or '' }}</textarea>
autocomplete="off">{{ team.description or '' }}</textarea>
<div class="mt-1 text-sm text-gray-500">
<span id="char-count">{{ team.description|length }}</span>/100 characters
</div>
</div>
<p class="mt-2 text-sm text-gray-500">
Brief description of your team that will be visible to everyone.
Expand Down Expand Up @@ -113,5 +116,14 @@ <h3 class="text-lg font-medium text-gray-900 mb-4">Team Description</h3>
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);
}
});
</script>
{% endblock %}
10 changes: 10 additions & 0 deletions app/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 675a9ce

Please sign in to comment.