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:
Nirodan
2026-04-24 13:52:53 +02:00
parent 8e2c2d740e
commit 80ec5eca7b
12 changed files with 232 additions and 75 deletions
+10 -25
View File
@@ -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
View File
@@ -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)
+8 -7
View File
@@ -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
View File
@@ -1,5 +1,6 @@
flask
flask-cors
flask-limiter
mysql-connector-python
werkzeug>=2.3
PyJWT
+26
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)