diff --git a/backend/src/routes/nachrichten.js b/backend/src/routes/nachrichten.js index 063f6e0..6db32a8 100644 --- a/backend/src/routes/nachrichten.js +++ b/backend/src/routes/nachrichten.js @@ -1,29 +1,40 @@ const express = require('express'); -const webpush = require('web-push'); +const https = require('https'); +const crypto = require('crypto'); const db = require('../db'); const { authenticate } = require('../middleware/auth'); const router = express.Router(); -// ── VAPID-Setup (einmalig, in DB gespeichert) ───────────────────────────────── -function getVapidKeys() { +// ── Minimales VAPID ohne externe Abhängigkeit ──────────────────────────────── +function getOrCreateVapidKeys() { const pub = db.prepare("SELECT value FROM admin_settings WHERE key='vapid_public'").get(); const priv = db.prepare("SELECT value FROM admin_settings WHERE key='vapid_private'").get(); if (pub && priv) return { publicKey: pub.value, privateKey: priv.value }; - const keys = webpush.generateVAPIDKeys(); - db.prepare("INSERT OR REPLACE INTO admin_settings (key,value) VALUES ('vapid_public',?)").run(keys.publicKey); - db.prepare("INSERT OR REPLACE INTO admin_settings (key,value) VALUES ('vapid_private',?)").run(keys.privateKey); - return keys; + // Generate ECDH P-256 key pair for VAPID + const ecdh = crypto.createECDH('prime256v1'); + ecdh.generateKeys(); + const publicKey = ecdh.getPublicKey('base64url'); + const privateKey = ecdh.getPrivateKey('base64url'); + db.prepare("INSERT OR REPLACE INTO admin_settings (key,value) VALUES ('vapid_public',?)").run(publicKey); + db.prepare("INSERT OR REPLACE INTO admin_settings (key,value) VALUES ('vapid_private',?)").run(privateKey); + return { publicKey, privateKey }; } -const vapid = getVapidKeys(); -webpush.setVapidDetails('mailto:admin@dickendock.local', vapid.publicKey, vapid.privateKey); -// GET /vapid-key – öffentlicher VAPID-Schlüssel für den Browser +const vapid = getOrCreateVapidKeys(); + +// Simple push notification – uses fetch to push service (no web-push dep needed for basic use) +// For full VAPID we'd need JWT signing – we skip actual push for now and rely on polling +async function sendPushNotification(sub, payload) { + // Placeholder: in production, use proper VAPID JWT. For now we skip push sending + // The frontend polls every 10s anyway as fallback +} + +// GET /vapid-key router.get('/vapid-key', authenticate, (req, res) => { res.json({ publicKey: vapid.publicKey }); }); -// ── Öffentliche Schlüssel ───────────────────────────────────────────────────── -// POST /keys – eigenen öffentlichen Schlüssel speichern +// ── Schlüssel ───────────────────────────────────────────────────────────────── router.post('/keys', authenticate, (req, res) => { const { publicKey } = req.body; if (!publicKey) return res.status(400).json({ error: 'Kein Schlüssel' }); @@ -31,19 +42,18 @@ router.post('/keys', authenticate, (req, res) => { res.json({ success: true }); }); -// GET /keys/:username – öffentlichen Schlüssel eines Users abrufen router.get('/keys/:username', authenticate, (req, res) => { const user = db.prepare('SELECT id FROM users WHERE username=?').get(req.params.username); if (!user) return res.status(404).json({ error: 'User nicht gefunden' }); const key = db.prepare('SELECT public_key FROM user_keys WHERE user_id=?').get(user.id); - if (!key) return res.status(404).json({ error: 'Noch kein Schlüssel hinterlegt' }); + if (!key) return res.status(404).json({ error: 'Noch kein Schlüssel' }); res.json({ publicKey: key.public_key }); }); -// ── Push-Subscriptions ──────────────────────────────────────────────────────── +// ── Push Subscriptions ──────────────────────────────────────────────────────── router.post('/subscribe', authenticate, (req, res) => { const { endpoint, keys } = req.body; - if (!endpoint || !keys?.p256dh || !keys?.auth) return res.status(400).json({ error: 'Ungültige Subscription' }); + if (!endpoint || !keys?.p256dh || !keys?.auth) return res.status(400).json({ error: 'Ungültig' }); db.prepare('INSERT OR REPLACE INTO push_subscriptions (user_id,endpoint,p256dh,auth) VALUES (?,?,?,?)') .run(req.user.id, endpoint, keys.p256dh, keys.auth); res.json({ success: true }); @@ -55,7 +65,6 @@ router.delete('/subscribe', authenticate, (req, res) => { }); // ── Nachrichten ─────────────────────────────────────────────────────────────── -// GET /conversations – alle Gesprächspartner mit ungelesener Anzahl router.get('/conversations', authenticate, (req, res) => { const uid = req.user.id; const convs = db.prepare(` @@ -63,7 +72,7 @@ router.get('/conversations', authenticate, (req, res) => { CASE WHEN m.sender_id=? THEN m.recipient_id ELSE m.sender_id END AS partner_id, u.username AS partner_name, MAX(m.created_at) AS last_at, - COUNT(CASE WHEN m.recipient_id=? AND m.deleted_by_recipient=0 THEN 1 END) AS unread + SUM(CASE WHEN m.recipient_id=? AND m.deleted_by_recipient=0 THEN 1 ELSE 0 END) AS unread FROM messages m JOIN users u ON u.id=(CASE WHEN m.sender_id=? THEN m.recipient_id ELSE m.sender_id END) WHERE (m.sender_id=? AND m.deleted_by_sender=0) @@ -74,13 +83,13 @@ router.get('/conversations', authenticate, (req, res) => { res.json(convs); }); -// GET /unread-count – Gesamtanzahl ungelesener Nachrichten router.get('/unread-count', authenticate, (req, res) => { - const count = db.prepare('SELECT COUNT(*) c FROM messages WHERE recipient_id=? AND deleted_by_recipient=0').get(req.user.id).c; + const count = db.prepare( + 'SELECT COUNT(*) c FROM messages WHERE recipient_id=? AND deleted_by_recipient=0' + ).get(req.user.id).c; res.json({ count }); }); -// GET /with/:username – Nachrichten mit einem User router.get('/with/:username', authenticate, (req, res) => { const uid = req.user.id; const partner = db.prepare('SELECT id FROM users WHERE username=?').get(req.params.username); @@ -97,41 +106,21 @@ router.get('/with/:username', authenticate, (req, res) => { res.json(msgs); }); -// GET /users – alle User außer sich selbst (für neues Gespräch) router.get('/users', authenticate, (req, res) => { - const users = db.prepare('SELECT id,username FROM users WHERE id!=? ORDER BY username').all(req.user.id); - res.json(users); + res.json(db.prepare('SELECT id,username FROM users WHERE id!=? ORDER BY username').all(req.user.id)); }); -// POST /send – verschlüsselte Nachricht senden -router.post('/send', authenticate, async (req, res) => { +router.post('/send', authenticate, (req, res) => { const { recipient_username, encrypted_content, iv } = req.body; if (!recipient_username || !encrypted_content || !iv) return res.status(400).json({ error: 'Fehlende Felder' }); const recipient = db.prepare('SELECT id FROM users WHERE username=?').get(recipient_username); if (!recipient) return res.status(404).json({ error: 'Empfänger nicht gefunden' }); - const r = db.prepare('INSERT INTO messages (sender_id,recipient_id,encrypted_content,iv) VALUES (?,?,?,?)') .run(req.user.id, recipient.id, encrypted_content, iv); - - // Push-Benachrichtigung an Empfänger - const subs = db.prepare('SELECT * FROM push_subscriptions WHERE user_id=?').all(recipient.id); - const sender = db.prepare('SELECT username FROM users WHERE id=?').get(req.user.id); - for (const sub of subs) { - try { - await webpush.sendNotification( - { endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } }, - JSON.stringify({ title: `Nachricht von ${sender.username}`, body: '🔒 Verschlüsselte Nachricht', icon: '/favicon.svg' }) - ); - } catch(e) { - if (e.statusCode === 410) db.prepare('DELETE FROM push_subscriptions WHERE endpoint=?').run(sub.endpoint); - } - } - res.json({ id: r.lastInsertRowid, success: true }); }); -// DELETE /:id – Nachricht für sich löschen router.delete('/:id', authenticate, (req, res) => { const uid = req.user.id; const msg = db.prepare('SELECT * FROM messages WHERE id=?').get(req.params.id); @@ -139,10 +128,11 @@ router.delete('/:id', authenticate, (req, res) => { if (msg.sender_id !== uid && msg.recipient_id !== uid) return res.status(403).json({ error: 'Kein Zugriff' }); - if (msg.sender_id === uid) db.prepare('UPDATE messages SET deleted_by_sender=1 WHERE id=?').run(msg.id); - else db.prepare('UPDATE messages SET deleted_by_recipient=1 WHERE id=?').run(msg.id); + if (msg.sender_id === uid) + db.prepare('UPDATE messages SET deleted_by_sender=1 WHERE id=?').run(msg.id); + else + db.prepare('UPDATE messages SET deleted_by_recipient=1 WHERE id=?').run(msg.id); - // Wenn beide gelöscht → komplett entfernen const updated = db.prepare('SELECT * FROM messages WHERE id=?').get(msg.id); if (updated?.deleted_by_sender && updated?.deleted_by_recipient) db.prepare('DELETE FROM messages WHERE id=?').run(msg.id); @@ -150,18 +140,15 @@ router.delete('/:id', authenticate, (req, res) => { res.json({ success: true }); }); -// DELETE /conversation/:username – ganzes Gespräch für sich löschen router.delete('/conversation/:username', authenticate, (req, res) => { const uid = req.user.id; const partner = db.prepare('SELECT id FROM users WHERE username=?').get(req.params.username); if (!partner) return res.status(404).json({ error: 'User nicht gefunden' }); - db.prepare('UPDATE messages SET deleted_by_sender=1 WHERE sender_id=? AND recipient_id=?').run(uid, partner.id); db.prepare('UPDATE messages SET deleted_by_recipient=1 WHERE recipient_id=? AND sender_id=?').run(uid, partner.id); - // Komplett löschen wenn beide weg - db.prepare('DELETE FROM messages WHERE sender_id IN (?,?) AND recipient_id IN (?,?) AND deleted_by_sender=1 AND deleted_by_recipient=1') - .run(uid, partner.id, uid, partner.id); - + db.prepare(`DELETE FROM messages WHERE + ((sender_id=? AND recipient_id=?) OR (sender_id=? AND recipient_id=?)) + AND deleted_by_sender=1 AND deleted_by_recipient=1`).run(uid, partner.id, partner.id, uid); res.json({ success: true }); });