Files
dickendock/backend/src/tools/nachrichten/routes.js

211 lines
9.0 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 db = require('../../db');
const { authenticate } = require('../../middleware/auth');
const router = express.Router();
// ── Pushover Helper ───────────────────────────────────────────────────────────
async function sendPushover(userKey, appToken, title, message, opts = {}) {
try {
const params = { token: appToken, user: userKey, title, message };
if (opts.retry && opts.expire) {
params.priority = 2;
params.retry = opts.retry;
params.expire = opts.expire;
}
await fetch('https://api.pushover.net/1/messages.json', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(params),
});
} catch {}
}
// ── Public Keys ───────────────────────────────────────────────────────────────
router.get('/keys/:userId', authenticate, (req, res) => {
const row = db.prepare('SELECT public_key FROM user_keys WHERE user_id = ?').get(req.params.userId);
res.json({ public_key: row?.public_key || null });
});
router.post('/keys', authenticate, (req, res) => {
const { public_key } = req.body;
if (!public_key) return res.status(400).json({ error: 'Kein Schlüssel' });
db.prepare(`
INSERT INTO user_keys (user_id, public_key, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(user_id) DO UPDATE SET public_key=excluded.public_key, updated_at=CURRENT_TIMESTAMP
`).run(req.user.id, public_key);
res.json({ ok: true });
});
// ── Users ─────────────────────────────────────────────────────────────────────
// Presence: User ist gerade aktiv im Chat
router.post('/presence', authenticate, (req, res) => {
const { active } = req.body;
if (active) {
db.prepare("UPDATE users SET chat_active_at=datetime('now','localtime') WHERE id=?").run(req.user.id);
} else {
db.prepare("UPDATE users SET chat_active_at=NULL WHERE id=?").run(req.user.id);
}
res.json({ ok: true });
});
router.get('/users', authenticate, (req, res) => {
const me = req.user.id;
const isAdmin = req.user.role === 'admin';
// Admins sehen alle. Normale User sehen:
// - keine hidden User
// - wenn sie selbst hidden sind: nur Admins
const meUser = db.prepare('SELECT hidden FROM users WHERE id=?').get(me);
const isMeHidden = !!meUser?.hidden;
let whereExtra = '';
if (!isAdmin && isMeHidden) {
// Ich bin hidden → sehe nur Admins
whereExtra = "AND u.role = 'admin'";
} else if (!isAdmin) {
// Normaler User → sieht keine hidden User
whereExtra = "AND (u.hidden = 0 OR u.hidden IS NULL)";
}
// Admin → kein Filter
const users = db.prepare(`
SELECT u.id, u.username, uk.public_key,
(SELECT COUNT(*) FROM messages
WHERE sender_id = u.id AND recipient_id = ? AND read_by_recipient = 0) AS unread,
(SELECT MAX(created_at) FROM messages
WHERE (sender_id = u.id AND recipient_id = ?)
OR (sender_id = ? AND recipient_id = u.id)) AS last_message_at
FROM users u
LEFT JOIN user_keys uk ON uk.user_id = u.id
WHERE u.id != ? ${whereExtra}
ORDER BY last_message_at DESC, u.username ASC
`).all(me, me, me, me);
res.json(users);
});
// ── Unread Count ──────────────────────────────────────────────────────────────
router.get('/unread', authenticate, (req, res) => {
const row = db.prepare(
'SELECT COUNT(*) AS count FROM messages WHERE recipient_id = ? AND read_by_recipient = 0'
).get(req.user.id);
res.json({ count: row.count });
});
// ── Messages ──────────────────────────────────────────────────────────────────
router.get('/messages/:userId', authenticate, (req, res) => {
const me = req.user.id;
const other = parseInt(req.params.userId);
db.prepare(`
UPDATE messages SET read_by_recipient = 1
WHERE recipient_id = ? AND sender_id = ? AND read_by_recipient = 0
`).run(me, other);
const msgs = db.prepare(`
SELECT id, sender_id, encrypted_content, iv, created_at,
(sender_id = ?) AS is_mine,
read_by_recipient
FROM messages
WHERE (sender_id = ? AND recipient_id = ?)
OR (recipient_id = ? AND sender_id = ?)
ORDER BY created_at ASC
`).all(me, me, other, me, other);
res.json(msgs);
});
router.post('/messages/:userId', authenticate, async (req, res) => {
const me = req.user.id;
const recipId = parseInt(req.params.userId);
const { encrypted_content, iv } = req.body;
if (!encrypted_content || !iv) return res.status(400).json({ error: 'Fehlende Felder' });
const recipient = db.prepare('SELECT id FROM users WHERE id = ?').get(recipId);
if (!recipient) return res.status(404).json({ error: 'Benutzer nicht gefunden' });
const result = db.prepare(
"INSERT INTO messages (sender_id, recipient_id, encrypted_content, iv, created_at) VALUES (?, ?, ?, ?, datetime('now', 'localtime'))"
).run(me, recipId, encrypted_content, iv);
const senderName = db.prepare('SELECT username FROM users WHERE id = ?').get(me)?.username || 'Jemand';
const pushover = db.prepare('SELECT * FROM pushover_settings WHERE user_id = ?').get(recipId);
if (pushover) {
// Kein Push wenn Empfänger gerade im Chat ist (aktiv in letzten 45 Sekunden)
const presence = db.prepare(`
SELECT chat_active_at FROM users WHERE id=? AND chat_active_at > datetime('now','localtime','-45 seconds')
`).get(recipId);
if (!presence) {
await sendPushover(pushover.user_key, pushover.app_token, 'DickenDock', `Neue Nachricht von ${senderName}`,
{ retry: pushover.retry, expire: pushover.expire });
}
}
res.json({ id: result.lastInsertRowid });
});
// DELETE einzelne Nachricht hard delete, beide Seiten dürfen löschen
router.delete('/messages/:messageId', authenticate, (req, res) => {
const me = req.user.id;
const mid = parseInt(req.params.messageId);
db.prepare(
'DELETE FROM messages WHERE id = ? AND (sender_id = ? OR recipient_id = ?)'
).run(mid, me, me);
res.json({ ok: true });
});
// DELETE gesamte Konversation hard delete
router.delete('/conversation/:userId', authenticate, (req, res) => {
const me = req.user.id;
const other = parseInt(req.params.userId);
db.prepare(`
DELETE FROM messages
WHERE (sender_id = ? AND recipient_id = ?) OR (sender_id = ? AND recipient_id = ?)
`).run(me, other, other, me);
res.json({ ok: true });
});
// DELETE alle eigenen Nachrichten (für Schlüssel-Reset)
router.delete('/my-messages', authenticate, (req, res) => {
db.prepare('DELETE FROM messages WHERE sender_id = ? OR recipient_id = ?')
.run(req.user.id, req.user.id);
res.json({ ok: true });
});
// ── Pushover Settings ─────────────────────────────────────────────────────────
router.get('/pushover', authenticate, (req, res) => {
const row = db.prepare('SELECT user_key, app_token, retry, expire FROM pushover_settings WHERE user_id = ?').get(req.user.id);
res.json(row || { user_key: '', app_token: '', retry: null, expire: null });
});
router.post('/pushover', authenticate, (req, res) => {
const { user_key, app_token, retry, expire } = req.body;
if (!user_key || !app_token) return res.status(400).json({ error: 'Beide Felder erforderlich' });
db.prepare(`
INSERT INTO pushover_settings (user_id, user_key, app_token, retry, expire)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(user_id) DO UPDATE SET user_key=excluded.user_key, app_token=excluded.app_token,
retry=excluded.retry, expire=excluded.expire
`).run(req.user.id, user_key, app_token, retry || null, expire || null);
res.json({ ok: true });
});
router.delete('/pushover', authenticate, (req, res) => {
db.prepare('DELETE FROM pushover_settings WHERE user_id = ?').run(req.user.id);
res.json({ ok: true });
});
router.post('/pushover/test', authenticate, async (req, res) => {
const row = db.prepare('SELECT * FROM pushover_settings WHERE user_id = ?').get(req.user.id);
if (!row) return res.status(400).json({ error: 'Keine Pushover-Einstellungen gespeichert' });
const useEmergency = req.body?.emergency === true && row.retry && row.expire;
const msg = useEmergency
? `🚨 Emergency-Test! Wiederholt alle ${row.retry}s für max. ${row.expire}s. Bitte in Pushover quittieren.`
: 'Pushover-Verbindung erfolgreich! 🎉';
await sendPushover(row.user_key, row.app_token, 'DickenDock', msg,
useEmergency ? { retry: row.retry, expire: row.expire } : {});
res.json({ ok: true });
});
module.exports = router;