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/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..d62ca37 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -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)
diff --git a/backend/auth/login.py b/backend/auth/login.py
index 08b937c..0d2657f 100644
--- a/backend/auth/login.py
+++ b/backend/auth/login.py
@@ -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")
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 9ebfc05..e905c84 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -1,5 +1,6 @@
flask
flask-cors
+flask-limiter
mysql-connector-python
werkzeug>=2.3
PyJWT
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/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..28a03dd 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -6,6 +6,7 @@ import Md5Tool from './components/Md5Tool';
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 +22,25 @@ function App() {
const role = localStorage.getItem('role');
return (
-
{this.state.error?.message}
+ ++ 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}
}