Files
dickendock/backend/nachrichten.js

169 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express');
const webpush = require('web-push');
const db = require('../db');
const { authenticate } = require('../middleware/auth');
const router = express.Router();
// ── VAPID-Setup (einmalig, in DB gespeichert) ─────────────────────────────────
function getVapidKeys() {
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;
}
const vapid = getVapidKeys();
webpush.setVapidDetails('mailto:admin@dickendock.local', vapid.publicKey, vapid.privateKey);
// GET /vapid-key öffentlicher VAPID-Schlüssel für den Browser
router.get('/vapid-key', authenticate, (req, res) => {
res.json({ publicKey: vapid.publicKey });
});
// ── Öffentliche Schlüssel ─────────────────────────────────────────────────────
// POST /keys eigenen öffentlichen Schlüssel speichern
router.post('/keys', authenticate, (req, res) => {
const { publicKey } = req.body;
if (!publicKey) return res.status(400).json({ error: 'Kein Schlüssel' });
db.prepare('INSERT OR REPLACE INTO user_keys (user_id,public_key,updated_at) VALUES (?,?,CURRENT_TIMESTAMP)').run(req.user.id, publicKey);
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' });
res.json({ publicKey: key.public_key });
});
// ── 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' });
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 });
});
router.delete('/subscribe', authenticate, (req, res) => {
db.prepare('DELETE FROM push_subscriptions WHERE user_id=?').run(req.user.id);
res.json({ success: true });
});
// ── Nachrichten ───────────────────────────────────────────────────────────────
// GET /conversations alle Gesprächspartner mit ungelesener Anzahl
router.get('/conversations', authenticate, (req, res) => {
const uid = req.user.id;
const convs = db.prepare(`
SELECT
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
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)
OR (m.recipient_id=? AND m.deleted_by_recipient=0)
GROUP BY partner_id
ORDER BY last_at DESC
`).all(uid, uid, uid, uid, uid);
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;
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);
if (!partner) return res.status(404).json({ error: 'User nicht gefunden' });
const msgs = db.prepare(`
SELECT m.id, m.sender_id, m.encrypted_content, m.iv, m.created_at,
s.username AS sender_name
FROM messages m
JOIN users s ON s.id=m.sender_id
WHERE ((m.sender_id=? AND m.recipient_id=? AND m.deleted_by_sender=0)
OR (m.sender_id=? AND m.recipient_id=? AND m.deleted_by_recipient=0))
ORDER BY m.created_at ASC
`).all(uid, partner.id, partner.id, uid);
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);
});
// POST /send verschlüsselte Nachricht senden
router.post('/send', authenticate, async (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);
if (!msg) return res.status(404).json({ error: 'Nicht gefunden' });
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);
// 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);
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);
res.json({ success: true });
});
module.exports = router;