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 <noreply@anthropic.com>
This commit is contained in:
Nirodan
2026-04-24 14:28:18 +02:00
parent 80ec5eca7b
commit 7f9c5c874a
17 changed files with 788 additions and 6 deletions
+15 -1
View File
@@ -10,7 +10,15 @@ 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 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")
@@ -20,6 +28,12 @@ limiter.init_app(app)
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
+1
View File
@@ -4,4 +4,5 @@ flask-limiter
mysql-connector-python mysql-connector-python
werkzeug>=2.3 werkzeug>=2.3
PyJWT PyJWT
bcrypt
python-dotenv python-dotenv
+6
View File
@@ -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
+38
View File
@@ -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
+39
View File
@@ -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
+29
View File
@@ -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
+41
View File
@@ -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
+37
View File
@@ -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
+46
View File
@@ -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
+13 -1
View File
@@ -3,6 +3,12 @@ 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';
@@ -29,7 +35,13 @@ function App() {
<Route path="/" element={isLoggedIn ? <ToolOverview /> : <Navigate to="/login" />} /> <Route path="/" element={isLoggedIn ? <ToolOverview /> : <Navigate to="/login" />} />
<Route path="/login" element={<LoginForm />} /> <Route path="/login" element={<LoginForm />} />
{/*<Route path="/register" element={<RegisterForm />} />*/} {/*<Route path="/register" element={<RegisterForm />} />*/}
<Route path="/tools/md5" element={isLoggedIn ? <Md5Tool /> : <Navigate to="/login" />} /> <Route path="/tools/md5" element={isLoggedIn ? <Md5Tool /> : <Navigate to="/login" />} />
<Route path="/tools/hasher" element={isLoggedIn ? <HasherTool /> : <Navigate to="/login" />} />
<Route path="/tools/base64" element={isLoggedIn ? <Base64Tool /> : <Navigate to="/login" />} />
<Route path="/tools/jwt" element={isLoggedIn ? <JwtDecoderTool /> : <Navigate to="/login" />} />
<Route path="/tools/password" element={isLoggedIn ? <PasswordGenTool />: <Navigate to="/login" />} />
<Route path="/tools/timestamp" element={isLoggedIn ? <TimestampTool /> : <Navigate to="/login" />} />
<Route path="/tools/textdiff" element={isLoggedIn ? <TextDiffTool /> : <Navigate to="/login" />} />
<Route <Route
path="/admin" path="/admin"
element={ element={
+65
View File
@@ -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;
+68
View File
@@ -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;
+108
View File
@@ -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;
+84
View File
@@ -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;
+101
View File
@@ -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;
+28 -4
View File
@@ -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 ? (