Security, code quality and frontend improvements
- Move SECRET_KEY out of docker-compose into .env (env_file), add .env.example - Add flask-limiter with 10 req/min on login route; introduce util/limiter.py - Replace direct mysql.connector.connect() calls with MySQLConnectionPool via util/db_pool.py - Fix deprecated datetime.utcnow() -> datetime.now(timezone.utc) in auth/login.py - Remove dead /api/scripts 410 route from admin.py - Add MD5 security warning in Md5Tool.jsx - Add ErrorBoundary component and wrap App.jsx - Expand README with setup guide, screenshot and project structure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+10
-25
@@ -1,8 +1,7 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from mysql.connector import connect
|
||||
from werkzeug.security import generate_password_hash
|
||||
from auth.token import verify_token
|
||||
from util.db_config import load_config
|
||||
from util.db_pool import get_connection
|
||||
from util.logger import logger
|
||||
|
||||
admin_bp = Blueprint("admin", __name__)
|
||||
@@ -47,8 +46,7 @@ def list_users():
|
||||
if err:
|
||||
return err
|
||||
try:
|
||||
cfg = load_config()
|
||||
conn = connect(**cfg)
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
_ensure_tables(cur)
|
||||
cur.execute("SELECT id, username, role FROM users ORDER BY username ASC")
|
||||
@@ -73,8 +71,7 @@ def create_user():
|
||||
if not username or not password:
|
||||
return jsonify({"message": "Username und Passwort erforderlich"}), 400
|
||||
try:
|
||||
cfg = load_config()
|
||||
conn = connect(**cfg)
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
_ensure_tables(cur)
|
||||
cur.execute("SELECT id FROM users WHERE username=%s", (username,))
|
||||
@@ -108,8 +105,7 @@ def update_user(user_id):
|
||||
if role is None and password is None:
|
||||
return jsonify({"message": "Nichts zu aktualisieren"}), 400
|
||||
try:
|
||||
cfg = load_config()
|
||||
conn = connect(**cfg)
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
_ensure_tables(cur)
|
||||
if role:
|
||||
@@ -135,8 +131,7 @@ def delete_user(user_id):
|
||||
if err:
|
||||
return err
|
||||
try:
|
||||
cfg = load_config()
|
||||
conn = connect(**cfg)
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
_ensure_tables(cur)
|
||||
# Schutz: Admin darf sich nicht selbst löschen
|
||||
@@ -170,8 +165,7 @@ def list_websites_admin():
|
||||
if err:
|
||||
return err
|
||||
try:
|
||||
cfg = load_config()
|
||||
conn = connect(**cfg)
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
_ensure_tables(cur)
|
||||
cur.execute("SELECT id, name, url, description FROM websites ORDER BY name ASC")
|
||||
@@ -196,8 +190,7 @@ def create_website():
|
||||
if not name or not url:
|
||||
return jsonify({"message": "Name und URL erforderlich"}), 400
|
||||
try:
|
||||
cfg = load_config()
|
||||
conn = connect(**cfg)
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
_ensure_tables(cur)
|
||||
cur.execute(
|
||||
@@ -221,8 +214,7 @@ def update_website(item_id):
|
||||
return err
|
||||
data = request.get_json() or {}
|
||||
try:
|
||||
cfg = load_config()
|
||||
conn = connect(**cfg)
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
_ensure_tables(cur)
|
||||
cur.execute(
|
||||
@@ -244,8 +236,7 @@ def delete_website(item_id):
|
||||
if err:
|
||||
return err
|
||||
try:
|
||||
cfg = load_config()
|
||||
conn = connect(**cfg)
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
_ensure_tables(cur)
|
||||
cur.execute("DELETE FROM websites WHERE id=%s", (item_id,))
|
||||
@@ -266,8 +257,7 @@ def list_websites_public():
|
||||
if not user:
|
||||
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||
try:
|
||||
cfg = load_config()
|
||||
conn = connect(**cfg)
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
_ensure_tables(cur)
|
||||
cur.execute("SELECT id, name, url, description FROM websites ORDER BY name ASC")
|
||||
@@ -278,8 +268,3 @@ def list_websites_public():
|
||||
except Exception as e:
|
||||
logger.error(f"[Public list_websites] {e}")
|
||||
return jsonify({"message": "Serverfehler"}), 500
|
||||
|
||||
|
||||
@admin_bp.route("/api/scripts", methods=["GET"])
|
||||
def list_scripts_public():
|
||||
return jsonify({"message": "Scripts wurden entfernt"}), 410
|
||||
|
||||
+3
-2
@@ -8,14 +8,15 @@ from flask import Flask, send_from_directory, redirect
|
||||
from util.logger import logger
|
||||
from util.db_config import is_configured, load_config, test_connection
|
||||
from util.setup_routes import setup_blueprint
|
||||
from util.limiter import limiter
|
||||
from auth import auth_bp
|
||||
from tools import md5_blueprint
|
||||
from admin import admin_bp
|
||||
|
||||
app = Flask(__name__, template_folder="templates")
|
||||
limiter.init_app(app)
|
||||
|
||||
|
||||
# 📦 Blueprints registrieren
|
||||
# Blueprints registrieren
|
||||
app.register_blueprint(setup_blueprint)
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(md5_blueprint)
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
from flask import request, jsonify
|
||||
from mysql.connector import connect
|
||||
from werkzeug.security import check_password_hash
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import jwt
|
||||
|
||||
from util.logger import logger
|
||||
from util.db_config import load_config
|
||||
from util.db_pool import get_connection
|
||||
from util.limiter import limiter
|
||||
from auth.token import SECRET_KEY
|
||||
|
||||
|
||||
@limiter.limit("10 per minute")
|
||||
def login_route():
|
||||
data = request.get_json()
|
||||
username = data.get('username')
|
||||
@@ -18,8 +20,7 @@ def login_route():
|
||||
return jsonify({"message": "Server misconfigured"}), 500
|
||||
|
||||
try:
|
||||
config = load_config()
|
||||
conn = connect(**config)
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
|
||||
user = cursor.fetchone()
|
||||
@@ -27,12 +28,12 @@ def login_route():
|
||||
conn.close()
|
||||
|
||||
if user and check_password_hash(user['password'], password):
|
||||
logger.info(f"✅ Login successful: {username}")
|
||||
logger.info(f"Login successful: {username}")
|
||||
|
||||
payload = {
|
||||
"username": user['username'],
|
||||
"role": user['role'],
|
||||
"exp": datetime.utcnow() + timedelta(minutes=60)
|
||||
"exp": datetime.now(timezone.utc) + timedelta(minutes=60)
|
||||
}
|
||||
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
flask
|
||||
flask-cors
|
||||
flask-limiter
|
||||
mysql-connector-python
|
||||
werkzeug>=2.3
|
||||
PyJWT
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import mysql.connector.pooling
|
||||
from util.logger import logger
|
||||
|
||||
_pool = None
|
||||
|
||||
|
||||
def get_connection():
|
||||
global _pool
|
||||
if _pool is None:
|
||||
from util.db_config import load_config
|
||||
config = load_config()
|
||||
if not config:
|
||||
raise RuntimeError("DB-Konfiguration nicht verfügbar")
|
||||
_pool = mysql.connector.pooling.MySQLConnectionPool(
|
||||
pool_name="tools_pool",
|
||||
pool_size=5,
|
||||
**config
|
||||
)
|
||||
logger.info("DB-Verbindungspool erstellt (pool_size=5)")
|
||||
return _pool.get_connection()
|
||||
|
||||
|
||||
def reset_pool():
|
||||
"""Pool zurücksetzen – nach Konfigurationsänderung aufrufen."""
|
||||
global _pool
|
||||
_pool = None
|
||||
@@ -0,0 +1,4 @@
|
||||
from flask_limiter import Limiter
|
||||
from flask_limiter.util import get_remote_address
|
||||
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
Reference in New Issue
Block a user