Whiteboard: neues Tool mit Canvas, Undo, Kollaboration via WebSocket, Berechtigungssystem
This commit is contained in:
@@ -33,7 +33,7 @@ app.use((_req, res, next) => {
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
||||
"font-src 'self' https://fonts.gstatic.com",
|
||||
// iCal-Feeds werden serverseitig geprosxt → nur 'self' nötig
|
||||
"connect-src 'self' https://web.archive.org",
|
||||
"connect-src 'self' https://web.archive.org wss://dickendock.sermer.org ws://localhost:4000",
|
||||
// data: für Avatare (base64), google.com + gstatic.com für Favicons im QuickLinks-Widget
|
||||
"img-src 'self' data: https://www.google.com https://*.gstatic.com https://image.tmdb.org",
|
||||
"worker-src 'self'",
|
||||
@@ -127,7 +127,104 @@ app.get('/api/build-time', (_req, res) => {
|
||||
} catch { res.json({ buildTime: 'unknown' }); }
|
||||
});
|
||||
|
||||
app.listen(4000, () => console.log('🚀 Dicken Dock läuft auf Port 4000'));
|
||||
// ── HTTP + WebSocket Server ───────────────────────────────────────────────────
|
||||
const http = require('http');
|
||||
const { WebSocketServer } = require('ws');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const server = http.createServer(app);
|
||||
const wss = new WebSocketServer({ server, path: '/ws/whiteboard' });
|
||||
|
||||
// Raum-Map: whiteboard_id → Set<ws>
|
||||
const rooms = new Map();
|
||||
|
||||
wss.on('connection', (ws, req) => {
|
||||
// Token aus Query-String auslesen: /ws/whiteboard?token=...&id=...
|
||||
const params = new URL(req.url, 'http://localhost').searchParams;
|
||||
const token = params.get('token');
|
||||
const wbId = parseInt(params.get('id'));
|
||||
|
||||
let userId = null;
|
||||
try {
|
||||
const p = jwt.verify(token, process.env.JWT_SECRET || 'dev-secret');
|
||||
userId = p.id;
|
||||
} catch {
|
||||
ws.close(1008, 'Unauthorized');
|
||||
return;
|
||||
}
|
||||
|
||||
// Zugriff prüfen
|
||||
const wb = db.prepare('SELECT owner_id FROM whiteboards WHERE id=?').get(wbId);
|
||||
if (!wb) { ws.close(1008, 'Not found'); return; }
|
||||
const isOwner = wb.owner_id === userId;
|
||||
const perm = db.prepare('SELECT role FROM whiteboard_permissions WHERE whiteboard_id=? AND user_id=?').get(wbId, userId);
|
||||
if (!isOwner && !perm) { ws.close(1008, 'Forbidden'); return; }
|
||||
const role = isOwner ? 'owner' : perm.role;
|
||||
|
||||
ws.userId = userId;
|
||||
ws.wbId = wbId;
|
||||
ws.role = role;
|
||||
ws.username = db.prepare('SELECT username FROM users WHERE id=?').get(userId)?.username || 'Unbekannt';
|
||||
|
||||
// Raum beitreten
|
||||
if (!rooms.has(wbId)) rooms.set(wbId, new Set());
|
||||
rooms.get(wbId).add(ws);
|
||||
|
||||
// Anderen im Raum mitteilen wer jointe
|
||||
broadcast(wbId, { type: 'user_join', userId, username: ws.username, role }, ws);
|
||||
|
||||
// Aktive User-Liste an neuen Client schicken
|
||||
const activeUsers = [...rooms.get(wbId)]
|
||||
.filter(c => c !== ws && c.readyState === 1)
|
||||
.map(c => ({ userId: c.userId, username: c.username, role: c.role }));
|
||||
ws.send(JSON.stringify({ type: 'active_users', users: activeUsers }));
|
||||
|
||||
ws.on('message', raw => {
|
||||
let msg;
|
||||
try { msg = JSON.parse(raw); } catch { return; }
|
||||
|
||||
switch (msg.type) {
|
||||
case 'cursor':
|
||||
// Cursor-Position live broadcasten (nur wenn nicht view-only)
|
||||
broadcast(wbId, { type:'cursor', userId, username:ws.username, x:msg.x, y:msg.y }, ws);
|
||||
break;
|
||||
|
||||
case 'elements':
|
||||
// Canvas-Änderungen von Edit-Berechtigten an alle broadcasten
|
||||
if (role === 'view') break;
|
||||
broadcast(wbId, { type:'elements', userId, elements: msg.elements }, ws);
|
||||
break;
|
||||
|
||||
case 'ping':
|
||||
ws.send(JSON.stringify({ type: 'pong' }));
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
const room = rooms.get(wbId);
|
||||
if (room) {
|
||||
room.delete(ws);
|
||||
if (room.size === 0) rooms.delete(wbId);
|
||||
else broadcast(wbId, { type: 'user_leave', userId, username: ws.username });
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', () => ws.terminate());
|
||||
});
|
||||
|
||||
function broadcast(wbId, msg, exclude = null) {
|
||||
const room = rooms.get(wbId);
|
||||
if (!room) return;
|
||||
const data = JSON.stringify(msg);
|
||||
for (const client of room) {
|
||||
if (client !== exclude && client.readyState === 1) {
|
||||
client.send(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server.listen(4000, () => console.log('🚀 Dicken Dock läuft auf Port 4000'));
|
||||
|
||||
// ── Push-Zeitplaner Hintergrund-Job ──────────────────────────────────────────
|
||||
const db = require('./db');
|
||||
|
||||
Reference in New Issue
Block a user