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 }) {
|
function Dashboard({ toast, mobile }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:1100 }}>
|
<div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:1100 }}>
|
||||||
@@ -697,6 +725,7 @@ function Dashboard({ toast, mobile }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<QuickLinks toast={toast} mobile={mobile} />
|
<QuickLinks toast={toast} mobile={mobile} />
|
||||||
|
<MessageBadge setActive={setActive}/>
|
||||||
<CalendarWidget/>
|
<CalendarWidget/>
|
||||||
<BestellStats/>
|
<BestellStats/>
|
||||||
<TodoList toast={toast} />
|
<TodoList toast={toast} />
|
||||||
@@ -1227,7 +1256,7 @@ export default function App() {
|
|||||||
<main style={mainStyle}>
|
<main style={mainStyle}>
|
||||||
{active==='dashboard' && <Dashboard toast={toast} mobile={mobile}/>}
|
{active==='dashboard' && <Dashboard toast={toast} mobile={mobile}/>}
|
||||||
{active==='admin' && <AdminPanel toast={toast} mobile={mobile} user={user}/>}
|
{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>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
{mobile && (
|
{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 Kalkulator3D, { SavedList } from './tools/kalkulator3d.jsx';
|
||||||
import Bestellungen from './tools/bestellungen.jsx';
|
import Bestellungen from './tools/bestellungen.jsx';
|
||||||
import Dateien from './tools/dateien.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 = [
|
export const TOOLS = [
|
||||||
{
|
{
|
||||||
@@ -36,6 +37,14 @@ export const TOOLS = [
|
|||||||
group: 'Werkzeuge',
|
group: 'Werkzeuge',
|
||||||
component: Dateien,
|
component: Dateien,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'nachrichten',
|
||||||
|
Icon: ZapIcon,
|
||||||
|
label: 'Nachrichten',
|
||||||
|
navLabel: 'Chat',
|
||||||
|
group: 'Werkzeuge',
|
||||||
|
component: Nachrichten,
|
||||||
|
},
|
||||||
// Neues Tool: { id:'...', Icon:..., label:'...', navLabel:'...', group:'...', component:... },
|
// Neues Tool: { id:'...', Icon:..., label:'...', navLabel:'...', group:'...', component:... },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user