diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index cc40285..c007e01 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -687,6 +687,34 @@ function BestellStats() {
);
}
+// ── Nachrichten-Badge ─────────────────────────────────────────────────────────
+function MessageBadge({ setActive }) {
+ const [count, setCount] = useState(0);
+ useEffect(() => {
+ const load = () => api('/nachrichten/unread-count').then(d=>setCount(d.count||0)).catch(()=>{});
+ load();
+ const t = setInterval(load, 15000);
+ return () => clearInterval(t);
+ }, []);
+ if (count === 0) return null;
+ return (
+
setActive('nachrichten')}>
+
💬
+
+
+ {count} neue Nachricht{count!==1?'en':''}
+
+
🔒 Ende-zu-Ende verschlüsselt
+
+
›
+
+ );
+}
+
function Dashboard({ toast, mobile }) {
return (
@@ -697,6 +725,7 @@ function Dashboard({ toast, mobile }) {
+
@@ -1227,7 +1256,7 @@ export default function App() {
{active==='dashboard' && }
{active==='admin' && }
- {activeTool && }
+ {activeTool && }
{mobile && (
diff --git a/frontend/src/crypto.js b/frontend/src/crypto.js
new file mode 100644
index 0000000..c67825a
--- /dev/null
+++ b/frontend/src/crypto.js
@@ -0,0 +1,64 @@
+// ── End-to-End Verschlüsselung (Web Crypto API) ───────────────────────────────
+// Jeder User hat ein ECDH-Schlüsselpaar. Nachrichten werden mit einem
+// gemeinsamen AES-GCM-Schlüssel verschlüsselt, der aus beiden Schlüsseln
+// abgeleitet wird. Der Server sieht nur den verschlüsselten Ciphertext.
+
+const STORE_KEY = 'dd_ec_keypair';
+
+export async function getOrCreateKeyPair() {
+ const stored = localStorage.getItem(STORE_KEY);
+ if (stored) {
+ const { pub, priv } = JSON.parse(stored);
+ const publicKey = await crypto.subtle.importKey('jwk', pub, { name:'ECDH', namedCurve:'P-256' }, true, []);
+ const privateKey = await crypto.subtle.importKey('jwk', priv, { name:'ECDH', namedCurve:'P-256' }, true, ['deriveKey']);
+ return { publicKey, privateKey };
+ }
+ const pair = await crypto.subtle.generateKey({ name:'ECDH', namedCurve:'P-256' }, true, ['deriveKey']);
+ const pub = await crypto.subtle.exportKey('jwk', pair.publicKey);
+ const priv = await crypto.subtle.exportKey('jwk', pair.privateKey);
+ localStorage.setItem(STORE_KEY, JSON.stringify({ pub, priv }));
+ return pair;
+}
+
+export async function exportPublicKey() {
+ const { publicKey } = await getOrCreateKeyPair();
+ const jwk = await crypto.subtle.exportKey('jwk', publicKey);
+ return JSON.stringify(jwk);
+}
+
+async function deriveSharedKey(myPrivateKey, theirPublicKeyJwk) {
+ const theirKey = await crypto.subtle.importKey(
+ 'jwk', JSON.parse(theirPublicKeyJwk),
+ { name:'ECDH', namedCurve:'P-256' }, false, []
+ );
+ return crypto.subtle.deriveKey(
+ { name:'ECDH', public: theirKey },
+ myPrivateKey,
+ { name:'AES-GCM', length:256 }, false, ['encrypt','decrypt']
+ );
+}
+
+export async function encryptMessage(plaintext, theirPublicKeyJwk) {
+ const { privateKey } = await getOrCreateKeyPair();
+ const sharedKey = await deriveSharedKey(privateKey, theirPublicKeyJwk);
+ const iv = crypto.getRandomValues(new Uint8Array(12));
+ const enc = new TextEncoder();
+ const ciphertext = await crypto.subtle.encrypt({ name:'AES-GCM', iv }, sharedKey, enc.encode(plaintext));
+ return {
+ encrypted_content: btoa(String.fromCharCode(...new Uint8Array(ciphertext))),
+ iv: btoa(String.fromCharCode(...iv)),
+ };
+}
+
+export async function decryptMessage(encryptedContent, ivBase64, theirPublicKeyJwk) {
+ const { privateKey } = await getOrCreateKeyPair();
+ const sharedKey = await deriveSharedKey(privateKey, theirPublicKeyJwk);
+ const ciphertext = Uint8Array.from(atob(encryptedContent), c => c.charCodeAt(0));
+ const iv = Uint8Array.from(atob(ivBase64), c => c.charCodeAt(0));
+ const plaintext = await crypto.subtle.decrypt({ name:'AES-GCM', iv }, sharedKey, ciphertext);
+ return new TextDecoder().decode(plaintext);
+}
+
+export function hasLocalKeyPair() {
+ return !!localStorage.getItem(STORE_KEY);
+}
diff --git a/frontend/src/toolRegistry.js b/frontend/src/toolRegistry.js
index 328460d..272cea4 100644
--- a/frontend/src/toolRegistry.js
+++ b/frontend/src/toolRegistry.js
@@ -1,7 +1,8 @@
import Kalkulator3D, { SavedList } from './tools/kalkulator3d.jsx';
import Bestellungen from './tools/bestellungen.jsx';
import Dateien from './tools/dateien.jsx';
-import { CalculatorIcon, ArchiveIcon, DatabaseIcon, AppsIcon } from './icons.jsx';
+import { CalculatorIcon, ArchiveIcon, DatabaseIcon, AppsIcon, ZapIcon } from './icons.jsx';
+import Nachrichten from './tools/nachrichten.jsx';
export const TOOLS = [
{
@@ -36,6 +37,14 @@ export const TOOLS = [
group: 'Werkzeuge',
component: Dateien,
},
+ {
+ id: 'nachrichten',
+ Icon: ZapIcon,
+ label: 'Nachrichten',
+ navLabel: 'Chat',
+ group: 'Werkzeuge',
+ component: Nachrichten,
+ },
// Neues Tool: { id:'...', Icon:..., label:'...', navLabel:'...', group:'...', component:... },
];