Clean release: drop local startup scripts and agent stub #1
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Nirodan/Tools.git
|
||||||
|
cd Tools
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Umgebungsvariablen konfigurieren
|
||||||
|
|
||||||
|
```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**.
|
Dieses Projekt steht unter der **Creative Commons BY-NC 4.0 Lizenz**.
|
||||||
➡️ Du darfst ihn verwenden, verändern und teilen – **aber nicht kommerziell nutzen.**
|
Du darfst es 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)
|
Volltext: https://creativecommons.org/licenses/by-nc/4.0
|
||||||
|
|
||||||
💡 Hinweis: Die Urheberschaft erfolgt unter Pseudonym.
|
Author: Nirodan – https://github.com/Nirodan
|
||||||
|
|
||||||
Author: Source page: Nirodan/Github:Nirodan Production
|
|
||||||
|
|||||||
+10
-25
@@ -1,8 +1,7 @@
|
|||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from mysql.connector import connect
|
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
from auth.token import verify_token
|
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
|
from util.logger import logger
|
||||||
|
|
||||||
admin_bp = Blueprint("admin", __name__)
|
admin_bp = Blueprint("admin", __name__)
|
||||||
@@ -47,8 +46,7 @@ def list_users():
|
|||||||
if err:
|
if err:
|
||||||
return err
|
return err
|
||||||
try:
|
try:
|
||||||
cfg = load_config()
|
conn = get_connection()
|
||||||
conn = connect(**cfg)
|
|
||||||
cur = conn.cursor(dictionary=True)
|
cur = conn.cursor(dictionary=True)
|
||||||
_ensure_tables(cur)
|
_ensure_tables(cur)
|
||||||
cur.execute("SELECT id, username, role FROM users ORDER BY username ASC")
|
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:
|
if not username or not password:
|
||||||
return jsonify({"message": "Username und Passwort erforderlich"}), 400
|
return jsonify({"message": "Username und Passwort erforderlich"}), 400
|
||||||
try:
|
try:
|
||||||
cfg = load_config()
|
conn = get_connection()
|
||||||
conn = connect(**cfg)
|
|
||||||
cur = conn.cursor(dictionary=True)
|
cur = conn.cursor(dictionary=True)
|
||||||
_ensure_tables(cur)
|
_ensure_tables(cur)
|
||||||
cur.execute("SELECT id FROM users WHERE username=%s", (username,))
|
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:
|
if role is None and password is None:
|
||||||
return jsonify({"message": "Nichts zu aktualisieren"}), 400
|
return jsonify({"message": "Nichts zu aktualisieren"}), 400
|
||||||
try:
|
try:
|
||||||
cfg = load_config()
|
conn = get_connection()
|
||||||
conn = connect(**cfg)
|
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
_ensure_tables(cur)
|
_ensure_tables(cur)
|
||||||
if role:
|
if role:
|
||||||
@@ -135,8 +131,7 @@ def delete_user(user_id):
|
|||||||
if err:
|
if err:
|
||||||
return err
|
return err
|
||||||
try:
|
try:
|
||||||
cfg = load_config()
|
conn = get_connection()
|
||||||
conn = connect(**cfg)
|
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
_ensure_tables(cur)
|
_ensure_tables(cur)
|
||||||
# Schutz: Admin darf sich nicht selbst löschen
|
# Schutz: Admin darf sich nicht selbst löschen
|
||||||
@@ -170,8 +165,7 @@ def list_websites_admin():
|
|||||||
if err:
|
if err:
|
||||||
return err
|
return err
|
||||||
try:
|
try:
|
||||||
cfg = load_config()
|
conn = get_connection()
|
||||||
conn = connect(**cfg)
|
|
||||||
cur = conn.cursor(dictionary=True)
|
cur = conn.cursor(dictionary=True)
|
||||||
_ensure_tables(cur)
|
_ensure_tables(cur)
|
||||||
cur.execute("SELECT id, name, url, description FROM websites ORDER BY name ASC")
|
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:
|
if not name or not url:
|
||||||
return jsonify({"message": "Name und URL erforderlich"}), 400
|
return jsonify({"message": "Name und URL erforderlich"}), 400
|
||||||
try:
|
try:
|
||||||
cfg = load_config()
|
conn = get_connection()
|
||||||
conn = connect(**cfg)
|
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
_ensure_tables(cur)
|
_ensure_tables(cur)
|
||||||
cur.execute(
|
cur.execute(
|
||||||
@@ -221,8 +214,7 @@ def update_website(item_id):
|
|||||||
return err
|
return err
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
try:
|
try:
|
||||||
cfg = load_config()
|
conn = get_connection()
|
||||||
conn = connect(**cfg)
|
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
_ensure_tables(cur)
|
_ensure_tables(cur)
|
||||||
cur.execute(
|
cur.execute(
|
||||||
@@ -244,8 +236,7 @@ def delete_website(item_id):
|
|||||||
if err:
|
if err:
|
||||||
return err
|
return err
|
||||||
try:
|
try:
|
||||||
cfg = load_config()
|
conn = get_connection()
|
||||||
conn = connect(**cfg)
|
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
_ensure_tables(cur)
|
_ensure_tables(cur)
|
||||||
cur.execute("DELETE FROM websites WHERE id=%s", (item_id,))
|
cur.execute("DELETE FROM websites WHERE id=%s", (item_id,))
|
||||||
@@ -266,8 +257,7 @@ def list_websites_public():
|
|||||||
if not user:
|
if not user:
|
||||||
return jsonify({"message": "Nicht autorisiert"}), 401
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
try:
|
try:
|
||||||
cfg = load_config()
|
conn = get_connection()
|
||||||
conn = connect(**cfg)
|
|
||||||
cur = conn.cursor(dictionary=True)
|
cur = conn.cursor(dictionary=True)
|
||||||
_ensure_tables(cur)
|
_ensure_tables(cur)
|
||||||
cur.execute("SELECT id, name, url, description FROM websites ORDER BY name ASC")
|
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:
|
except Exception as e:
|
||||||
logger.error(f"[Public list_websites] {e}")
|
logger.error(f"[Public list_websites] {e}")
|
||||||
return jsonify({"message": "Serverfehler"}), 500
|
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.logger import logger
|
||||||
from util.db_config import is_configured, load_config, test_connection
|
from util.db_config import is_configured, load_config, test_connection
|
||||||
from util.setup_routes import setup_blueprint
|
from util.setup_routes import setup_blueprint
|
||||||
|
from util.limiter import limiter
|
||||||
from auth import auth_bp
|
from auth import auth_bp
|
||||||
from tools import md5_blueprint
|
from tools import md5_blueprint
|
||||||
from admin import admin_bp
|
from admin import admin_bp
|
||||||
|
|
||||||
app = Flask(__name__, template_folder="templates")
|
app = Flask(__name__, template_folder="templates")
|
||||||
|
limiter.init_app(app)
|
||||||
|
|
||||||
|
# Blueprints registrieren
|
||||||
# 📦 Blueprints registrieren
|
|
||||||
app.register_blueprint(setup_blueprint)
|
app.register_blueprint(setup_blueprint)
|
||||||
app.register_blueprint(auth_bp)
|
app.register_blueprint(auth_bp)
|
||||||
app.register_blueprint(md5_blueprint)
|
app.register_blueprint(md5_blueprint)
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
from flask import request, jsonify
|
from flask import request, jsonify
|
||||||
from mysql.connector import connect
|
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
from util.logger import logger
|
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
|
from auth.token import SECRET_KEY
|
||||||
|
|
||||||
|
|
||||||
|
@limiter.limit("10 per minute")
|
||||||
def login_route():
|
def login_route():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
username = data.get('username')
|
username = data.get('username')
|
||||||
@@ -18,8 +20,7 @@ def login_route():
|
|||||||
return jsonify({"message": "Server misconfigured"}), 500
|
return jsonify({"message": "Server misconfigured"}), 500
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = load_config()
|
conn = get_connection()
|
||||||
conn = connect(**config)
|
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
|
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
|
||||||
user = cursor.fetchone()
|
user = cursor.fetchone()
|
||||||
@@ -27,12 +28,12 @@ def login_route():
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
if user and check_password_hash(user['password'], password):
|
if user and check_password_hash(user['password'], password):
|
||||||
logger.info(f"✅ Login successful: {username}")
|
logger.info(f"Login successful: {username}")
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"username": user['username'],
|
"username": user['username'],
|
||||||
"role": user['role'],
|
"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")
|
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
flask
|
flask
|
||||||
flask-cors
|
flask-cors
|
||||||
|
flask-limiter
|
||||||
mysql-connector-python
|
mysql-connector-python
|
||||||
werkzeug>=2.3
|
werkzeug>=2.3
|
||||||
PyJWT
|
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)
|
||||||
@@ -5,9 +5,8 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "5050:5000"
|
- "5050:5000"
|
||||||
environment:
|
env_file:
|
||||||
- SECRET_KEY=dev-change-me
|
- .env
|
||||||
- DB_CONFIG_PATH=/config/db_config.json
|
|
||||||
volumes:
|
volumes:
|
||||||
- config-data:/config
|
- config-data:/config
|
||||||
- logs-data:/app/backend/logs
|
- logs-data:/app/backend/logs
|
||||||
|
|||||||
+20
-17
@@ -6,6 +6,7 @@ import Md5Tool from './components/Md5Tool';
|
|||||||
import NavBar from './components/NavBar';
|
import NavBar from './components/NavBar';
|
||||||
import ToolOverview from './components/ToolOverview';
|
import ToolOverview from './components/ToolOverview';
|
||||||
import AdminDashboard from './components/AdminDashboard';
|
import AdminDashboard from './components/AdminDashboard';
|
||||||
|
import ErrorBoundary from './components/ErrorBoundary';
|
||||||
|
|
||||||
|
|
||||||
import './css/base.css';
|
import './css/base.css';
|
||||||
@@ -21,23 +22,25 @@ function App() {
|
|||||||
const role = localStorage.getItem('role');
|
const role = localStorage.getItem('role');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<ErrorBoundary>
|
||||||
<NavBar />
|
<BrowserRouter>
|
||||||
<Routes>
|
<NavBar />
|
||||||
<Route path="/" element={isLoggedIn ? <ToolOverview /> : <Navigate to="/login" />} />
|
<Routes>
|
||||||
<Route path="/login" element={<LoginForm />} />
|
<Route path="/" element={isLoggedIn ? <ToolOverview /> : <Navigate to="/login" />} />
|
||||||
{/*<Route path="/register" element={<RegisterForm />} />*/}
|
<Route path="/login" element={<LoginForm />} />
|
||||||
<Route path="/tools/md5" element={isLoggedIn ? <Md5Tool /> : <Navigate to="/login" />} />
|
{/*<Route path="/register" element={<RegisterForm />} />*/}
|
||||||
<Route
|
<Route path="/tools/md5" element={isLoggedIn ? <Md5Tool /> : <Navigate to="/login" />} />
|
||||||
path="/admin"
|
<Route
|
||||||
element={
|
path="/admin"
|
||||||
isLoggedIn && role === 'admin'
|
element={
|
||||||
? <AdminDashboard />
|
isLoggedIn && role === 'admin'
|
||||||
: <Navigate to="/" />
|
? <AdminDashboard />
|
||||||
}
|
: <Navigate to="/" />
|
||||||
/>
|
}
|
||||||
</Routes>
|
/>
|
||||||
</BrowserRouter>
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Component } from 'react';
|
||||||
|
|
||||||
|
class ErrorBoundary extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false, error: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error) {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error, info) {
|
||||||
|
console.error('ErrorBoundary caught:', error, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||||
|
<h2>Ein unerwarteter Fehler ist aufgetreten.</h2>
|
||||||
|
<p style={{ opacity: 0.7 }}>{this.state.error?.message}</p>
|
||||||
|
<button onClick={() => this.setState({ hasError: false, error: null })}>
|
||||||
|
Erneut versuchen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
||||||
@@ -18,11 +18,16 @@ function Md5Tool() {
|
|||||||
return (
|
return (
|
||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
<h2>MD5 Hasher</h2>
|
<h2>MD5 Hasher</h2>
|
||||||
|
<p style={{ color: '#e07b00', fontSize: '0.875rem', marginBottom: '0.75rem' }}>
|
||||||
|
<strong>Sicherheitshinweis:</strong> MD5 ist kryptografisch unsicher und darf
|
||||||
|
nicht zum Hashen von Passwörtern verwendet werden. Für Passwörter bitte
|
||||||
|
bcrypt, Argon2 oder scrypt einsetzen.
|
||||||
|
</p>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
placeholder="Gib ein Passwort ein"
|
placeholder="Eingabe"
|
||||||
/>
|
/>
|
||||||
<button onClick={hashPassword}>Hash berechnen</button>
|
<button onClick={hashPassword}>Hash berechnen</button>
|
||||||
{result && <p><strong>MD5:</strong> {result}</p>}
|
{result && <p><strong>MD5:</strong> {result}</p>}
|
||||||
|
|||||||
Reference in New Issue
Block a user