diff --git a/server/config/database.js b/server/config/database.js
index bdac28a..6ad340c 100644
--- a/server/config/database.js
+++ b/server/config/database.js
@@ -157,9 +157,19 @@ export async function initDatabase() {
updated_at TIMESTAMP DEFAULT NOW()
);
+ CREATE TABLE IF NOT EXISTS room_shares (
+ id SERIAL PRIMARY KEY,
+ room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ created_at TIMESTAMP DEFAULT NOW(),
+ UNIQUE(room_id, user_id)
+ );
+
CREATE INDEX IF NOT EXISTS idx_rooms_user_id ON rooms(user_id);
CREATE INDEX IF NOT EXISTS idx_rooms_uid ON rooms(uid);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
+ CREATE INDEX IF NOT EXISTS idx_room_shares_room_id ON room_shares(room_id);
+ CREATE INDEX IF NOT EXISTS idx_room_shares_user_id ON room_shares(user_id);
`);
} else {
await db.exec(`
@@ -197,9 +207,21 @@ export async function initDatabase() {
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
+ CREATE TABLE IF NOT EXISTS room_shares (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ room_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE(room_id, user_id),
+ FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+ );
+
CREATE INDEX IF NOT EXISTS idx_rooms_user_id ON rooms(user_id);
CREATE INDEX IF NOT EXISTS idx_rooms_uid ON rooms(uid);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
+ CREATE INDEX IF NOT EXISTS idx_room_shares_room_id ON room_shares(room_id);
+ CREATE INDEX IF NOT EXISTS idx_room_shares_user_id ON room_shares(user_id);
`);
}
diff --git a/server/routes/rooms.js b/server/routes/rooms.js
index a812783..ecffd65 100644
--- a/server/routes/rooms.js
+++ b/server/routes/rooms.js
@@ -22,25 +22,56 @@ function getUserAvatarURL(req, user) {
return `${baseUrl}/api/auth/avatar/initials/${encodeURIComponent(user.name)}${color}`;
}
-// GET /api/rooms - List user's rooms
+// GET /api/rooms - List user's rooms (owned + shared)
router.get('/', authenticateToken, async (req, res) => {
try {
const db = getDb();
- const rooms = await db.all(`
- SELECT r.*, u.name as owner_name
+ 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]);
- res.json({ rooms });
+ 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: 'Räume konnten nicht geladen werden' });
}
});
+// 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: 'Benutzersuche fehlgeschlagen' });
+ }
+});
+
// GET /api/rooms/:uid - Get room details
router.get('/:uid', authenticateToken, async (req, res) => {
try {
@@ -56,7 +87,24 @@ router.get('/:uid', authenticateToken, async (req, res) => {
return res.status(404).json({ error: 'Raum nicht gefunden' });
}
- res.json({ room });
+ // 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: 'Keine Berechtigung' });
+ }
+ 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: 'Raum konnte nicht geladen werden' });
@@ -197,15 +245,100 @@ router.delete('/:uid', authenticateToken, async (req, res) => {
res.status(500).json({ error: 'Raum konnte nicht gelöscht werden' });
}
});
+// 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: 'Raum nicht gefunden oder keine Berechtigung' });
+ }
+ 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: 'Fehler beim Laden der Freigaben' });
+ }
+});
+
+// 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: 'Benutzer-ID erforderlich' });
+ }
+ 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: 'Raum nicht gefunden oder keine Berechtigung' });
+ }
+ if (user_id === req.user.id) {
+ return res.status(400).json({ error: 'Du kannst den Raum nicht mit dir selbst teilen' });
+ }
+ // 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: 'Raum ist bereits mit diesem Benutzer geteilt' });
+ }
+ 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: 'Fehler beim Teilen des Raums' });
+ }
+});
+
+// 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: 'Raum nicht gefunden oder keine Berechtigung' });
+ }
+ 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: 'Fehler beim Entfernen der Freigabe' });
+ }
+});
// 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 = ? AND user_id = ?', [req.params.uid, req.user.id]);
+ const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
if (!room) {
- return res.status(404).json({ error: 'Raum nicht gefunden oder keine Berechtigung' });
+ return res.status(404).json({ error: 'Raum nicht gefunden' });
+ }
+
+ // 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: 'Keine Berechtigung' });
+ }
}
await createMeeting(room, `${req.protocol}://${req.get('host')}`);
@@ -239,7 +372,10 @@ router.post('/:uid/join', authenticateToken, async (req, res) => {
return res.status(400).json({ error: 'Meeting läuft nicht. Bitte warten Sie, bis der Moderator das Meeting gestartet hat.' });
}
- const isModerator = room.user_id === req.user.id || room.all_join_moderator;
+ // 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 });
@@ -253,10 +389,19 @@ router.post('/:uid/join', authenticateToken, async (req, res) => {
router.post('/:uid/end', 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]);
+ const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
if (!room) {
- return res.status(404).json({ error: 'Raum nicht gefunden oder keine Berechtigung' });
+ return res.status(404).json({ error: 'Raum nicht gefunden' });
+ }
+
+ // 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: 'Keine Berechtigung' });
+ }
}
await endMeeting(room.uid);
diff --git a/src/components/RoomCard.jsx b/src/components/RoomCard.jsx
index c3bd3cd..1f783cc 100644
--- a/src/components/RoomCard.jsx
+++ b/src/components/RoomCard.jsx
@@ -1,4 +1,4 @@
-import { Users, Play, Trash2, Radio, Loader2 } from 'lucide-react';
+import { Users, Play, Trash2, Radio, Loader2, Share2 } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useState, useEffect } from 'react';
import api from '../services/api';
@@ -39,9 +39,15 @@ export default function RoomCard({ room, onDelete }) {
{t('common.live')}
)}
+ {room.shared ? (
+
+
- {room.uid.substring(0, 8)}... + {room.shared ? room.owner_name : `${room.uid.substring(0, 8)}...`}
@@ -93,7 +99,7 @@ export default function RoomCard({ room, onDelete }) { {starting ?{t('room.shareDescription')}
+ + {/* User search */} +