Whiteboard: neues Tool mit Canvas, Undo, Kollaboration via WebSocket, Berechtigungssystem
This commit is contained in:
153
backend/src/tools/whiteboard/routes.js
Normal file
153
backend/src/tools/whiteboard/routes.js
Normal 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;
|
||||
Reference in New Issue
Block a user