Dateien nach "frontend/src" hochladen
This commit is contained in:
@@ -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
64
frontend/src/crypto.js
Normal 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);
|
||||
}
|
||||
@@ -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:... },
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user