From 8e2c2d740e10743e9e6ef142f4e54d678b334a98 Mon Sep 17 00:00:00 2001 From: Nirodan Date: Thu, 22 Jan 2026 16:43:14 +0100 Subject: [PATCH 1/4] Clean release: drop local startup scripts and agent stub --- AGENT.md | 0 startup.cmd | 15 --------------- 2 files changed, 15 deletions(-) delete mode 100644 AGENT.md delete mode 100644 startup.cmd diff --git a/AGENT.md b/AGENT.md deleted file mode 100644 index e69de29..0000000 diff --git a/startup.cmd b/startup.cmd deleted file mode 100644 index fe1a6c2..0000000 --- a/startup.cmd +++ /dev/null @@ -1,15 +0,0 @@ -@echo off -setlocal -cd /d %~dp0 - -echo === Fresh rebuild + start === -Docker compose -f docker-compose.dev.yml down --rmi local --remove-orphans -Docker compose -f docker-compose.dev.yml up --build --force-recreate -d || goto :err - -Docker compose -f docker-compose.dev.yml ps -echo Open http://localhost:5050/ -goto :eof - -:err -echo Startup failed. See errors above. -exit /b 1 From 80ec5eca7bcde548297225498195b275a25abc38 Mon Sep 17 00:00:00 2001 From: Nirodan Date: Fri, 24 Apr 2026 13:52:53 +0200 Subject: [PATCH 2/4] Security, code quality and frontend improvements - Move SECRET_KEY out of docker-compose into .env (env_file), add .env.example - Add flask-limiter with 10 req/min on login route; introduce util/limiter.py - Replace direct mysql.connector.connect() calls with MySQLConnectionPool via util/db_pool.py - Fix deprecated datetime.utcnow() -> datetime.now(timezone.utc) in auth/login.py - Remove dead /api/scripts 410 route from admin.py - Add MD5 security warning in Md5Tool.jsx - Add ErrorBoundary component and wrap App.jsx - Expand README with setup guide, screenshot and project structure Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 13 +++ README.md | 126 ++++++++++++++++++---- backend/admin.py | 35 ++---- backend/app.py | 5 +- backend/auth/login.py | 15 +-- backend/requirements.txt | 1 + backend/util/db_pool.py | 26 +++++ backend/util/limiter.py | 4 + docker-compose.dev.yml | 5 +- frontend/src/App.jsx | 37 ++++--- frontend/src/components/ErrorBoundary.jsx | 33 ++++++ frontend/src/components/Md5Tool.jsx | 7 +- 12 files changed, 232 insertions(+), 75 deletions(-) create mode 100644 .env.example create mode 100644 backend/util/db_pool.py create mode 100644 backend/util/limiter.py create mode 100644 frontend/src/components/ErrorBoundary.jsx 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 +![Admin-Dashboard](logs/Screenshot%202026-01-22%20151541.png) --- -## 📦 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 ( - - - - : } /> - } /> - {/*} />*/} - : } /> - - : - } - /> - - + + + + + : } /> + } /> + {/*} />*/} + : } /> + + : + } + /> + + + ); } diff --git a/frontend/src/components/ErrorBoundary.jsx b/frontend/src/components/ErrorBoundary.jsx new file mode 100644 index 0000000..e305524 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.jsx @@ -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 ( +
+

Ein unerwarteter Fehler ist aufgetreten.

+

{this.state.error?.message}

+ +
+ ); + } + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/frontend/src/components/Md5Tool.jsx b/frontend/src/components/Md5Tool.jsx index 1af5d95..611822d 100644 --- a/frontend/src/components/Md5Tool.jsx +++ b/frontend/src/components/Md5Tool.jsx @@ -18,11 +18,16 @@ function Md5Tool() { return (

MD5 Hasher

+

+ 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}

} From 7f9c5c874a804ea52718ec93f2a538ba6213afc5 Mon Sep 17 00:00:00 2001 From: Nirodan Date: Fri, 24 Apr 2026 14:28:18 +0200 Subject: [PATCH 3/4] Add 6 new tools: Hasher, Base64, JWT Decoder, Password Gen, Timestamp, Text Diff - backend/tools/hasher.py: POST /api/hash/sha256 and /api/hash/bcrypt (bcrypt added to requirements) - backend/tools/base64tool.py: POST /api/base64/encode and /api/base64/decode - backend/tools/jwtdecoder.py: POST /api/jwt/decode (signature verification disabled) - backend/tools/passwordgen.py: POST /api/password/generate with charset and length options - backend/tools/timestamp.py: POST /api/timestamp/convert (unix<->date, ISO 8601 + German format) - backend/tools/textdiff.py: POST /api/text/diff returning structured added/removed/unchanged lines - All blueprints registered in app.py and tools/__init__.py - React components with copy button, dark/light mode support via CSS variables - ToolOverview rebuilt as card grid; App.jsx routes added for all tools Co-Authored-By: Claude Sonnet 4.6 --- backend/app.py | 16 ++- backend/requirements.txt | 1 + backend/tools/__init__.py | 6 ++ backend/tools/base64tool.py | 38 +++++++ backend/tools/hasher.py | 39 +++++++ backend/tools/jwtdecoder.py | 29 ++++++ backend/tools/passwordgen.py | 41 ++++++++ backend/tools/textdiff.py | 37 +++++++ backend/tools/timestamp.py | 46 +++++++++ frontend/src/App.jsx | 14 ++- frontend/src/components/Base64Tool.jsx | 65 ++++++++++++ frontend/src/components/HasherTool.jsx | 68 ++++++++++++ frontend/src/components/JwtDecoderTool.jsx | 69 +++++++++++++ frontend/src/components/PasswordGenTool.jsx | 108 ++++++++++++++++++++ frontend/src/components/TextDiffTool.jsx | 84 +++++++++++++++ frontend/src/components/TimestampTool.jsx | 101 ++++++++++++++++++ frontend/src/components/ToolOverview.jsx | 32 +++++- 17 files changed, 788 insertions(+), 6 deletions(-) create mode 100644 backend/tools/base64tool.py create mode 100644 backend/tools/hasher.py create mode 100644 backend/tools/jwtdecoder.py create mode 100644 backend/tools/passwordgen.py create mode 100644 backend/tools/textdiff.py create mode 100644 backend/tools/timestamp.py create mode 100644 frontend/src/components/Base64Tool.jsx create mode 100644 frontend/src/components/HasherTool.jsx create mode 100644 frontend/src/components/JwtDecoderTool.jsx create mode 100644 frontend/src/components/PasswordGenTool.jsx create mode 100644 frontend/src/components/TextDiffTool.jsx create mode 100644 frontend/src/components/TimestampTool.jsx diff --git a/backend/app.py b/backend/app.py index d62ca37..9d50df7 100644 --- a/backend/app.py +++ b/backend/app.py @@ -10,7 +10,15 @@ from util.db_config import is_configured, load_config, test_connection from util.setup_routes import setup_blueprint from util.limiter import limiter from auth import auth_bp -from tools import md5_blueprint +from tools import ( + md5_blueprint, + hasher_blueprint, + base64_blueprint, + jwt_decoder_blueprint, + passwordgen_blueprint, + timestamp_blueprint, + textdiff_blueprint, +) from admin import admin_bp app = Flask(__name__, template_folder="templates") @@ -20,6 +28,12 @@ limiter.init_app(app) app.register_blueprint(setup_blueprint) app.register_blueprint(auth_bp) app.register_blueprint(md5_blueprint) +app.register_blueprint(hasher_blueprint) +app.register_blueprint(base64_blueprint) +app.register_blueprint(jwt_decoder_blueprint) +app.register_blueprint(passwordgen_blueprint) +app.register_blueprint(timestamp_blueprint) +app.register_blueprint(textdiff_blueprint) app.register_blueprint(admin_bp) # 🌐 React-Frontend ausliefern diff --git a/backend/requirements.txt b/backend/requirements.txt index e905c84..c47fdf7 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,4 +4,5 @@ flask-limiter mysql-connector-python werkzeug>=2.3 PyJWT +bcrypt python-dotenv \ No newline at end of file diff --git a/backend/tools/__init__.py b/backend/tools/__init__.py index 45f169f..c53c248 100644 --- a/backend/tools/__init__.py +++ b/backend/tools/__init__.py @@ -1 +1,7 @@ from .md5 import md5_blueprint +from .hasher import hasher_blueprint +from .base64tool import base64_blueprint +from .jwtdecoder import jwt_decoder_blueprint +from .passwordgen import passwordgen_blueprint +from .timestamp import timestamp_blueprint +from .textdiff import textdiff_blueprint diff --git a/backend/tools/base64tool.py b/backend/tools/base64tool.py new file mode 100644 index 0000000..d9109d9 --- /dev/null +++ b/backend/tools/base64tool.py @@ -0,0 +1,38 @@ +from flask import Blueprint, request, jsonify +import base64 +from util.logger import logger +from auth.token import verify_token + +base64_blueprint = Blueprint('base64_tool', __name__) + + +@base64_blueprint.route('/api/base64/encode', methods=['POST']) +def encode(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() + text = data.get("text", "") + result = base64.b64encode(text.encode()).decode() + logger.info(f"Base64 kodiert von {user['username']}") + return jsonify({"result": result}) + except Exception as e: + logger.error(f"Fehler Base64 encode: {e}") + return jsonify({"message": "Fehler beim Kodieren"}), 500 + + +@base64_blueprint.route('/api/base64/decode', methods=['POST']) +def decode(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() + text = data.get("text", "").strip() + result = base64.b64decode(text.encode()).decode('utf-8') + logger.info(f"Base64 dekodiert von {user['username']}") + return jsonify({"result": result}) + except Exception as e: + logger.error(f"Fehler Base64 decode: {e}") + return jsonify({"message": "Ungültiger Base64-Input"}), 400 diff --git a/backend/tools/hasher.py b/backend/tools/hasher.py new file mode 100644 index 0000000..e2ae61f --- /dev/null +++ b/backend/tools/hasher.py @@ -0,0 +1,39 @@ +from flask import Blueprint, request, jsonify +import hashlib +import bcrypt +from util.logger import logger +from auth.token import verify_token + +hasher_blueprint = Blueprint('hasher_tool', __name__) + + +@hasher_blueprint.route('/api/hash/sha256', methods=['POST']) +def hash_sha256(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() + text = data.get("text", "") + result = hashlib.sha256(text.encode()).hexdigest() + logger.info(f"SHA256 erstellt von {user['username']}") + return jsonify({"sha256": result, "by": user['username']}) + except Exception as e: + logger.error(f"Fehler SHA256: {e}") + return jsonify({"message": "Fehler beim Hashen"}), 500 + + +@hasher_blueprint.route('/api/hash/bcrypt', methods=['POST']) +def hash_bcrypt(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() + text = data.get("text", "") + hashed = bcrypt.hashpw(text.encode(), bcrypt.gensalt()).decode() + logger.info(f"bcrypt erstellt von {user['username']}") + return jsonify({"bcrypt": hashed, "by": user['username']}) + except Exception as e: + logger.error(f"Fehler bcrypt: {e}") + return jsonify({"message": "Fehler beim Hashen"}), 500 diff --git a/backend/tools/jwtdecoder.py b/backend/tools/jwtdecoder.py new file mode 100644 index 0000000..9cc7561 --- /dev/null +++ b/backend/tools/jwtdecoder.py @@ -0,0 +1,29 @@ +from flask import Blueprint, request, jsonify +import jwt +from datetime import datetime, timezone +from util.logger import logger +from auth.token import verify_token + +jwt_decoder_blueprint = Blueprint('jwt_decoder_tool', __name__) + + +@jwt_decoder_blueprint.route('/api/jwt/decode', methods=['POST']) +def decode_jwt(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() + token = data.get("token", "").strip() + header = jwt.get_unverified_header(token) + payload = jwt.decode(token, options={"verify_signature": False}) + + expired = False + if "exp" in payload: + expired = payload["exp"] < datetime.now(timezone.utc).timestamp() + + logger.info(f"JWT dekodiert von {user['username']}") + return jsonify({"header": header, "payload": payload, "expired": expired}) + except Exception as e: + logger.error(f"Fehler JWT decode: {e}") + return jsonify({"message": "Ungültiger JWT Token"}), 400 diff --git a/backend/tools/passwordgen.py b/backend/tools/passwordgen.py new file mode 100644 index 0000000..33b537d --- /dev/null +++ b/backend/tools/passwordgen.py @@ -0,0 +1,41 @@ +from flask import Blueprint, request, jsonify +import secrets +import string +from util.logger import logger +from auth.token import verify_token + +passwordgen_blueprint = Blueprint('passwordgen_tool', __name__) + + +@passwordgen_blueprint.route('/api/password/generate', methods=['POST']) +def generate_password(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() + length = min(max(int(data.get("length", 16)), 8), 64) + use_uppercase = data.get("uppercase", True) + use_lowercase = data.get("lowercase", True) + use_numbers = data.get("numbers", True) + use_symbols = data.get("symbols", False) + + charset = "" + if use_uppercase: + charset += string.ascii_uppercase + if use_lowercase: + charset += string.ascii_lowercase + if use_numbers: + charset += string.digits + if use_symbols: + charset += string.punctuation + + if not charset: + return jsonify({"message": "Mindestens ein Zeichensatz muss ausgewählt sein"}), 400 + + password = ''.join(secrets.choice(charset) for _ in range(length)) + logger.info(f"Passwort generiert von {user['username']}") + return jsonify({"password": password}) + except Exception as e: + logger.error(f"Fehler Passwortgenerator: {e}") + return jsonify({"message": "Fehler beim Generieren"}), 500 diff --git a/backend/tools/textdiff.py b/backend/tools/textdiff.py new file mode 100644 index 0000000..6948cab --- /dev/null +++ b/backend/tools/textdiff.py @@ -0,0 +1,37 @@ +from flask import Blueprint, request, jsonify +import difflib +from util.logger import logger +from auth.token import verify_token + +textdiff_blueprint = Blueprint('textdiff_tool', __name__) + + +@textdiff_blueprint.route('/api/text/diff', methods=['POST']) +def text_diff(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() + text1 = data.get("text1", "") + text2 = data.get("text2", "") + + lines1 = text1.splitlines(keepends=True) + lines2 = text2.splitlines(keepends=True) + + result = [] + for line in difflib.unified_diff(lines1, lines2, lineterm=''): + if line.startswith('+++') or line.startswith('---') or line.startswith('@@'): + continue + if line.startswith('+'): + result.append({"type": "added", "text": line[1:]}) + elif line.startswith('-'): + result.append({"type": "removed", "text": line[1:]}) + else: + result.append({"type": "unchanged", "text": line[1:] if line.startswith(' ') else line}) + + logger.info(f"Text-Diff erstellt von {user['username']}") + return jsonify({"diff": result}) + except Exception as e: + logger.error(f"Fehler Text-Diff: {e}") + return jsonify({"message": "Fehler beim Vergleich"}), 500 diff --git a/backend/tools/timestamp.py b/backend/tools/timestamp.py new file mode 100644 index 0000000..1e47f46 --- /dev/null +++ b/backend/tools/timestamp.py @@ -0,0 +1,46 @@ +from flask import Blueprint, request, jsonify +from datetime import datetime, timezone +from util.logger import logger +from auth.token import verify_token + +timestamp_blueprint = Blueprint('timestamp_tool', __name__) + +_DATE_FORMATS = [ + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d", + "%d.%m.%Y %H:%M:%S", + "%d.%m.%Y", +] + + +@timestamp_blueprint.route('/api/timestamp/convert', methods=['POST']) +def convert_timestamp(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() + value = data.get("value", "").strip() + direction = data.get("direction", "unix_to_date") + + if direction == "unix_to_date": + ts = float(value) + dt_utc = datetime.fromtimestamp(ts, tz=timezone.utc) + return jsonify({"utc": dt_utc.isoformat(), "unix": int(ts)}) + + dt = None + for fmt in _DATE_FORMATS: + try: + dt = datetime.strptime(value, fmt).replace(tzinfo=timezone.utc) + break + except ValueError: + continue + if dt is None: + return jsonify({"message": "Ungültiges Datumsformat"}), 400 + + logger.info(f"Timestamp konvertiert von {user['username']}") + return jsonify({"unix": int(dt.timestamp()), "utc": dt.isoformat()}) + except Exception as e: + logger.error(f"Fehler Timestamp: {e}") + return jsonify({"message": "Ungültiger Wert"}), 400 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 28a03dd..d6879bd 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,6 +3,12 @@ import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import LoginForm from './components/LoginForm'; //import RegisterForm from './components/RegisterForm'; import Md5Tool from './components/Md5Tool'; +import HasherTool from './components/HasherTool'; +import Base64Tool from './components/Base64Tool'; +import JwtDecoderTool from './components/JwtDecoderTool'; +import PasswordGenTool from './components/PasswordGenTool'; +import TimestampTool from './components/TimestampTool'; +import TextDiffTool from './components/TextDiffTool'; import NavBar from './components/NavBar'; import ToolOverview from './components/ToolOverview'; import AdminDashboard from './components/AdminDashboard'; @@ -29,7 +35,13 @@ function App() { : } /> } /> {/*} />*/} - : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> { + 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 ( +
+

Base64 Encoder / Decoder

+ { setInput(e.target.value); setResult(''); setError(''); }} + placeholder="Text eingeben" + /> + + + {error &&

{error}

} + {result && ( +
+ + {result} + + +
+ )} +
+ ); +} + +export default Base64Tool; diff --git a/frontend/src/components/HasherTool.jsx b/frontend/src/components/HasherTool.jsx new file mode 100644 index 0000000..e32c226 --- /dev/null +++ b/frontend/src/components/HasherTool.jsx @@ -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 ( +
+

SHA256 / bcrypt Hasher

+

+ Hinweis: bcrypt ist sicher für Passwörter – SHA256 für Datenintegrität. +

+ { setInput(e.target.value); setResult(''); }} + placeholder="Text eingeben" + /> + + + {result && ( +
+ + {result} + + +
+ )} +
+ ); +} + +export default HasherTool; diff --git a/frontend/src/components/JwtDecoderTool.jsx b/frontend/src/components/JwtDecoderTool.jsx new file mode 100644 index 0000000..71643c2 --- /dev/null +++ b/frontend/src/components/JwtDecoderTool.jsx @@ -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 ( +
+

JWT Decoder

+

+ Hinweis: Dieser Decoder verifiziert keine Signatur – nur zur Analyse. +

+