From 7f9c5c874a804ea52718ec93f2a538ba6213afc5 Mon Sep 17 00:00:00 2001 From: Nirodan Date: Fri, 24 Apr 2026 14:28:18 +0200 Subject: [PATCH] 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. +

+