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
|
||||||
|
|
||||||
Dieses Projekt steht unter der **Creative Commons BY-NC 4.0 Lizenz**.
|
```bash
|
||||||
➡️ Du darfst ihn verwenden, verändern und teilen – **aber nicht kommerziell nutzen.**
|
git clone https://github.com/Nirodan/Tools.git
|
||||||
Volltext: [https://creativecommons.org/licenses/by-nc/4.0] (https://creativecommons.org/licenses/by-nc/4.0)
|
cd Tools
|
||||||
|
```
|
||||||
|
|
||||||
💡 Hinweis: Die Urheberschaft erfolgt unter Pseudonym.
|
### 2. Umgebungsvariablen konfigurieren
|
||||||
|
|
||||||
Author: Source page: Nirodan/Github:Nirodan Production
|
```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
|
||||||
|
|||||||
+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
|
|
||||||
|
|||||||
+20
-4
@@ -8,17 +8,32 @@ 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,
|
||||||
|
hasher_blueprint,
|
||||||
|
base64_blueprint,
|
||||||
|
jwt_decoder_blueprint,
|
||||||
|
passwordgen_blueprint,
|
||||||
|
timestamp_blueprint,
|
||||||
|
textdiff_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)
|
||||||
|
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)
|
app.register_blueprint(admin_bp)
|
||||||
|
|
||||||
# 🌐 React-Frontend ausliefern
|
# 🌐 React-Frontend ausliefern
|
||||||
@@ -29,7 +44,8 @@ def serve_frontend(path):
|
|||||||
return redirect('/setup')
|
return redirect('/setup')
|
||||||
|
|
||||||
if path.startswith('setup') or path.startswith('api'):
|
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'))
|
dist_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'frontend', 'dist'))
|
||||||
file_path = os.path.join(dist_dir, path)
|
file_path = os.path.join(dist_dir, path)
|
||||||
|
|||||||
+14
-10
@@ -1,25 +1,29 @@
|
|||||||
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(silent=True) or {}
|
||||||
username = data.get('username')
|
username = data.get('username', '').strip()
|
||||||
password = data.get('password')
|
password = data.get('password', '')
|
||||||
|
|
||||||
|
if not username or not password:
|
||||||
|
return jsonify({"message": "Username und Passwort erforderlich"}), 400
|
||||||
|
|
||||||
if not SECRET_KEY:
|
if not SECRET_KEY:
|
||||||
logger.error("Login blocked: SECRET_KEY is not configured.")
|
logger.error("Login blocked: SECRET_KEY is not configured.")
|
||||||
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 +31,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,6 +1,8 @@
|
|||||||
flask
|
flask
|
||||||
flask-cors
|
flask-cors
|
||||||
|
flask-limiter
|
||||||
mysql-connector-python
|
mysql-connector-python
|
||||||
werkzeug>=2.3
|
werkzeug>=2.3
|
||||||
PyJWT
|
PyJWT
|
||||||
|
bcrypt
|
||||||
python-dotenv
|
python-dotenv
|
||||||
@@ -1 +1,7 @@
|
|||||||
from .md5 import md5_blueprint
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# Ensure logs directory exists
|
|
||||||
os.makedirs("logs", exist_ok=True)
|
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(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
format=fmt,
|
||||||
handlers=[
|
handlers=[
|
||||||
logging.FileHandler("logs/app.log"),
|
logging.FileHandler("logs/app.log"),
|
||||||
logging.FileHandler("logs/error.log"),
|
error_handler,
|
||||||
logging.StreamHandler()
|
logging.StreamHandler()
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Hauptlogger, wird von anderen Modulen importiert
|
|
||||||
logger = logging.getLogger("main")
|
logger = logging.getLogger("main")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import time
|
|||||||
import os
|
import os
|
||||||
from flask import Blueprint, request, render_template, redirect, jsonify, send_from_directory
|
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_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 auth.setup_admin import initialize_admin_user
|
||||||
from util.logger import logger
|
from util.logger import logger
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ def setup():
|
|||||||
"database": request.form['database']
|
"database": request.form['database']
|
||||||
}
|
}
|
||||||
save_config(db_config)
|
save_config(db_config)
|
||||||
|
reset_pool()
|
||||||
if test_connection(db_config):
|
if test_connection(db_config):
|
||||||
initialize_admin_user(db_config)
|
initialize_admin_user(db_config)
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+32
-17
@@ -3,9 +3,16 @@ import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
|||||||
import LoginForm from './components/LoginForm';
|
import LoginForm from './components/LoginForm';
|
||||||
//import RegisterForm from './components/RegisterForm';
|
//import RegisterForm from './components/RegisterForm';
|
||||||
import Md5Tool from './components/Md5Tool';
|
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 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 +28,31 @@ 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 path="/tools/hasher" element={isLoggedIn ? <HasherTool /> : <Navigate to="/login" />} />
|
||||||
element={
|
<Route path="/tools/base64" element={isLoggedIn ? <Base64Tool /> : <Navigate to="/login" />} />
|
||||||
isLoggedIn && role === 'admin'
|
<Route path="/tools/jwt" element={isLoggedIn ? <JwtDecoderTool /> : <Navigate to="/login" />} />
|
||||||
? <AdminDashboard />
|
<Route path="/tools/password" element={isLoggedIn ? <PasswordGenTool />: <Navigate to="/login" />} />
|
||||||
: <Navigate to="/" />
|
<Route path="/tools/timestamp" element={isLoggedIn ? <TimestampTool /> : <Navigate to="/login" />} />
|
||||||
}
|
<Route path="/tools/textdiff" element={isLoggedIn ? <TextDiffTool /> : <Navigate to="/login" />} />
|
||||||
/>
|
<Route
|
||||||
</Routes>
|
path="/admin"
|
||||||
</BrowserRouter>
|
element={
|
||||||
|
isLoggedIn && role === 'admin'
|
||||||
|
? <AdminDashboard />
|
||||||
|
: <Navigate to="/" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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 Base64Tool() {
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const [result, setResult] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const request = async (endpoint) => {
|
||||||
|
setError('');
|
||||||
|
setResult('');
|
||||||
|
try {
|
||||||
|
const res = await axios.post(endpoint, { text: input });
|
||||||
|
setResult(res.data.result);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || 'Fehler');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copy = () => {
|
||||||
|
navigator.clipboard.writeText(result);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main-content">
|
||||||
|
<h2>Base64 Encoder / Decoder</h2>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => { setInput(e.target.value); setResult(''); setError(''); }}
|
||||||
|
placeholder="Text eingeben"
|
||||||
|
/>
|
||||||
|
<button onClick={() => request('/api/base64/encode')}>Encode</button>
|
||||||
|
<button onClick={() => request('/api/base64/decode')}>Decode</button>
|
||||||
|
{error && <p className="error">{error}</p>}
|
||||||
|
{result && (
|
||||||
|
<div style={resultBox}>
|
||||||
|
<span style={{ wordBreak: 'break-all', color: 'var(--text)', fontFamily: 'monospace', fontSize: '0.9rem' }}>
|
||||||
|
{result}
|
||||||
|
</span>
|
||||||
|
<button className="ghost" onClick={copy} style={{ flexShrink: 0, margin: 0 }}>
|
||||||
|
{copied ? 'Kopiert!' : 'Kopieren'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Base64Tool;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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 HasherTool() {
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const [algo, setAlgo] = useState('sha256');
|
||||||
|
const [result, setResult] = useState('');
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleHash = async () => {
|
||||||
|
try {
|
||||||
|
const res = await axios.post(`/api/hash/${algo}`, { text: input });
|
||||||
|
setResult(res.data[algo]);
|
||||||
|
} catch {
|
||||||
|
alert('Fehler beim Hashen');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copy = () => {
|
||||||
|
navigator.clipboard.writeText(result);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main-content">
|
||||||
|
<h2>SHA256 / bcrypt Hasher</h2>
|
||||||
|
<p style={{ color: 'var(--accent)', fontSize: '0.875rem', marginBottom: '0.75rem' }}>
|
||||||
|
<strong>Hinweis:</strong> bcrypt ist sicher für Passwörter – SHA256 für Datenintegrität.
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => { setInput(e.target.value); setResult(''); }}
|
||||||
|
placeholder="Text eingeben"
|
||||||
|
/>
|
||||||
|
<select value={algo} onChange={(e) => { setAlgo(e.target.value); setResult(''); }} style={{ marginTop: '8px' }}>
|
||||||
|
<option value="sha256">SHA256</option>
|
||||||
|
<option value="bcrypt">bcrypt</option>
|
||||||
|
</select>
|
||||||
|
<button onClick={handleHash}>Hash berechnen</button>
|
||||||
|
{result && (
|
||||||
|
<div style={resultBox}>
|
||||||
|
<span style={{ wordBreak: 'break-all', color: 'var(--text)', fontFamily: 'monospace', fontSize: '0.9rem' }}>
|
||||||
|
{result}
|
||||||
|
</span>
|
||||||
|
<button className="ghost" onClick={copy} style={{ flexShrink: 0, margin: 0 }}>
|
||||||
|
{copied ? 'Kopiert!' : 'Kopieren'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HasherTool;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import axios from '../services/api';
|
||||||
|
|
||||||
|
const sectionBox = {
|
||||||
|
marginTop: '12px',
|
||||||
|
padding: '14px',
|
||||||
|
background: 'var(--surface-2)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
};
|
||||||
|
|
||||||
|
function JwtDecoderTool() {
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const [decoded, setDecoded] = useState(null);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const handleDecode = async () => {
|
||||||
|
setError('');
|
||||||
|
setDecoded(null);
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/api/jwt/decode', { token: input });
|
||||||
|
setDecoded(res.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || 'Ungültiger JWT');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main-content">
|
||||||
|
<h2>JWT Decoder</h2>
|
||||||
|
<p style={{ color: 'var(--accent)', fontSize: '0.875rem', marginBottom: '0.75rem' }}>
|
||||||
|
<strong>Hinweis:</strong> Dieser Decoder verifiziert keine Signatur – nur zur Analyse.
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => { setInput(e.target.value); setDecoded(null); setError(''); }}
|
||||||
|
placeholder="JWT Token einfügen"
|
||||||
|
style={{ resize: 'vertical', fontFamily: 'monospace', fontSize: '0.85rem' }}
|
||||||
|
/>
|
||||||
|
<button onClick={handleDecode}>Dekodieren</button>
|
||||||
|
{error && <p className="error">{error}</p>}
|
||||||
|
{decoded && (
|
||||||
|
<>
|
||||||
|
<div style={sectionBox}>
|
||||||
|
<p className="eyebrow" style={{ marginBottom: '6px' }}>Header</p>
|
||||||
|
<pre style={{ margin: 0, color: 'var(--text)', fontSize: '0.875rem', whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
|
||||||
|
{JSON.stringify(decoded.header, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<div style={sectionBox}>
|
||||||
|
<p className="eyebrow" style={{ marginBottom: '6px' }}>Payload</p>
|
||||||
|
<pre style={{ margin: 0, color: 'var(--text)', fontSize: '0.875rem', whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
|
||||||
|
{JSON.stringify(decoded.payload, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<div style={{ ...sectionBox, borderColor: decoded.expired ? '#ef4444' : '#22c55e' }}>
|
||||||
|
<p className="eyebrow" style={{ marginBottom: '4px' }}>Status</p>
|
||||||
|
<span style={{ fontWeight: 700, color: decoded.expired ? '#ef4444' : '#22c55e' }}>
|
||||||
|
{decoded.expired ? 'Abgelaufen' : 'Gültig'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JwtDecoderTool;
|
||||||
@@ -12,8 +12,6 @@ function LoginForm() {
|
|||||||
const res = await axios.post('/api/login', { username, password });
|
const res = await axios.post('/api/login', { username, password });
|
||||||
localStorage.setItem('token', res.data.token);
|
localStorage.setItem('token', res.data.token);
|
||||||
localStorage.setItem('role', res.data.role);
|
localStorage.setItem('role', res.data.role);
|
||||||
navigate('/', { replace: true });
|
|
||||||
// ensure nav + route state reflect the new token immediately
|
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Login fehlgeschlagen');
|
alert('Login fehlgeschlagen');
|
||||||
|
|||||||
@@ -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>}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="main-content">
|
||||||
|
<h2>Passwort-Generator</h2>
|
||||||
|
|
||||||
|
<label style={{ display: 'block', color: 'var(--muted)', fontWeight: 600, marginBottom: '6px' }}>
|
||||||
|
Länge: {length}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={8}
|
||||||
|
max={64}
|
||||||
|
value={length}
|
||||||
|
onChange={(e) => setLength(Number(e.target.value))}
|
||||||
|
style={{ accentColor: 'var(--accent)', padding: 0, border: 'none', background: 'transparent', marginBottom: '12px' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '16px', margin: '8px 0 12px' }}>
|
||||||
|
{[
|
||||||
|
{ label: 'Großbuchstaben', value: uppercase, set: setUppercase },
|
||||||
|
{ label: 'Kleinbuchstaben', value: lowercase, set: setLowercase },
|
||||||
|
{ label: 'Zahlen', value: numbers, set: setNumbers },
|
||||||
|
{ label: 'Sonderzeichen', value: symbols, set: setSymbols },
|
||||||
|
].map(({ label, value, set }) => (
|
||||||
|
<label key={label} style={{ display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer', color: 'var(--text)', fontWeight: 500 }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={value}
|
||||||
|
onChange={(e) => set(e.target.checked)}
|
||||||
|
style={{ width: 'auto', accentColor: 'var(--accent)' }}
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '4px' }}>
|
||||||
|
<span className="muted" style={{ fontSize: '0.875rem' }}>Stärke:</span>
|
||||||
|
<span style={{ fontWeight: 700, color: strength.color }}>{strength.label}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onClick={generate}>Generieren</button>
|
||||||
|
{error && <p className="error">{error}</p>}
|
||||||
|
{result && (
|
||||||
|
<div style={resultBox}>
|
||||||
|
<span style={{ wordBreak: 'break-all', color: 'var(--text)', fontFamily: 'monospace', fontSize: '0.95rem' }}>
|
||||||
|
{result}
|
||||||
|
</span>
|
||||||
|
<button className="ghost" onClick={copy} style={{ flexShrink: 0, margin: 0 }}>
|
||||||
|
{copied ? 'Kopiert!' : 'Kopieren'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PasswordGenTool;
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import axios from '../services/api';
|
||||||
|
|
||||||
|
const diffColors = {
|
||||||
|
added: { bg: 'rgba(34, 197, 94, 0.12)', text: '#22c55e', prefix: '+ ' },
|
||||||
|
removed: { bg: 'rgba(239, 68, 68, 0.12)', text: '#ef4444', prefix: '- ' },
|
||||||
|
unchanged: { bg: 'transparent', text: 'var(--muted)', prefix: ' ' },
|
||||||
|
};
|
||||||
|
|
||||||
|
function TextDiffTool() {
|
||||||
|
const [text1, setText1] = useState('');
|
||||||
|
const [text2, setText2] = useState('');
|
||||||
|
const [diff, setDiff] = useState(null);
|
||||||
|
|
||||||
|
const compare = async () => {
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/api/text/diff', { text1, text2 });
|
||||||
|
setDiff(res.data.diff);
|
||||||
|
} catch {
|
||||||
|
alert('Fehler beim Vergleich');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main-content">
|
||||||
|
<h2>Text Diff</h2>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
|
||||||
|
<div>
|
||||||
|
<p className="muted" style={{ marginBottom: '6px', fontWeight: 600 }}>Text A</p>
|
||||||
|
<textarea
|
||||||
|
rows={10}
|
||||||
|
value={text1}
|
||||||
|
onChange={(e) => { setText1(e.target.value); setDiff(null); }}
|
||||||
|
placeholder="Originaltext eingeben..."
|
||||||
|
style={{ resize: 'vertical', fontFamily: 'monospace', fontSize: '0.875rem' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="muted" style={{ marginBottom: '6px', fontWeight: 600 }}>Text B</p>
|
||||||
|
<textarea
|
||||||
|
rows={10}
|
||||||
|
value={text2}
|
||||||
|
onChange={(e) => { setText2(e.target.value); setDiff(null); }}
|
||||||
|
placeholder="Geänderter Text eingeben..."
|
||||||
|
style={{ resize: 'vertical', fontFamily: 'monospace', fontSize: '0.875rem' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onClick={compare}>Vergleichen</button>
|
||||||
|
|
||||||
|
{diff !== null && (
|
||||||
|
<div style={{ marginTop: '16px', border: '1px solid var(--border)', borderRadius: '12px', overflow: 'hidden' }}>
|
||||||
|
{diff.length === 0 ? (
|
||||||
|
<p style={{ padding: '12px 14px', color: 'var(--muted)', margin: 0 }}>Keine Unterschiede gefunden.</p>
|
||||||
|
) : (
|
||||||
|
diff.map((line, i) => {
|
||||||
|
const c = diffColors[line.type] || diffColors.unchanged;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
padding: '2px 14px',
|
||||||
|
background: c.bg,
|
||||||
|
color: c.text,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
borderLeft: `3px solid ${line.type !== 'unchanged' ? c.text : 'transparent'}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{c.prefix}{line.text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextDiffTool;
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { useState, useEffect } 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 TimestampTool() {
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
const [direction, setDirection] = useState('unix_to_date');
|
||||||
|
const [result, setResult] = useState(null);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [now, setNow] = useState(Math.floor(Date.now() / 1000));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => setNow(Math.floor(Date.now() / 1000)), 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const convert = async () => {
|
||||||
|
setError('');
|
||||||
|
setResult(null);
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/api/timestamp/convert', { value, direction });
|
||||||
|
setResult(res.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || 'Ungültiger Wert');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resultText = result
|
||||||
|
? direction === 'unix_to_date' ? result.utc : String(result.unix)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const copy = () => {
|
||||||
|
navigator.clipboard.writeText(resultText);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main-content">
|
||||||
|
<h2>Timestamp Converter</h2>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '12px', padding: '10px 14px', background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: '12px' }}>
|
||||||
|
<span className="muted" style={{ fontSize: '0.875rem' }}>Aktueller Unix Timestamp: </span>
|
||||||
|
<span style={{ fontFamily: 'monospace', color: 'var(--accent)', fontWeight: 700 }}>{now}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
|
||||||
|
{[
|
||||||
|
{ key: 'unix_to_date', label: 'Unix → Datum' },
|
||||||
|
{ key: 'date_to_unix', label: 'Datum → Unix' },
|
||||||
|
].map(({ key, label }) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
className={direction === key ? '' : 'ghost'}
|
||||||
|
onClick={() => { setDirection(key); setResult(null); setError(''); }}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => { setValue(e.target.value); setResult(null); setError(''); }}
|
||||||
|
placeholder={direction === 'unix_to_date' ? 'z.B. 1700000000' : 'z.B. 2024-01-15 oder 15.01.2024'}
|
||||||
|
/>
|
||||||
|
<button onClick={convert}>Konvertieren</button>
|
||||||
|
{error && <p className="error">{error}</p>}
|
||||||
|
{result && (
|
||||||
|
<div style={resultBox}>
|
||||||
|
<div>
|
||||||
|
{direction === 'unix_to_date' ? (
|
||||||
|
<span style={{ fontFamily: 'monospace', color: 'var(--text)' }}>{result.utc}</span>
|
||||||
|
) : (
|
||||||
|
<span style={{ fontFamily: 'monospace', color: 'var(--text)' }}>{result.unix}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button className="ghost" onClick={copy} style={{ flexShrink: 0, margin: 0 }}>
|
||||||
|
{copied ? 'Kopiert!' : 'Kopieren'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimestampTool;
|
||||||
@@ -2,9 +2,18 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import axios from '../services/api';
|
import axios from '../services/api';
|
||||||
|
|
||||||
|
const TOOLS = [
|
||||||
|
{ icon: '🔒', path: '/tools/md5', title: 'MD5 Hasher', desc: 'MD5-Hash berechnen (nur zur Analyse)' },
|
||||||
|
{ icon: '#️⃣', path: '/tools/hasher', title: 'SHA256 / bcrypt', desc: 'Sichere Hash-Algorithmen für Strings' },
|
||||||
|
{ icon: '📦', path: '/tools/base64', title: 'Base64', desc: 'Text kodieren und dekodieren' },
|
||||||
|
{ icon: '🔑', path: '/tools/jwt', title: 'JWT Decoder', desc: 'JWT Token analysieren (ohne Signaturprüfung)' },
|
||||||
|
{ icon: '🔐', path: '/tools/password', title: 'Passwort-Generator', desc: 'Sichere Passwörter generieren' },
|
||||||
|
{ icon: '🕐', path: '/tools/timestamp', title: 'Timestamp Converter', desc: 'Unix Timestamp in Datum umrechnen' },
|
||||||
|
{ icon: '📝', path: '/tools/textdiff', title: 'Text Diff', desc: 'Zwei Texte vergleichen' },
|
||||||
|
];
|
||||||
|
|
||||||
function ToolOverview() {
|
function ToolOverview() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const role = localStorage.getItem('role');
|
|
||||||
const [websites, setWebsites] = useState([]);
|
const [websites, setWebsites] = useState([]);
|
||||||
const [loadingWebsites, setLoadingWebsites] = useState(true);
|
const [loadingWebsites, setLoadingWebsites] = useState(true);
|
||||||
|
|
||||||
@@ -13,7 +22,7 @@ function ToolOverview() {
|
|||||||
try {
|
try {
|
||||||
const res = await axios.get('/api/websites');
|
const res = await axios.get('/api/websites');
|
||||||
setWebsites(res.data);
|
setWebsites(res.data);
|
||||||
} catch (e) {
|
} catch {
|
||||||
setWebsites([]);
|
setWebsites([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingWebsites(false);
|
setLoadingWebsites(false);
|
||||||
@@ -27,9 +36,24 @@ function ToolOverview() {
|
|||||||
<h2>Tool-Übersicht</h2>
|
<h2>Tool-Übersicht</h2>
|
||||||
<p>Wähle ein Tool aus:</p>
|
<p>Wähle ein Tool aus:</p>
|
||||||
|
|
||||||
<button onClick={() => navigate('/tools/md5')}>🔒 MD5 Tool</button><br /><br />
|
<div className="card-grid">
|
||||||
|
{TOOLS.map((tool) => (
|
||||||
|
<div
|
||||||
|
key={tool.path}
|
||||||
|
className="link-card"
|
||||||
|
onClick={() => navigate(tool.path)}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<div className="link-card__icon">{tool.icon}</div>
|
||||||
|
<div>
|
||||||
|
<div className="link-card__title">{tool.title}</div>
|
||||||
|
<div className="link-card__desc">{tool.desc}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 style={{ marginTop: '24px' }}>🌐 Externe Webseiten</h3>
|
<h3 style={{ marginTop: '24px' }}>Externe Webseiten</h3>
|
||||||
{loadingWebsites ? (
|
{loadingWebsites ? (
|
||||||
<p className="muted">Lade Links...</p>
|
<p className="muted">Lade Links...</p>
|
||||||
) : websites.length === 0 ? (
|
) : websites.length === 0 ? (
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import App from './App.jsx';
|
import App from './App.jsx';
|
||||||
import './css/base.css';
|
|
||||||
import './css/dark.css';
|
|
||||||
import './css/light.css';
|
|
||||||
import './css/navbar.css';
|
import './css/navbar.css';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
-15
@@ -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
|
|
||||||
Reference in New Issue
Block a user