Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ services:
- USERS_CONFIG_PATH=/app/configs/users.json
- STORAGE_BACKEND=local # Options: local, s3, minio
- STORAGE_BASE_PATH=/app/data
- RATE_LIMIT_STORAGE_URI=memory://
volumes:
- webserver_data_staged:/app/data/staged
- webserver_data_archived:/app/data/archived
Expand Down
39 changes: 39 additions & 0 deletions webserver/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
login_required,
current_user,
)
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from werkzeug.security import check_password_hash
from werkzeug.utils import secure_filename
from datetime import datetime, timedelta, timezone
Expand All @@ -50,6 +52,17 @@
login_manager.login_view = "login"
login_manager.login_message = "Please log in to access this page."

# ── Rate Limiting ───────────────────────────────────────────────────────────
limiter_storage_uri = os.getenv("RATE_LIMIT_STORAGE_URI", "memory://")
limiter = Limiter(
key_func=get_remote_address,
app=app,
default_limits=[],
storage_uri=limiter_storage_uri,
strategy="fixed-window",
)



class User(UserMixin):
def __init__(self, user_id: str):
Expand Down Expand Up @@ -142,6 +155,10 @@ def user_db_scope(user_id: str):


@app.route("/login", methods=["GET", "POST"])
@limiter.limit(
"5 per minute",
error_message="Too many login attempts. Please try again later.",
)
def login():
if current_user.is_authenticated:
return redirect(url_for("overview"))
Expand Down Expand Up @@ -882,6 +899,10 @@ def get_file_size_mb(filepath):


@app.route("/api/upload", methods=["POST"])
@limiter.limit(
"10 per minute",
error_message="Too many upload attempts. Please slow down.",
)
@login_required
def upload_file():
"""Handle file upload via drag and drop"""
Expand Down Expand Up @@ -946,6 +967,10 @@ def upload_file():


@app.route("/api/files", methods=["GET"])
@limiter.limit(
"30 per minute",
error_message="Too many requests. Please slow down.",
)
@login_required
def get_files():
"""Get list of files in data_incoming and data_staged_for_parsing directories"""
Expand Down Expand Up @@ -1004,6 +1029,20 @@ def get_files():
# ==================== ERROR HANDLERS ====================


@app.errorhandler(429)
def ratelimit_handler(e):
"""Handle rate limit exceeded errors."""
return (
jsonify(
{
"error": "Rate limit exceeded",
"message": str(e.description),
}
),
429,
)


@app.errorhandler(404)
def not_found(error):
return render_template("404.html"), 404
Expand Down
1 change: 1 addition & 0 deletions webserver/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Flask==2.3.3
Flask-Login==0.6.3
Flask-Limiter==3.5.0
psycopg2-binary==2.9.7
folium==0.14.0
gunicorn==22.0.0
Expand Down
Loading