Whiteboard: neues Tool mit Canvas, Undo, Kollaboration via WebSocket, Berechtigungssystem

This commit is contained in:
2026-06-25 20:38:03 +02:00
parent 8eea117321
commit e96946e246
6 changed files with 964 additions and 4 deletions

View File

@@ -0,0 +1,153 @@
const express = require('express');
const db = require('../../db');
const { authenticate, requireAdmin } = require('../../middleware/auth');
const router = express.Router();
// ── DB-Migration ──────────────────────────────────────────────────────────────
(function migrate() {
db.exec(`
CREATE TABLE IF NOT EXISTS whiteboards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_id INTEGER NOT NULL,
title TEXT NOT NULL DEFAULT 'Neues Whiteboard',
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
);
CREATE TABLE IF NOT EXISTS whiteboard_data (
whiteboard_id INTEGER PRIMARY KEY REFERENCES whiteboards(id) ON DELETE CASCADE,
elements TEXT NOT NULL DEFAULT '[]',
viewport TEXT NOT NULL DEFAULT '{"x":0,"y":0,"zoom":1}'
);
CREATE TABLE IF NOT EXISTS whiteboard_permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
whiteboard_id INTEGER NOT NULL REFERENCES whiteboards(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL,
role TEXT NOT NULL DEFAULT 'edit',
UNIQUE(whiteboard_id, user_id)
);
`);
})();
const uid = req => req.user.id;
// Hilfsfunktion: Hat User Zugriff? Gibt 'owner'|'edit'|'view'|null zurück
function access(whiteboardId, userId) {
const wb = db.prepare('SELECT owner_id FROM whiteboards WHERE id=?').get(whiteboardId);
if (!wb) return null;
if (wb.owner_id === userId) return 'owner';
const perm = db.prepare('SELECT role FROM whiteboard_permissions WHERE whiteboard_id=? AND user_id=?').get(whiteboardId, userId);
return perm?.role || null;
}
// ── Liste ─────────────────────────────────────────────────────────────────────
router.get('/', authenticate, (req, res) => {
const me = uid(req);
// Eigene + geteilte Whiteboards
const own = db.prepare(`
SELECT w.*, 'owner' as role, u.username as owner_name
FROM whiteboards w JOIN users u ON u.id=w.owner_id
WHERE w.owner_id=? ORDER BY w.updated_at DESC
`).all(me);
const shared = db.prepare(`
SELECT w.*, p.role, u.username as owner_name
FROM whiteboards w
JOIN whiteboard_permissions p ON p.whiteboard_id=w.id AND p.user_id=?
JOIN users u ON u.id=w.owner_id
ORDER BY w.updated_at DESC
`).all(me);
res.json({ whiteboards: [...own, ...shared] });
});
// ── Erstellen ─────────────────────────────────────────────────────────────────
router.post('/', authenticate, (req, res) => {
const { title = 'Neues Whiteboard' } = req.body;
const me = uid(req);
const r = db.prepare("INSERT INTO whiteboards (owner_id, title) VALUES (?,?)").run(me, title.trim() || 'Neues Whiteboard');
db.prepare("INSERT INTO whiteboard_data (whiteboard_id) VALUES (?)").run(r.lastInsertRowid);
res.json(db.prepare('SELECT * FROM whiteboards WHERE id=?').get(r.lastInsertRowid));
});
// ── Umbenennen ────────────────────────────────────────────────────────────────
router.patch('/:id/title', authenticate, (req, res) => {
const { title } = req.body;
if (!title?.trim()) return res.status(400).json({ error: 'Titel fehlt' });
const me = uid(req);
const role = access(req.params.id, me);
if (!role || role === 'view') return res.status(403).json({ error: 'Kein Zugriff' });
db.prepare("UPDATE whiteboards SET title=?, updated_at=datetime('now','localtime') WHERE id=?").run(title.trim(), req.params.id);
res.json({ ok: true });
});
// ── Löschen ───────────────────────────────────────────────────────────────────
router.delete('/:id', authenticate, (req, res) => {
const me = uid(req);
const wb = db.prepare('SELECT * FROM whiteboards WHERE id=?').get(req.params.id);
if (!wb) return res.status(404).json({ error: 'Nicht gefunden' });
if (wb.owner_id !== me) return res.status(403).json({ error: 'Nur der Ersteller kann löschen' });
db.prepare('DELETE FROM whiteboards WHERE id=?').run(wb.id);
res.json({ ok: true });
});
// ── Canvas laden ──────────────────────────────────────────────────────────────
router.get('/:id/data', authenticate, (req, res) => {
const me = uid(req);
const role = access(req.params.id, me);
if (!role) return res.status(403).json({ error: 'Kein Zugriff' });
const data = db.prepare('SELECT * FROM whiteboard_data WHERE whiteboard_id=?').get(req.params.id);
const wb = db.prepare('SELECT * FROM whiteboards WHERE id=?').get(req.params.id);
const perms = db.prepare(`
SELECT p.*, u.username FROM whiteboard_permissions p
JOIN users u ON u.id=p.user_id WHERE p.whiteboard_id=?
`).all(req.params.id);
const owner = db.prepare('SELECT username FROM users WHERE id=?').get(wb.owner_id);
res.json({ ...data, role, title: wb.title, owner: owner.username, permissions: perms });
});
// ── Canvas speichern ──────────────────────────────────────────────────────────
// Feste Route vor :id-Routen
router.post('/:id/save', authenticate, (req, res) => {
const me = uid(req);
const role = access(req.params.id, me);
if (!role || role === 'view') return res.status(403).json({ error: 'Kein Schreibzugriff' });
const { elements, viewport } = req.body;
db.prepare(`
UPDATE whiteboard_data SET elements=?, viewport=? WHERE whiteboard_id=?
`).run(JSON.stringify(elements || []), JSON.stringify(viewport || {x:0,y:0,zoom:1}), req.params.id);
db.prepare("UPDATE whiteboards SET updated_at=datetime('now','localtime') WHERE id=?").run(req.params.id);
res.json({ ok: true });
});
// ── Berechtigungen: alle User für Share-Modal ─────────────────────────────────
router.get('/users-list', authenticate, (req, res) => {
const me = uid(req);
const users = db.prepare('SELECT id, username FROM users WHERE id!=? ORDER BY username').all(me);
res.json({ users });
});
// ── Berechtigungen setzen ─────────────────────────────────────────────────────
router.put('/:id/permissions', authenticate, (req, res) => {
const me = uid(req);
const wb = db.prepare('SELECT * FROM whiteboards WHERE id=?').get(req.params.id);
if (!wb) return res.status(404).json({ error: 'Nicht gefunden' });
if (wb.owner_id !== me) return res.status(403).json({ error: 'Nur der Ersteller kann Berechtigungen vergeben' });
const { permissions } = req.body; // [{user_id, role: 'edit'|'view'|null}]
if (!Array.isArray(permissions)) return res.status(400).json({ error: 'permissions fehlt' });
const upsert = db.prepare("INSERT INTO whiteboard_permissions (whiteboard_id,user_id,role) VALUES (?,?,?) ON CONFLICT(whiteboard_id,user_id) DO UPDATE SET role=excluded.role");
const remove = db.prepare("DELETE FROM whiteboard_permissions WHERE whiteboard_id=? AND user_id=?");
const tx = db.transaction(() => {
for (const p of permissions) {
if (!p.user_id) continue;
if (p.role === null || p.role === 'none') remove.run(req.params.id, p.user_id);
else upsert.run(req.params.id, p.user_id, p.role);
}
});
tx();
const updated = db.prepare(`
SELECT p.*, u.username FROM whiteboard_permissions p
JOIN users u ON u.id=p.user_id WHERE p.whiteboard_id=?
`).all(req.params.id);
res.json({ permissions: updated });
});
module.exports = router;