From 98972e882476475d91668c4b21c60b031f36114a Mon Sep 17 00:00:00 2001 From: Dicken Date: Wed, 27 May 2026 15:32:11 +0200 Subject: [PATCH] Dateien nach "frontend/src" hochladen --- frontend/src/App.jsx | 31 ++++++++++++++++- frontend/src/crypto.js | 64 ++++++++++++++++++++++++++++++++++++ frontend/src/toolRegistry.js | 11 ++++++- 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 frontend/src/crypto.js 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:... }, ];