diff --git a/server/config/database.js b/server/config/database.js index 777eba0..e318f22 100644 --- a/server/config/database.js +++ b/server/config/database.js @@ -201,6 +201,9 @@ export async function initDatabase() { room_name TEXT NOT NULL, from_user TEXT NOT NULL, join_url TEXT NOT NULL, + meet_id TEXT, + max_participants INTEGER DEFAULT 0, + allow_recording INTEGER DEFAULT 1, created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_fed_rooms_user_id ON federated_rooms(user_id); @@ -289,6 +292,9 @@ export async function initDatabase() { room_name TEXT NOT NULL, from_user TEXT NOT NULL, join_url TEXT NOT NULL, + meet_id TEXT, + max_participants INTEGER DEFAULT 0, + allow_recording INTEGER DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); @@ -319,6 +325,24 @@ export async function initDatabase() { await db.exec('ALTER TABLE users ADD COLUMN verification_token_expires DATETIME'); } } + if (!(await db.columnExists('federated_rooms', 'meet_id'))) { + await db.exec('ALTER TABLE federated_rooms ADD COLUMN meet_id TEXT'); + } + if (!(await db.columnExists('federated_rooms', 'max_participants'))) { + await db.exec('ALTER TABLE federated_rooms ADD COLUMN max_participants INTEGER DEFAULT 0'); + } + if (!(await db.columnExists('federated_rooms', 'allow_recording'))) { + await db.exec('ALTER TABLE federated_rooms ADD COLUMN allow_recording INTEGER DEFAULT 1'); + } + if (!(await db.columnExists('federation_invitations', 'room_uid'))) { + await db.exec('ALTER TABLE federation_invitations ADD COLUMN room_uid TEXT'); + } + if (!(await db.columnExists('federation_invitations', 'max_participants'))) { + await db.exec('ALTER TABLE federation_invitations ADD COLUMN max_participants INTEGER DEFAULT 0'); + } + if (!(await db.columnExists('federation_invitations', 'allow_recording'))) { + await db.exec('ALTER TABLE federation_invitations ADD COLUMN allow_recording INTEGER DEFAULT 1'); + } // ── Default admin ─────────────────────────────────────────────────────── const adminEmail = process.env.ADMIN_EMAIL || 'admin@example.com'; diff --git a/server/routes/federation.js b/server/routes/federation.js index 13f2fec..ad71853 100644 --- a/server/routes/federation.js +++ b/server/routes/federation.js @@ -80,6 +80,9 @@ router.post('/invite', authenticateToken, async (req, res) => { from_user: `${req.user.name}@${getFederationDomain()}`, to_user: to, room_name: room.name, + room_uid: room.uid, + max_participants: room.max_participants ?? 0, + allow_recording: room.record_meeting ?? 1, message: message || null, join_url: joinUrl, timestamp: new Date().toISOString(), @@ -126,7 +129,7 @@ router.post('/receive', async (req, res) => { } // Extract expected fields from the incoming payload - const { invite_id, from_user, to_user, room_name, message, join_url } = payload; + const { invite_id, from_user, to_user, room_name, room_uid, max_participants, allow_recording, message, join_url } = payload; if (!invite_id || !from_user || !to_user || !room_name || !join_url) { return res.status(400).json({ error: 'Incomplete invitation payload' }); @@ -177,6 +180,18 @@ router.post('/receive', async (req, res) => { [invite_id, from_user, targetUser.id, room_name, message || null, join_url] ); + // Store room_uid, max_participants, allow_recording if those columns already exist + // (we update after initial insert to stay compatible with old schema) + const inv = await db.get('SELECT id FROM federation_invitations WHERE invite_id = ? AND to_user_id = ?', [invite_id, targetUser.id]); + if (inv && room_uid !== undefined) { + try { + await db.run( + 'UPDATE federation_invitations SET room_uid = ?, max_participants = ?, allow_recording = ? WHERE id = ?', + [room_uid || null, max_participants ?? 0, allow_recording ?? 1, inv.id] + ); + } catch { /* column may not exist on very old installs */ } + } + // Send notification email (fire-and-forget, don't fail the request if mail fails) try { const appUrl = process.env.APP_URL || ''; @@ -254,9 +269,15 @@ router.post('/invitations/:id/accept', authenticateToken, async (req, res) => { ); if (!existing) { await db.run( - `INSERT INTO federated_rooms (user_id, invite_id, room_name, from_user, join_url) - VALUES (?, ?, ?, ?, ?)`, - [req.user.id, invitation.invite_id, invitation.room_name, invitation.from_user, invitation.join_url] + `INSERT INTO federated_rooms (user_id, invite_id, room_name, from_user, join_url, meet_id, max_participants, allow_recording) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [ + req.user.id, invitation.invite_id, invitation.room_name, + invitation.from_user, invitation.join_url, + invitation.room_uid || null, + invitation.max_participants ?? 0, + invitation.allow_recording ?? 1, + ] ); } diff --git a/server/routes/rooms.js b/server/routes/rooms.js index 7891150..06e8469 100644 --- a/server/routes/rooms.js +++ b/server/routes/rooms.js @@ -419,7 +419,7 @@ 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, + SELECT r.uid, r.name, r.welcome_message, r.access_code, r.record_meeting, r.max_participants, r.anyone_can_start, u.name as owner_name FROM rooms r JOIN users u ON r.user_id = u.id @@ -439,6 +439,9 @@ router.get('/:uid/public', async (req, res) => { owner_name: room.owner_name, welcome_message: room.welcome_message, has_access_code: !!room.access_code, + allow_recording: !!room.record_meeting, + max_participants: room.max_participants ?? 0, + anyone_can_start: !!room.anyone_can_start, }, running, }); diff --git a/src/components/FederatedRoomCard.jsx b/src/components/FederatedRoomCard.jsx index f94a814..e33c025 100644 --- a/src/components/FederatedRoomCard.jsx +++ b/src/components/FederatedRoomCard.jsx @@ -1,4 +1,4 @@ -import { Globe, Play, Trash2, ExternalLink } from 'lucide-react'; +import { Globe, Trash2, ExternalLink, Hash, Users, Video, VideoOff } from 'lucide-react'; import { useLanguage } from '../contexts/LanguageContext'; import api from '../services/api'; import toast from 'react-hot-toast'; @@ -22,6 +22,8 @@ export default function FederatedRoomCard({ room, onRemove }) { } }; + const recordingOn = room.allow_recording === 1 || room.allow_recording === true; + return (
{t('federation.readOnlyNotice')}
diff --git a/src/i18n/de.json b/src/i18n/de.json index 5160bd9..9c317b1 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -201,6 +201,8 @@ "guestHasAccount": "Haben Sie ein Konto?", "guestSignIn": "Anmelden", "guestRoomNotFound": "Raum nicht gefunden", + "guestRecordingNotice": "Dieses Meeting könnte aufgenommen werden, inkl. Ihrer Audio / Video.", + "guestRecordingConsent": "Ich bin damit einverstanden, dass dieses Meeting aufgenommen werden kann.", "shared": "Geteilt", "shareTitle": "Raum teilen", "shareDescription": "Teilen Sie diesen Raum mit anderen Benutzern, damit diese ihn in ihrem Dashboard sehen und beitreten k\u00f6nnen.", @@ -342,6 +344,11 @@ "removeRoomConfirm": "Raum wirklich entfernen?", "roomRemoved": "Raum entfernt", "roomRemoveFailed": "Raum konnte nicht entfernt werden", - "acceptedSaved": "Einladung angenommen – Raum wurde in deinem Dashboard gespeichert!" + "acceptedSaved": "Einladung angenommen – Raum wurde in deinem Dashboard gespeichert!", + "meetingId": "Meeting ID", + "maxParticipants": "Max. Teilnehmer", + "recordingOn": "Aufnahme aktiviert", + "recordingOff": "Aufnahme deaktiviert", + "unlimited": "Unbegrenzt" } } \ No newline at end of file diff --git a/src/i18n/en.json b/src/i18n/en.json index 9a7e773..1c9b4f6 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -201,6 +201,8 @@ "guestHasAccount": "Have an account?", "guestSignIn": "Sign in", "guestRoomNotFound": "Room not found", + "guestRecordingNotice": "This meeting may be recorded, including your audio and video.", + "guestRecordingConsent": "I understand that this meeting may be recorded.", "shared": "Shared", "shareTitle": "Share room", "shareDescription": "Share this room with other users so they can see it in their dashboard and join meetings.", @@ -342,6 +344,11 @@ "removeRoomConfirm": "Really remove this room?", "roomRemoved": "Room removed", "roomRemoveFailed": "Could not remove room", - "acceptedSaved": "Invitation accepted – room saved to your dashboard!" + "acceptedSaved": "Invitation accepted – room saved to your dashboard!", + "meetingId": "Meeting ID", + "maxParticipants": "Max. participants", + "recordingOn": "Recording enabled", + "recordingOff": "Recording disabled", + "unlimited": "Unlimited" } } \ No newline at end of file diff --git a/src/pages/GuestJoin.jsx b/src/pages/GuestJoin.jsx index 6ae814b..8d3a401 100644 --- a/src/pages/GuestJoin.jsx +++ b/src/pages/GuestJoin.jsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; -import { Video, User, Lock, Shield, ArrowRight, Loader2, Users, Radio } from 'lucide-react'; +import { Video, User, Lock, Shield, ArrowRight, Loader2, Users, Radio, AlertCircle } from 'lucide-react'; import BrandLogo from '../components/BrandLogo'; import api from '../services/api'; import toast from 'react-hot-toast'; @@ -20,6 +20,7 @@ export default function GuestJoin() { const [accessCode, setAccessCode] = useState(''); const [moderatorCode, setModeratorCode] = useState(''); const [status, setStatus] = useState({ running: false }); + const [recordingConsent, setRecordingConsent] = useState(false); useEffect(() => { const fetchRoom = async () => { @@ -61,6 +62,11 @@ export default function GuestJoin() { return; } + if (roomInfo?.allow_recording && !recordingConsent) { + toast.error(t('room.guestRecordingConsent')); + return; + } + setJoining(true); try { const res = await api.post(`/rooms/${uid}/guest-join`, { @@ -206,9 +212,28 @@ export default function GuestJoin() { + {/* Recording consent notice */} + {roomInfo.allow_recording && ( +{t('room.guestRecordingNotice')}
+