Files
redlight/server/routes/rooms.js
Michelle 7426ae8088
All checks were successful
Build & Push Docker Image / build (push) Successful in 1m9s
Update language, add LICENSE and README
2026-02-24 21:04:19 +01:00

520 lines
17 KiB
JavaScript

import { Router } from 'express';
import crypto from 'crypto';
import { getDb } from '../config/database.js';
import { authenticateToken } from '../middleware/auth.js';
import {
createMeeting,
joinMeeting,
endMeeting,
getMeetingInfo,
isMeetingRunning,
} from '../config/bbb.js';
const router = Router();
// Build avatar URL for a user (uploaded image or generated initials)
function getUserAvatarURL(req, user) {
const baseUrl = `${req.protocol}://${req.get('host')}`;
if (user.avatar_image) {
return `${baseUrl}/api/auth/avatar/${user.avatar_image}`;
}
const color = user.avatar_color ? `?color=${encodeURIComponent(user.avatar_color)}` : '';
return `${baseUrl}/api/auth/avatar/initials/${encodeURIComponent(user.name)}${color}`;
}
// GET /api/rooms - List user's rooms (owned + shared)
router.get('/', authenticateToken, async (req, res) => {
try {
const db = getDb();
const ownRooms = await db.all(`
SELECT r.*, u.name as owner_name, 0 as shared
FROM rooms r
JOIN users u ON r.user_id = u.id
WHERE r.user_id = ?
ORDER BY r.created_at DESC
`, [req.user.id]);
const sharedRooms = await db.all(`
SELECT r.*, u.name as owner_name, 1 as shared
FROM rooms r
JOIN users u ON r.user_id = u.id
JOIN room_shares rs ON rs.room_id = r.id
WHERE rs.user_id = ?
ORDER BY r.created_at DESC
`, [req.user.id]);
res.json({ rooms: [...ownRooms, ...sharedRooms] });
} catch (err) {
console.error('List rooms error:', err);
res.status(500).json({ error: 'Rooms could not be loaded' });
}
});
// GET /api/rooms/users/search - Search users for sharing (must be before /:uid routes)
router.get('/users/search', authenticateToken, async (req, res) => {
try {
const { q } = req.query;
if (!q || q.length < 2) {
return res.json({ users: [] });
}
const db = getDb();
const searchTerm = `%${q}%`;
const users = await db.all(`
SELECT id, name, email, avatar_color, avatar_image
FROM users
WHERE (name LIKE ? OR email LIKE ?) AND id != ?
LIMIT 10
`, [searchTerm, searchTerm, req.user.id]);
res.json({ users });
} catch (err) {
console.error('Search users error:', err);
res.status(500).json({ error: 'User search failed' });
}
});
// GET /api/rooms/:uid - Get room details
router.get('/:uid', authenticateToken, async (req, res) => {
try {
const db = getDb();
const room = await db.get(`
SELECT r.*, u.name as owner_name
FROM rooms r
JOIN users u ON r.user_id = u.id
WHERE r.uid = ?
`, [req.params.uid]);
if (!room) {
return res.status(404).json({ error: 'Room not found' });
}
// Check access: owner, admin, or shared
if (room.user_id !== req.user.id && req.user.role !== 'admin') {
const share = await db.get('SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, req.user.id]);
if (!share) {
return res.status(403).json({ error: 'No permission' });
}
room.shared = 1;
}
// Get shared users
const sharedUsers = await db.all(`
SELECT u.id, u.name, u.email, u.avatar_color, u.avatar_image
FROM room_shares rs
JOIN users u ON rs.user_id = u.id
WHERE rs.room_id = ?
`, [room.id]);
res.json({ room, sharedUsers });
} catch (err) {
console.error('Get room error:', err);
res.status(500).json({ error: 'Room could not be loaded' });
}
});
// POST /api/rooms - Create room
router.post('/', authenticateToken, async (req, res) => {
try {
const {
name,
welcome_message,
max_participants,
access_code,
mute_on_join,
require_approval,
anyone_can_start,
all_join_moderator,
record_meeting,
guest_access,
moderator_code,
} = req.body;
if (!name || name.trim().length === 0) {
return res.status(400).json({ error: 'Room name is required' });
}
const uid = crypto.randomBytes(8).toString('hex');
const db = getDb();
const result = await db.run(`
INSERT INTO rooms (uid, name, user_id, welcome_message, max_participants, access_code, mute_on_join, require_approval, anyone_can_start, all_join_moderator, record_meeting, guest_access, moderator_code)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
uid,
name.trim(),
req.user.id,
welcome_message || 'Willkommen im Meeting!',
max_participants || 0,
access_code || null,
mute_on_join !== false ? 1 : 0,
require_approval ? 1 : 0,
anyone_can_start ? 1 : 0,
all_join_moderator ? 1 : 0,
record_meeting !== false ? 1 : 0,
guest_access ? 1 : 0,
moderator_code || null,
]);
const room = await db.get('SELECT * FROM rooms WHERE id = ?', [result.lastInsertRowid]);
res.status(201).json({ room });
} catch (err) {
console.error('Create room error:', err);
res.status(500).json({ error: 'Room could not be created' });
}
});
// PUT /api/rooms/:uid - Update room
router.put('/:uid', authenticateToken, async (req, res) => {
try {
const db = getDb();
const room = await db.get('SELECT * FROM rooms WHERE uid = ? AND user_id = ?', [req.params.uid, req.user.id]);
if (!room) {
return res.status(404).json({ error: 'Room not found or no permission' });
}
const {
name,
welcome_message,
max_participants,
access_code,
mute_on_join,
require_approval,
anyone_can_start,
all_join_moderator,
record_meeting,
guest_access,
moderator_code,
} = req.body;
await db.run(`
UPDATE rooms SET
name = COALESCE(?, name),
welcome_message = COALESCE(?, welcome_message),
max_participants = COALESCE(?, max_participants),
access_code = ?,
mute_on_join = COALESCE(?, mute_on_join),
require_approval = COALESCE(?, require_approval),
anyone_can_start = COALESCE(?, anyone_can_start),
all_join_moderator = COALESCE(?, all_join_moderator),
record_meeting = COALESCE(?, record_meeting),
guest_access = COALESCE(?, guest_access),
moderator_code = ?,
updated_at = CURRENT_TIMESTAMP
WHERE uid = ?
`, [
name,
welcome_message,
max_participants,
access_code ?? room.access_code,
mute_on_join !== undefined ? (mute_on_join ? 1 : 0) : null,
require_approval !== undefined ? (require_approval ? 1 : 0) : null,
anyone_can_start !== undefined ? (anyone_can_start ? 1 : 0) : null,
all_join_moderator !== undefined ? (all_join_moderator ? 1 : 0) : null,
record_meeting !== undefined ? (record_meeting ? 1 : 0) : null,
guest_access !== undefined ? (guest_access ? 1 : 0) : null,
moderator_code !== undefined ? (moderator_code || null) : room.moderator_code,
req.params.uid,
]);
const updated = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
res.json({ room: updated });
} catch (err) {
console.error('Update room error:', err);
res.status(500).json({ error: 'Room could not be updated' });
}
});
// DELETE /api/rooms/:uid - Delete room
router.delete('/:uid', authenticateToken, async (req, res) => {
try {
const db = getDb();
const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
if (!room) {
return res.status(404).json({ error: 'Room not found' });
}
if (room.user_id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'No permission' });
}
await db.run('DELETE FROM rooms WHERE uid = ?', [req.params.uid]);
res.json({ message: 'Room deleted successfully' });
} catch (err) {
console.error('Delete room error:', err);
res.status(500).json({ error: 'Room could not be deleted' });
}
});
// GET /api/rooms/:uid/shares - Get shared users for a room
router.get('/:uid/shares', authenticateToken, async (req, res) => {
try {
const db = getDb();
const room = await db.get('SELECT * FROM rooms WHERE uid = ? AND user_id = ?', [req.params.uid, req.user.id]);
if (!room) {
return res.status(404).json({ error: 'Room not found or no permission' });
}
const shares = await db.all(`
SELECT u.id, u.name, u.email, u.avatar_color, u.avatar_image
FROM room_shares rs
JOIN users u ON rs.user_id = u.id
WHERE rs.room_id = ?
`, [room.id]);
res.json({ shares });
} catch (err) {
console.error('Get shares error:', err);
res.status(500).json({ error: 'Error loading shares' });
}
});
// POST /api/rooms/:uid/shares - Share room with a user
router.post('/:uid/shares', authenticateToken, async (req, res) => {
try {
const { user_id } = req.body;
if (!user_id) {
return res.status(400).json({ error: 'User ID is required' });
}
const db = getDb();
const room = await db.get('SELECT * FROM rooms WHERE uid = ? AND user_id = ?', [req.params.uid, req.user.id]);
if (!room) {
return res.status(404).json({ error: 'Room not found or no permission' });
}
if (user_id === req.user.id) {
return res.status(400).json({ error: 'You cannot share the room with yourself' });
}
// Check if already shared
const existing = await db.get('SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, user_id]);
if (existing) {
return res.status(400).json({ error: 'Room is already shared with this user' });
}
await db.run('INSERT INTO room_shares (room_id, user_id) VALUES (?, ?)', [room.id, user_id]);
const shares = await db.all(`
SELECT u.id, u.name, u.email, u.avatar_color, u.avatar_image
FROM room_shares rs
JOIN users u ON rs.user_id = u.id
WHERE rs.room_id = ?
`, [room.id]);
res.json({ shares });
} catch (err) {
console.error('Share room error:', err);
res.status(500).json({ error: 'Error sharing room' });
}
});
// DELETE /api/rooms/:uid/shares/:userId - Remove share
router.delete('/:uid/shares/:userId', authenticateToken, async (req, res) => {
try {
const db = getDb();
const room = await db.get('SELECT * FROM rooms WHERE uid = ? AND user_id = ?', [req.params.uid, req.user.id]);
if (!room) {
return res.status(404).json({ error: 'Room not found or no permission' });
}
await db.run('DELETE FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, parseInt(req.params.userId)]);
const shares = await db.all(`
SELECT u.id, u.name, u.email, u.avatar_color, u.avatar_image
FROM room_shares rs
JOIN users u ON rs.user_id = u.id
WHERE rs.room_id = ?
`, [room.id]);
res.json({ shares });
} catch (err) {
console.error('Remove share error:', err);
res.status(500).json({ error: 'Error removing share' });
}
});
// POST /api/rooms/:uid/start - Start meeting
router.post('/:uid/start', authenticateToken, async (req, res) => {
try {
const db = getDb();
const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
if (!room) {
return res.status(404).json({ error: 'Room not found' });
}
// Check access: owner or shared
const isOwner = room.user_id === req.user.id;
if (!isOwner) {
const share = await db.get('SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, req.user.id]);
if (!share) {
return res.status(403).json({ error: 'No permission' });
}
}
await createMeeting(room, `${req.protocol}://${req.get('host')}`);
const avatarURL = getUserAvatarURL(req, req.user);
const joinUrl = await joinMeeting(room.uid, req.user.name, true, avatarURL);
res.json({ joinUrl });
} catch (err) {
console.error('Start meeting error:', err);
res.status(500).json({ error: 'Meeting could not be started' });
}
});
// POST /api/rooms/:uid/join - Join meeting
router.post('/:uid/join', authenticateToken, async (req, res) => {
try {
const db = getDb();
const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
if (!room) {
return res.status(404).json({ error: 'Room not found' });
}
// Check access code if set
if (room.access_code && req.body.access_code !== room.access_code) {
return res.status(403).json({ error: 'Wrong access code' });
}
// Check if meeting is running
const running = await isMeetingRunning(room.uid);
if (!running) {
return res.status(400).json({ error: 'Meeting is not running. Please wait for the moderator to start the meeting.' });
}
// Owner and shared users join as moderator
const isOwner = room.user_id === req.user.id;
const isShared = !isOwner && await db.get('SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, req.user.id]);
const isModerator = isOwner || !!isShared || room.all_join_moderator;
const avatarURL = getUserAvatarURL(req, req.user);
const joinUrl = await joinMeeting(room.uid, req.user.name, isModerator, avatarURL);
res.json({ joinUrl });
} catch (err) {
console.error('Join meeting error:', err);
res.status(500).json({ error: 'Could not join meeting' });
}
});
// POST /api/rooms/:uid/end - End meeting
router.post('/:uid/end', authenticateToken, async (req, res) => {
try {
const db = getDb();
const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
if (!room) {
return res.status(404).json({ error: 'Room not found' });
}
// Check access: owner or shared user
const isOwner = room.user_id === req.user.id;
if (!isOwner) {
const share = await db.get('SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, req.user.id]);
if (!share) {
return res.status(403).json({ error: 'No permission' });
}
}
await endMeeting(room.uid);
res.json({ message: 'Meeting ended' });
} catch (err) {
console.error('End meeting error:', err);
res.status(500).json({ error: 'Meeting could not be ended' });
}
});
// GET /api/rooms/:uid/public - Get public room info (no auth needed)
router.get('/:uid/public', async (req, res) => {
try {
const db = getDb();
const room = await db.get(`
SELECT r.uid, r.name, r.welcome_message, r.access_code,
u.name as owner_name
FROM rooms r
JOIN users u ON r.user_id = u.id
WHERE r.uid = ?
`, [req.params.uid]);
if (!room) {
return res.status(404).json({ error: 'Room not found' });
}
const running = await isMeetingRunning(room.uid);
res.json({
room: {
uid: room.uid,
name: room.name,
owner_name: room.owner_name,
welcome_message: room.welcome_message,
has_access_code: !!room.access_code,
},
running,
});
} catch (err) {
console.error('Public room info error:', err);
res.status(500).json({ error: 'Room info could not be loaded' });
}
});
// POST /api/rooms/:uid/guest-join - Join meeting as guest (no auth needed)
router.post('/:uid/guest-join', async (req, res) => {
try {
const { name, access_code, moderator_code } = req.body;
if (!name || name.trim().length === 0) {
return res.status(400).json({ error: 'Name is required' });
}
const db = getDb();
const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
if (!room) {
return res.status(404).json({ error: 'Room not found' });
}
// Check access code if set
if (room.access_code && access_code !== room.access_code) {
return res.status(403).json({ error: 'Wrong access code' });
}
// Check if meeting is running (or if anyone_can_start is enabled)
const running = await isMeetingRunning(room.uid);
if (!running && !room.anyone_can_start) {
return res.status(400).json({ error: 'Meeting is not running. Please wait for the moderator to start the meeting.' });
}
// If meeting not running but anyone_can_start, create it
if (!running && room.anyone_can_start) {
await createMeeting(room, `${req.protocol}://${req.get('host')}`);
}
// Check moderator code
let isModerator = !!room.all_join_moderator;
if (!isModerator && moderator_code && room.moderator_code && moderator_code === room.moderator_code) {
isModerator = true;
}
const baseUrl = `${req.protocol}://${req.get('host')}`;
const guestAvatarURL = `${baseUrl}/api/auth/avatar/initials/${encodeURIComponent(name.trim())}`;
const joinUrl = await joinMeeting(room.uid, name.trim(), isModerator, guestAvatarURL);
res.json({ joinUrl });
} catch (err) {
console.error('Guest join error:', err);
res.status(500).json({ error: 'Guest join failed' });
}
});
// GET /api/rooms/:uid/status - Check if meeting is running (public, no guard needed)
router.get('/:uid/status', async (req, res) => {
try {
const running = await isMeetingRunning(req.params.uid);
let info = null;
if (running) {
try {
info = await getMeetingInfo(req.params.uid);
} catch (e) {
// Meeting info might fail
}
}
res.json({
running,
participantCount: info?.participantCount || 0,
moderatorCount: info?.moderatorCount || 0,
});
} catch (err) {
res.json({ running: false, participantCount: 0, moderatorCount: 0 });
}
});
export default router;