Dateien nach "frontend/src" hochladen

This commit is contained in:
2026-05-27 15:32:11 +02:00
parent 6281b0cc50
commit 98972e8824
3 changed files with 104 additions and 2 deletions

View File

@@ -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 (
<div style={{...S.card,marginBottom:10,padding:'12px 14px',
border:'1px solid rgba(78,205,196,0.25)',background:'rgba(78,205,196,0.07)',
display:'flex',alignItems:'center',gap:12,cursor:'pointer'}}
onClick={()=>setActive('nachrichten')}>
<div style={{width:36,height:36,borderRadius:'50%',background:'linear-gradient(135deg,#4ecdc4,#ff6b9d)',
display:'flex',alignItems:'center',justifyContent:'center',fontSize:18,flexShrink:0}}>💬</div>
<div style={{flex:1}}>
<div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:13,fontWeight:700}}>
{count} neue Nachricht{count!==1?'en':''}
</div>
<div style={{color:'rgba(78,205,196,0.7)',fontFamily:'monospace',fontSize:10,marginTop:2}}>🔒 Ende-zu-Ende verschlüsselt</div>
</div>
<div style={{color:'rgba(78,205,196,0.5)',fontSize:16}}></div>
</div>
);
}
function Dashboard({ toast, mobile }) {
return (
<div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:1100 }}>
@@ -697,6 +725,7 @@ function Dashboard({ toast, mobile }) {
</p>
</div>
<QuickLinks toast={toast} mobile={mobile} />
<MessageBadge setActive={setActive}/>
<CalendarWidget/>
<BestellStats/>
<TodoList toast={toast} />
@@ -1227,7 +1256,7 @@ export default function App() {
<main style={mainStyle}>
{active==='dashboard' && <Dashboard toast={toast} mobile={mobile}/>}
{active==='admin' && <AdminPanel toast={toast} mobile={mobile} user={user}/>}
{activeTool && <activeTool.component toast={toast} mobile={mobile}/>}
{activeTool && <activeTool.component toast={toast} mobile={mobile} user={user}/>}
</main>
</div>
{mobile && (

64
frontend/src/crypto.js Normal file
View File

@@ -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);
}

View File

@@ -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:... },
];