diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..3c69f39
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,13 @@
+# ============================================================
+# Umgebungsvariablen für docker-compose.dev.yml
+# Kopiere diese Datei zu ".env" und passe die Werte an.
+# ============================================================
+
+# JWT Secret Key
+# Ersetze diesen Wert in Produktion durch einen langen, zufälligen String,
+# z.B. generiert mit: python -c "import secrets; print(secrets.token_hex(32))"
+SECRET_KEY=dev-change-me
+
+# Pfad zur Datenbankkonfigurationsdatei im Container
+# Standardwert für Docker: /config/db_config.json
+DB_CONFIG_PATH=/config/db_config.json
diff --git a/AGENT.md b/AGENT.md
deleted file mode 100644
index e69de29..0000000
diff --git a/README.md b/README.md
index 6332b1e..10120ed 100644
--- a/README.md
+++ b/README.md
@@ -1,34 +1,120 @@
-# 🐳 Docker-Webplattform: React + Flask + externe MariaDB
+# Docker-Webplattform: React + Flask + externe MariaDB
-Dieses Projekt ist eine vollständig dockerisierte Webanwendung, die ein React-Frontend und ein Flask-Backend **in einem einzigen Container** vereint. Sie kommuniziert mit einer **externen MariaDB-Datenbank** (z. B. auf einem Unraid-Server) und bietet ein Setup-System, Login, Rollenverwaltung und modulare Tools.
+Vollständig dockerisierte Webanwendung mit React-Frontend und Flask-Backend in einem einzigen Container. Verbindet sich mit einer externen MariaDB-Datenbank und bietet Setup-System, Login, Rollenverwaltung und modulare Tools.
---
-## ⚙️ Was macht der Docker-Container?
+## Screenshot
-- 🚀 Startet automatisch **Flask + React** in einer Umgebung
-- 🛠 Bei Erststart zeigt er eine **Setup-Webseite** zum Eintragen der DB-Verbindung
-- 💾 Speichert die Konfiguration in `config/db_config.json`
-- 🌐 Verbindet sich mit der **externen MariaDB-Datenbank**
-- 👤 Erstellt automatisch ein `admin`-Benutzerkonto (`admin / admin`)
-- 🔐 Ermöglicht Login und Rollenzuordnung
-- 🧰 Liefert das React-Frontend direkt über Flask aus (keine extra Node-Instanz)
-- 🧾 Verwaltet Tools wie z. B. ein MD5-Hash-Modul
-- ✅ Docker-fähig, kompatibel mit Docker Compose oder Portainer
+
---
-## 📦 Projektstruktur
+## Setup-Anleitung
-## 📄 Lizenz
+### Voraussetzungen
+- Docker & Docker Compose installiert
+- Externe MariaDB/MySQL-Datenbank erreichbar (z.B. auf Unraid, NAS oder anderem Server)
-Außerdem ist es notwendig den Ursprüglichen Entwickler anzugeben
+### 1. Repository klonen
-Dieses Projekt steht unter der **Creative Commons BY-NC 4.0 Lizenz**.
-➡️ Du darfst ihn verwenden, verändern und teilen – **aber nicht kommerziell nutzen.**
-Volltext: [https://creativecommons.org/licenses/by-nc/4.0] (https://creativecommons.org/licenses/by-nc/4.0)
+```bash
+git clone https://github.com/Nirodan/Tools.git
+cd Tools
+```
-💡 Hinweis: Die Urheberschaft erfolgt unter Pseudonym.
+### 2. Umgebungsvariablen konfigurieren
-Author: Source page: Nirodan/Github:Nirodan Production
\ No newline at end of file
+```bash
+cp .env.example .env
+```
+
+Dann `.env` öffnen und `SECRET_KEY` durch einen sicheren zufälligen Wert ersetzen:
+
+```bash
+# Sicheren Key generieren:
+python -c "import secrets; print(secrets.token_hex(32))"
+```
+
+### 3. Container starten
+
+```bash
+docker compose -f docker-compose.dev.yml up -d
+```
+
+### 4. Datenbank einrichten (Erststart)
+
+Beim ersten Start öffnet sich automatisch die Setup-Seite unter `http://localhost:5050/setup`.
+
+Dort die MariaDB-Verbindungsdaten eintragen:
+
+| Feld | Beispiel |
+|------------|-------------------|
+| Host | `192.168.1.100` |
+| Port | `3306` |
+| User | `tools_user` |
+| Password | `geheimes_pw` |
+| Database | `tools_db` |
+
+Nach dem Speichern wird automatisch ein Admin-Account angelegt:
+- **Benutzername:** `admin`
+- **Passwort:** `admin` *(bitte sofort im Admin-Dashboard ändern)*
+
+### 5. Anwendung aufrufen
+
+```
+http://localhost:5050
+```
+
+---
+
+## Was der Container macht
+
+- Startet automatisch Flask + React in einer Umgebung
+- Bei Erststart: Setup-Webseite zum Eintragen der DB-Verbindung
+- Speichert die Konfiguration in einem Docker-Volume (`config/db_config.json`)
+- Verbindet sich mit der externen MariaDB-Datenbank
+- Erstellt automatisch ein `admin`-Benutzerkonto
+- Liefert das React-Frontend direkt über Flask aus (keine extra Node-Instanz)
+
+---
+
+## Projektstruktur
+
+```
+Tools/
+├── backend/
+│ ├── app.py # Flask-Einstiegspunkt
+│ ├── admin.py # Admin-API (Nutzer- und Websitenverwaltung)
+│ ├── auth/ # Login, Logout, Token-Verifikation
+│ ├── tools/ # Modulare Tools (MD5 etc.)
+│ └── util/ # DB-Pool, Konfiguration, Logger, Rate-Limiter
+├── frontend/
+│ └── src/
+│ ├── App.jsx
+│ ├── components/ # React-Komponenten
+│ └── services/api.js # Axios-Instanz mit Auth-Interceptor
+├── docker-compose.dev.yml
+├── Dockerfile
+├── .env.example # Vorlage für Umgebungsvariablen
+└── README.md
+```
+
+---
+
+## Sicherheitshinweise
+
+- `SECRET_KEY` niemals im Klartext in der Versionskontrolle speichern – immer `.env` verwenden
+- Standard-Admin-Passwort (`admin`) nach dem Erststart sofort ändern
+- MD5 ist kryptografisch unsicher – nicht für Passwort-Hashing verwenden
+
+---
+
+## Lizenz
+
+Dieses Projekt steht unter der **Creative Commons BY-NC 4.0 Lizenz**.
+Du darfst es verwenden, verändern und teilen – aber nicht kommerziell nutzen.
+Volltext: https://creativecommons.org/licenses/by-nc/4.0
+
+Author: Nirodan – https://github.com/Nirodan
diff --git a/backend/admin.py b/backend/admin.py
index ca298dc..9eb905b 100644
--- a/backend/admin.py
+++ b/backend/admin.py
@@ -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
diff --git a/backend/app.py b/backend/app.py
index b20a579..401dc3e 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -8,17 +8,32 @@ 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 tools import (
+ md5_blueprint,
+ hasher_blueprint,
+ base64_blueprint,
+ jwt_decoder_blueprint,
+ passwordgen_blueprint,
+ timestamp_blueprint,
+ textdiff_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)
+app.register_blueprint(hasher_blueprint)
+app.register_blueprint(base64_blueprint)
+app.register_blueprint(jwt_decoder_blueprint)
+app.register_blueprint(passwordgen_blueprint)
+app.register_blueprint(timestamp_blueprint)
+app.register_blueprint(textdiff_blueprint)
app.register_blueprint(admin_bp)
# 🌐 React-Frontend ausliefern
@@ -29,7 +44,8 @@ def serve_frontend(path):
return redirect('/setup')
if path.startswith('setup') or path.startswith('api'):
- return redirect(f'/{path}')
+ from flask import abort
+ abort(404)
dist_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'frontend', 'dist'))
file_path = os.path.join(dist_dir, path)
diff --git a/backend/auth/login.py b/backend/auth/login.py
index 08b937c..cec8474 100644
--- a/backend/auth/login.py
+++ b/backend/auth/login.py
@@ -1,25 +1,29 @@
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')
- password = data.get('password')
+ data = request.get_json(silent=True) or {}
+ username = data.get('username', '').strip()
+ password = data.get('password', '')
+
+ if not username or not password:
+ return jsonify({"message": "Username und Passwort erforderlich"}), 400
if not SECRET_KEY:
logger.error("Login blocked: SECRET_KEY is not configured.")
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 +31,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")
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 9ebfc05..c47fdf7 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -1,6 +1,8 @@
flask
flask-cors
+flask-limiter
mysql-connector-python
werkzeug>=2.3
PyJWT
+bcrypt
python-dotenv
\ No newline at end of file
diff --git a/backend/tools/__init__.py b/backend/tools/__init__.py
index 45f169f..c53c248 100644
--- a/backend/tools/__init__.py
+++ b/backend/tools/__init__.py
@@ -1 +1,7 @@
from .md5 import md5_blueprint
+from .hasher import hasher_blueprint
+from .base64tool import base64_blueprint
+from .jwtdecoder import jwt_decoder_blueprint
+from .passwordgen import passwordgen_blueprint
+from .timestamp import timestamp_blueprint
+from .textdiff import textdiff_blueprint
diff --git a/backend/tools/base64tool.py b/backend/tools/base64tool.py
new file mode 100644
index 0000000..d9109d9
--- /dev/null
+++ b/backend/tools/base64tool.py
@@ -0,0 +1,38 @@
+from flask import Blueprint, request, jsonify
+import base64
+from util.logger import logger
+from auth.token import verify_token
+
+base64_blueprint = Blueprint('base64_tool', __name__)
+
+
+@base64_blueprint.route('/api/base64/encode', methods=['POST'])
+def encode():
+ user = verify_token()
+ if not user:
+ return jsonify({"message": "Nicht autorisiert"}), 401
+ try:
+ data = request.get_json()
+ text = data.get("text", "")
+ result = base64.b64encode(text.encode()).decode()
+ logger.info(f"Base64 kodiert von {user['username']}")
+ return jsonify({"result": result})
+ except Exception as e:
+ logger.error(f"Fehler Base64 encode: {e}")
+ return jsonify({"message": "Fehler beim Kodieren"}), 500
+
+
+@base64_blueprint.route('/api/base64/decode', methods=['POST'])
+def decode():
+ user = verify_token()
+ if not user:
+ return jsonify({"message": "Nicht autorisiert"}), 401
+ try:
+ data = request.get_json()
+ text = data.get("text", "").strip()
+ result = base64.b64decode(text.encode()).decode('utf-8')
+ logger.info(f"Base64 dekodiert von {user['username']}")
+ return jsonify({"result": result})
+ except Exception as e:
+ logger.error(f"Fehler Base64 decode: {e}")
+ return jsonify({"message": "Ungültiger Base64-Input"}), 400
diff --git a/backend/tools/hasher.py b/backend/tools/hasher.py
new file mode 100644
index 0000000..e2ae61f
--- /dev/null
+++ b/backend/tools/hasher.py
@@ -0,0 +1,39 @@
+from flask import Blueprint, request, jsonify
+import hashlib
+import bcrypt
+from util.logger import logger
+from auth.token import verify_token
+
+hasher_blueprint = Blueprint('hasher_tool', __name__)
+
+
+@hasher_blueprint.route('/api/hash/sha256', methods=['POST'])
+def hash_sha256():
+ user = verify_token()
+ if not user:
+ return jsonify({"message": "Nicht autorisiert"}), 401
+ try:
+ data = request.get_json()
+ text = data.get("text", "")
+ result = hashlib.sha256(text.encode()).hexdigest()
+ logger.info(f"SHA256 erstellt von {user['username']}")
+ return jsonify({"sha256": result, "by": user['username']})
+ except Exception as e:
+ logger.error(f"Fehler SHA256: {e}")
+ return jsonify({"message": "Fehler beim Hashen"}), 500
+
+
+@hasher_blueprint.route('/api/hash/bcrypt', methods=['POST'])
+def hash_bcrypt():
+ user = verify_token()
+ if not user:
+ return jsonify({"message": "Nicht autorisiert"}), 401
+ try:
+ data = request.get_json()
+ text = data.get("text", "")
+ hashed = bcrypt.hashpw(text.encode(), bcrypt.gensalt()).decode()
+ logger.info(f"bcrypt erstellt von {user['username']}")
+ return jsonify({"bcrypt": hashed, "by": user['username']})
+ except Exception as e:
+ logger.error(f"Fehler bcrypt: {e}")
+ return jsonify({"message": "Fehler beim Hashen"}), 500
diff --git a/backend/tools/jwtdecoder.py b/backend/tools/jwtdecoder.py
new file mode 100644
index 0000000..12f7efe
--- /dev/null
+++ b/backend/tools/jwtdecoder.py
@@ -0,0 +1,29 @@
+from flask import Blueprint, request, jsonify
+import jwt
+from datetime import datetime, timezone
+from util.logger import logger
+from auth.token import verify_token
+
+jwt_decoder_blueprint = Blueprint('jwt_decoder_tool', __name__)
+
+
+@jwt_decoder_blueprint.route('/api/jwt/decode', methods=['POST'])
+def decode_jwt():
+ user = verify_token()
+ if not user:
+ return jsonify({"message": "Nicht autorisiert"}), 401
+ try:
+ data = request.get_json()
+ token = data.get("token", "").strip()
+ header = jwt.get_unverified_header(token)
+ payload = jwt.decode(token, options={"verify_signature": False}, algorithms=["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512"])
+
+ expired = False
+ if "exp" in payload:
+ expired = payload["exp"] < datetime.now(timezone.utc).timestamp()
+
+ logger.info(f"JWT dekodiert von {user['username']}")
+ return jsonify({"header": header, "payload": payload, "expired": expired})
+ except Exception as e:
+ logger.error(f"Fehler JWT decode: {e}")
+ return jsonify({"message": "Ungültiger JWT Token"}), 400
diff --git a/backend/tools/passwordgen.py b/backend/tools/passwordgen.py
new file mode 100644
index 0000000..33b537d
--- /dev/null
+++ b/backend/tools/passwordgen.py
@@ -0,0 +1,41 @@
+from flask import Blueprint, request, jsonify
+import secrets
+import string
+from util.logger import logger
+from auth.token import verify_token
+
+passwordgen_blueprint = Blueprint('passwordgen_tool', __name__)
+
+
+@passwordgen_blueprint.route('/api/password/generate', methods=['POST'])
+def generate_password():
+ user = verify_token()
+ if not user:
+ return jsonify({"message": "Nicht autorisiert"}), 401
+ try:
+ data = request.get_json()
+ length = min(max(int(data.get("length", 16)), 8), 64)
+ use_uppercase = data.get("uppercase", True)
+ use_lowercase = data.get("lowercase", True)
+ use_numbers = data.get("numbers", True)
+ use_symbols = data.get("symbols", False)
+
+ charset = ""
+ if use_uppercase:
+ charset += string.ascii_uppercase
+ if use_lowercase:
+ charset += string.ascii_lowercase
+ if use_numbers:
+ charset += string.digits
+ if use_symbols:
+ charset += string.punctuation
+
+ if not charset:
+ return jsonify({"message": "Mindestens ein Zeichensatz muss ausgewählt sein"}), 400
+
+ password = ''.join(secrets.choice(charset) for _ in range(length))
+ logger.info(f"Passwort generiert von {user['username']}")
+ return jsonify({"password": password})
+ except Exception as e:
+ logger.error(f"Fehler Passwortgenerator: {e}")
+ return jsonify({"message": "Fehler beim Generieren"}), 500
diff --git a/backend/tools/textdiff.py b/backend/tools/textdiff.py
new file mode 100644
index 0000000..6948cab
--- /dev/null
+++ b/backend/tools/textdiff.py
@@ -0,0 +1,37 @@
+from flask import Blueprint, request, jsonify
+import difflib
+from util.logger import logger
+from auth.token import verify_token
+
+textdiff_blueprint = Blueprint('textdiff_tool', __name__)
+
+
+@textdiff_blueprint.route('/api/text/diff', methods=['POST'])
+def text_diff():
+ user = verify_token()
+ if not user:
+ return jsonify({"message": "Nicht autorisiert"}), 401
+ try:
+ data = request.get_json()
+ text1 = data.get("text1", "")
+ text2 = data.get("text2", "")
+
+ lines1 = text1.splitlines(keepends=True)
+ lines2 = text2.splitlines(keepends=True)
+
+ result = []
+ for line in difflib.unified_diff(lines1, lines2, lineterm=''):
+ if line.startswith('+++') or line.startswith('---') or line.startswith('@@'):
+ continue
+ if line.startswith('+'):
+ result.append({"type": "added", "text": line[1:]})
+ elif line.startswith('-'):
+ result.append({"type": "removed", "text": line[1:]})
+ else:
+ result.append({"type": "unchanged", "text": line[1:] if line.startswith(' ') else line})
+
+ logger.info(f"Text-Diff erstellt von {user['username']}")
+ return jsonify({"diff": result})
+ except Exception as e:
+ logger.error(f"Fehler Text-Diff: {e}")
+ return jsonify({"message": "Fehler beim Vergleich"}), 500
diff --git a/backend/tools/timestamp.py b/backend/tools/timestamp.py
new file mode 100644
index 0000000..1e47f46
--- /dev/null
+++ b/backend/tools/timestamp.py
@@ -0,0 +1,46 @@
+from flask import Blueprint, request, jsonify
+from datetime import datetime, timezone
+from util.logger import logger
+from auth.token import verify_token
+
+timestamp_blueprint = Blueprint('timestamp_tool', __name__)
+
+_DATE_FORMATS = [
+ "%Y-%m-%dT%H:%M:%S",
+ "%Y-%m-%d %H:%M:%S",
+ "%Y-%m-%d",
+ "%d.%m.%Y %H:%M:%S",
+ "%d.%m.%Y",
+]
+
+
+@timestamp_blueprint.route('/api/timestamp/convert', methods=['POST'])
+def convert_timestamp():
+ user = verify_token()
+ if not user:
+ return jsonify({"message": "Nicht autorisiert"}), 401
+ try:
+ data = request.get_json()
+ value = data.get("value", "").strip()
+ direction = data.get("direction", "unix_to_date")
+
+ if direction == "unix_to_date":
+ ts = float(value)
+ dt_utc = datetime.fromtimestamp(ts, tz=timezone.utc)
+ return jsonify({"utc": dt_utc.isoformat(), "unix": int(ts)})
+
+ dt = None
+ for fmt in _DATE_FORMATS:
+ try:
+ dt = datetime.strptime(value, fmt).replace(tzinfo=timezone.utc)
+ break
+ except ValueError:
+ continue
+ if dt is None:
+ return jsonify({"message": "Ungültiges Datumsformat"}), 400
+
+ logger.info(f"Timestamp konvertiert von {user['username']}")
+ return jsonify({"unix": int(dt.timestamp()), "utc": dt.isoformat()})
+ except Exception as e:
+ logger.error(f"Fehler Timestamp: {e}")
+ return jsonify({"message": "Ungültiger Wert"}), 400
diff --git a/backend/util/db_pool.py b/backend/util/db_pool.py
new file mode 100644
index 0000000..fe8c0b9
--- /dev/null
+++ b/backend/util/db_pool.py
@@ -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
diff --git a/backend/util/limiter.py b/backend/util/limiter.py
new file mode 100644
index 0000000..d8bf600
--- /dev/null
+++ b/backend/util/limiter.py
@@ -0,0 +1,4 @@
+from flask_limiter import Limiter
+from flask_limiter.util import get_remote_address
+
+limiter = Limiter(key_func=get_remote_address)
diff --git a/backend/util/logger.py b/backend/util/logger.py
index 9def01a..c9e985b 100644
--- a/backend/util/logger.py
+++ b/backend/util/logger.py
@@ -1,19 +1,21 @@
import logging
import os
-# Ensure logs directory exists
os.makedirs("logs", exist_ok=True)
-# Configure logger
+fmt = "%(asctime)s [%(levelname)s] %(message)s"
+
+error_handler = logging.FileHandler("logs/error.log")
+error_handler.setLevel(logging.ERROR)
+
logging.basicConfig(
level=logging.INFO,
- format="%(asctime)s [%(levelname)s] %(message)s",
+ format=fmt,
handlers=[
logging.FileHandler("logs/app.log"),
- logging.FileHandler("logs/error.log"),
+ error_handler,
logging.StreamHandler()
]
)
-# Hauptlogger, wird von anderen Modulen importiert
logger = logging.getLogger("main")
diff --git a/backend/util/setup_routes.py b/backend/util/setup_routes.py
index 64be420..06f9142 100644
--- a/backend/util/setup_routes.py
+++ b/backend/util/setup_routes.py
@@ -2,6 +2,7 @@ import time
import os
from flask import Blueprint, request, render_template, redirect, jsonify, send_from_directory
from util.db_config import load_config, save_config, test_connection, is_configured
+from util.db_pool import reset_pool
from auth.setup_admin import initialize_admin_user
from util.logger import logger
@@ -39,6 +40,7 @@ def setup():
"database": request.form['database']
}
save_config(db_config)
+ reset_pool()
if test_connection(db_config):
initialize_admin_user(db_config)
return redirect('/')
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 92dd560..d99d87d 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -5,9 +5,8 @@ services:
dockerfile: Dockerfile
ports:
- "5050:5000"
- environment:
- - SECRET_KEY=dev-change-me
- - DB_CONFIG_PATH=/config/db_config.json
+ env_file:
+ - .env
volumes:
- config-data:/config
- logs-data:/app/backend/logs
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 23d8137..d6879bd 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -3,9 +3,16 @@ import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import LoginForm from './components/LoginForm';
//import RegisterForm from './components/RegisterForm';
import Md5Tool from './components/Md5Tool';
+import HasherTool from './components/HasherTool';
+import Base64Tool from './components/Base64Tool';
+import JwtDecoderTool from './components/JwtDecoderTool';
+import PasswordGenTool from './components/PasswordGenTool';
+import TimestampTool from './components/TimestampTool';
+import TextDiffTool from './components/TextDiffTool';
import NavBar from './components/NavBar';
import ToolOverview from './components/ToolOverview';
import AdminDashboard from './components/AdminDashboard';
+import ErrorBoundary from './components/ErrorBoundary';
import './css/base.css';
@@ -21,23 +28,31 @@ function App() {
const role = localStorage.getItem('role');
return (
-
{error}
} + {result && ( +{this.state.error?.message}
+ ++ Hinweis: bcrypt ist sicher für Passwörter – SHA256 für Datenintegrität. +
+ { setInput(e.target.value); setResult(''); }} + placeholder="Text eingeben" + /> + + + {result && ( ++ Hinweis: Dieser Decoder verifiziert keine Signatur – nur zur Analyse. +
++ Sicherheitshinweis: MD5 ist kryptografisch unsicher und darf + nicht zum Hashen von Passwörtern verwendet werden. Für Passwörter bitte + bcrypt, Argon2 oder scrypt einsetzen. +
setInput(e.target.value)} - placeholder="Gib ein Passwort ein" + placeholder="Eingabe" /> {result &&MD5: {result}
} diff --git a/frontend/src/components/PasswordGenTool.jsx b/frontend/src/components/PasswordGenTool.jsx new file mode 100644 index 0000000..b0fcf4b --- /dev/null +++ b/frontend/src/components/PasswordGenTool.jsx @@ -0,0 +1,108 @@ +import { useState } from 'react'; +import axios from '../services/api'; + +const resultBox = { + marginTop: '12px', + padding: '12px 14px', + background: 'var(--surface-2)', + border: '1px solid var(--border)', + borderRadius: '12px', + display: 'flex', + alignItems: 'center', + gap: '10px', + justifyContent: 'space-between', +}; + +function getStrength(length, charsets) { + const score = (charsets >= 3 ? 2 : charsets >= 2 ? 1 : 0) + (length >= 16 ? 2 : length >= 12 ? 1 : 0); + if (score >= 4) return { label: 'Stark', color: '#22c55e' }; + if (score >= 2) return { label: 'Mittel', color: '#f59e0b' }; + return { label: 'Schwach', color: '#ef4444' }; +} + +function PasswordGenTool() { + const [length, setLength] = useState(16); + const [uppercase, setUppercase] = useState(true); + const [lowercase, setLowercase] = useState(true); + const [numbers, setNumbers] = useState(true); + const [symbols, setSymbols] = useState(false); + const [result, setResult] = useState(''); + const [copied, setCopied] = useState(false); + const [error, setError] = useState(''); + + const charsets = [uppercase, lowercase, numbers, symbols].filter(Boolean).length; + const strength = getStrength(length, charsets); + + const generate = async () => { + setError(''); + try { + const res = await axios.post('/api/password/generate', { length, uppercase, lowercase, numbers, symbols }); + setResult(res.data.password); + } catch (err) { + setError(err.response?.data?.message || 'Fehler beim Generieren'); + } + }; + + const copy = () => { + navigator.clipboard.writeText(result); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }; + + return ( +{error}
} + {result && ( +Text A
+Text B
+Keine Unterschiede gefunden.
+ ) : ( + diff.map((line, i) => { + const c = diffColors[line.type] || diffColors.unchanged; + return ( +{error}
} + {result && ( +Wähle ein Tool aus:
-Lade Links...
) : websites.length === 0 ? ( diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 89bd47e..52b41ae 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,9 +1,6 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App.jsx'; -import './css/base.css'; -import './css/dark.css'; -import './css/light.css'; import './css/navbar.css'; diff --git a/startup.cmd b/startup.cmd deleted file mode 100644 index fe1a6c2..0000000 --- a/startup.cmd +++ /dev/null @@ -1,15 +0,0 @@ -@echo off -setlocal -cd /d %~dp0 - -echo === Fresh rebuild + start === -Docker compose -f docker-compose.dev.yml down --rmi local --remove-orphans -Docker compose -f docker-compose.dev.yml up --build --force-recreate -d || goto :err - -Docker compose -f docker-compose.dev.yml ps -echo Open http://localhost:5050/ -goto :eof - -:err -echo Startup failed. See errors above. -exit /b 1