diff --git a/app/app.py b/app/app.py index e169432..4ef6d47 100644 --- a/app/app.py +++ b/app/app.py @@ -34,6 +34,60 @@ def notifications(ws): ws.send(json.dumps({"type": "pong", "payload": data})) +# ================ +# DECORATORS +# ================ +import structlog + +logger = structlog.get_logger() + +@app.before_request +def log_request(): + logger.info( + "request", + path=request.path, + method=request.method, + remote=request.remote_addr, + ) + +@app.after_request +def log_response(response): + logger.info( + "response", + path=request.path, + status=response.status_code, + ) + return response + + +from limits import RateLimitItemPerMinute +from time import time + +rate_store = {} + +def rate_limited(f): + @wraps(f) + def wrapper(*args, **kwargs): + if app.config["AUTH_MODE"] == "oauth": + email = session["user"]["email"] + else: + email = session["db_user"]["email"] + + key = f"{email}:{f.__name__}" + limit = RateLimitItemPerMinute(30) # 30 req/min por endpoint + + now = int(time()) + window = now // 60 + + used = rate_store.get((key, window), 0) + if used >= limit.amount: + return jsonify({"error": "Rate limit exceeded"}), 429 + + rate_store[(key, window)] = used + 1 + return f(*args, **kwargs) + return wrapper + + def admin_required(f): @wraps(f) def wrapper(*args, **kwargs): @@ -290,6 +344,7 @@ def list_conversations(): @app.route("/api/export/md/") @login_required +@rate_limited def export_md(cid): db = SessionLocal() conv = db.query(Conversation).filter_by(id=cid).first() @@ -314,6 +369,7 @@ from docx import Document @app.route("/api/import/", methods=["POST"]) @login_required +@rate_limited def import_file(cid): file = request.files.get("file") if not file: @@ -351,6 +407,7 @@ def import_file(cid): @app.route("/api/export/pdf/") @login_required +@rate_limited def export_pdf(cid): db = SessionLocal() conv = db.query(Conversation).filter_by(id=cid).first() @@ -372,6 +429,7 @@ def export_pdf(cid): @app.route("/api/history/") @login_required +@rate_limited def history(cid): db = SessionLocal() msgs = db.query(Message).filter_by(conversation_id=cid).order_by(Message.created_at).all() @@ -381,6 +439,7 @@ def history(cid): @app.route("/api/attachment/", methods=["POST"]) @login_required +@rate_limited def upload_attachment(cid): db = SessionLocal() file = request.files.get("file") @@ -401,6 +460,7 @@ def upload_attachment(cid): @app.route("/api/attachments/") @login_required +@rate_limited def list_attachments(cid): db = SessionLocal() atts = db.query(Attachment).filter_by(conversation_id=cid).all() @@ -411,6 +471,7 @@ def list_attachments(cid): @app.route("/api/download/") @login_required +@rate_limited def download(att_id): db = SessionLocal() att = db.query(Attachment).filter_by(id=att_id).first() @@ -424,6 +485,7 @@ def download(att_id): @app.route("/api/stream") @login_required +@rate_limited def stream(): cid = request.args.get("conversation_id") model = request.args.get("model", "llama3.2") @@ -485,6 +547,7 @@ def update_tags(cid): @app.route("/api/search") @login_required +@rate_limited def search(): q = request.args.get("q", "").lower() user_email = session["user"]["email"]