feat(logging): implement centralized logging system and replace console errors with structured logs
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
Build & Push Docker Image / build (release) Successful in 7m27s

feat(federation): add room sync and deletion notification endpoints for federated instances

fix(federation): handle room deletion and update settings during sync process

feat(federation): enhance FederatedRoomCard and FederatedRoomDetail components to display deleted rooms

i18n: add translations for room deletion messages in English and German
This commit is contained in:
2026-03-01 12:20:14 +01:00
parent 89b2a853d3
commit 57bb1fb696
22 changed files with 674 additions and 269 deletions

View File

@@ -2,6 +2,7 @@ import { Router } from 'express';
import bcrypt from 'bcryptjs';
import { getDb } from '../config/database.js';
import { authenticateToken, requireAdmin } from '../middleware/auth.js';
import { log } from '../config/logger.js';
const EMAIL_RE = /^[^\s@]{1,64}@[^\s@]{1,253}\.[^\s@]{2,}$/;
@@ -57,7 +58,7 @@ router.post('/users', authenticateToken, requireAdmin, async (req, res) => {
const user = await db.get('SELECT id, name, display_name, email, role, created_at FROM users WHERE id = ?', [result.lastInsertRowid]);
res.status(201).json({ user });
} catch (err) {
console.error('Create user error:', err);
log.admin.error(`Create user error: ${err.message}`);
res.status(500).json({ error: 'User could not be created' });
}
});
@@ -75,7 +76,7 @@ router.get('/users', authenticateToken, requireAdmin, async (req, res) => {
res.json({ users });
} catch (err) {
console.error('List users error:', err);
log.admin.error(`List users error: ${err.message}`);
res.status(500).json({ error: 'Users could not be loaded' });
}
});
@@ -109,7 +110,7 @@ router.put('/users/:id/role', authenticateToken, requireAdmin, async (req, res)
res.json({ user: updated });
} catch (err) {
console.error('Update role error:', err);
log.admin.error(`Update role error: ${err.message}`);
res.status(500).json({ error: 'Role could not be updated' });
}
});
@@ -139,7 +140,7 @@ router.delete('/users/:id', authenticateToken, requireAdmin, async (req, res) =>
await db.run('DELETE FROM users WHERE id = ?', [req.params.id]);
res.json({ message: 'User deleted' });
} catch (err) {
console.error('Delete user error:', err);
log.admin.error(`Delete user error: ${err.message}`);
res.status(500).json({ error: 'User could not be deleted' });
}
});
@@ -158,7 +159,7 @@ router.put('/users/:id/password', authenticateToken, requireAdmin, async (req, r
res.json({ message: 'Password reset' });
} catch (err) {
console.error('Reset password error:', err);
log.admin.error(`Reset password error: ${err.message}`);
res.status(500).json({ error: 'Password could not be reset' });
}
});

View File

@@ -11,9 +11,10 @@ import { getDb } from '../config/database.js';
import redis from '../config/redis.js';
import { authenticateToken, generateToken } from '../middleware/auth.js';
import { isMailerConfigured, sendVerificationEmail } from '../config/mailer.js';
import { log } from '../config/logger.js';
if (!process.env.JWT_SECRET) {
console.error('FATAL: JWT_SECRET environment variable is not set.');
log.auth.error('FATAL: JWT_SECRET environment variable is not set.');
process.exit(1);
}
const JWT_SECRET = process.env.JWT_SECRET;
@@ -174,7 +175,7 @@ router.post('/register', registerLimiter, async (req, res) => {
try {
await sendVerificationEmail(email.toLowerCase(), display_name, verifyUrl, appName);
} catch (mailErr) {
console.error('Verification mail failed:', mailErr.message);
log.auth.error(`Verification mail failed: ${mailErr.message}`);
// Account is created but email failed — user can resend from login page
return res.status(201).json({ needsVerification: true, emailFailed: true, message: 'Account created but verification email could not be sent. Please try resending.' });
}
@@ -193,7 +194,7 @@ router.post('/register', registerLimiter, async (req, res) => {
res.status(201).json({ token, user });
} catch (err) {
console.error('Register error:', err);
log.auth.error(`Register error: ${err.message}`);
res.status(500).json({ error: 'Registration failed' });
}
});
@@ -227,7 +228,7 @@ router.get('/verify-email', async (req, res) => {
res.json({ verified: true, message: 'Email verified successfully' });
} catch (err) {
console.error('Verify email error:', err);
log.auth.error(`Verify email error: ${err.message}`);
res.status(500).json({ error: 'Verification failed' });
}
});
@@ -282,13 +283,13 @@ router.post('/resend-verification', resendVerificationLimiter, async (req, res)
try {
await sendVerificationEmail(email.toLowerCase(), user.display_name || user.name, verifyUrl, appName);
} catch (mailErr) {
console.error('Resend verification mail failed:', mailErr.message);
log.auth.error(`Resend verification mail failed: ${mailErr.message}`);
return res.status(502).json({ error: 'Email could not be sent. Please check your SMTP configuration.' });
}
res.json({ message: 'If an account exists, a new email has been sent.' });
} catch (err) {
console.error('Resend verification error:', err);
log.auth.error(`Resend verification error: ${err.message}`);
res.status(500).json({ error: 'Internal server error' });
}
});
@@ -323,7 +324,7 @@ router.post('/login', loginLimiter, async (req, res) => {
res.json({ token, user: safeUser });
} catch (err) {
console.error('Login error:', err);
log.auth.error(`Login error: ${err.message}`);
res.status(500).json({ error: 'Login failed' });
}
});
@@ -341,14 +342,14 @@ router.post('/logout', authenticateToken, async (req, res) => {
try {
await redis.setex(`blacklist:${decoded.jti}`, ttl, '1');
} catch (redisErr) {
console.warn('Redis blacklist write failed:', redisErr.message);
log.auth.warn(`Redis blacklist write failed: ${redisErr.message}`);
}
}
}
res.json({ message: 'Logged out successfully' });
} catch (err) {
console.error('Logout error:', err);
log.auth.error(`Logout error: ${err.message}`);
res.status(500).json({ error: 'Logout failed' });
}
});
@@ -422,7 +423,7 @@ router.put('/profile', authenticateToken, profileLimiter, async (req, res) => {
const updated = await db.get('SELECT id, name, display_name, email, role, theme, language, avatar_color, avatar_image, email_verified FROM users WHERE id = ?', [req.user.id]);
res.json({ user: updated });
} catch (err) {
console.error('Profile update error:', err);
log.auth.error(`Profile update error: ${err.message}`);
res.status(500).json({ error: 'Profile could not be updated' });
}
});
@@ -457,7 +458,7 @@ router.put('/password', authenticateToken, passwordLimiter, async (req, res) =>
res.json({ message: 'Password changed successfully' });
} catch (err) {
console.error('Password change error:', err);
log.auth.error(`Password change error: ${err.message}`);
res.status(500).json({ error: 'Password could not be changed' });
}
});
@@ -514,7 +515,7 @@ router.post('/avatar', authenticateToken, avatarLimiter, async (req, res) => {
const updated = await db.get('SELECT id, name, display_name, email, role, theme, language, avatar_color, avatar_image, email_verified FROM users WHERE id = ?', [req.user.id]);
res.json({ user: updated });
} catch (err) {
console.error('Avatar upload error:', err);
log.auth.error(`Avatar upload error: ${err.message}`);
res.status(500).json({ error: 'Avatar could not be uploaded' });
}
});
@@ -533,7 +534,7 @@ router.delete('/avatar', authenticateToken, avatarLimiter, async (req, res) => {
const updated = await db.get('SELECT id, name, display_name, email, role, theme, language, avatar_color, avatar_image, email_verified FROM users WHERE id = ?', [req.user.id]);
res.json({ user: updated });
} catch (err) {
console.error('Avatar delete error:', err);
log.auth.error(`Avatar delete error: ${err.message}`);
res.status(500).json({ error: 'Avatar could not be removed' });
}
});

View File

@@ -5,6 +5,7 @@ import fs from 'fs';
import { fileURLToPath } from 'url';
import { getDb } from '../config/database.js';
import { authenticateToken, requireAdmin } from '../middleware/auth.js';
import { log } from '../config/logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -88,7 +89,7 @@ router.get('/', async (req, res) => {
defaultTheme: defaultTheme || null,
});
} catch (err) {
console.error('Get branding error:', err);
log.branding.error('Get branding error:', err);
res.status(500).json({ error: 'Could not load branding' });
}
});
@@ -149,7 +150,7 @@ router.delete('/logo', authenticateToken, requireAdmin, async (req, res) => {
}
res.json({ message: 'Logo removed' });
} catch (err) {
console.error('Delete logo error:', err);
log.branding.error('Delete logo error:', err);
res.status(500).json({ error: 'Could not remove logo' });
}
});
@@ -167,7 +168,7 @@ router.put('/name', authenticateToken, requireAdmin, async (req, res) => {
await setSetting('app_name', appName.trim());
res.json({ appName: appName.trim() });
} catch (err) {
console.error('Update app name error:', err);
log.branding.error('Update app name error:', err);
res.status(500).json({ error: 'Could not update app name' });
}
});
@@ -186,7 +187,7 @@ router.put('/default-theme', authenticateToken, requireAdmin, async (req, res) =
await setSetting('default_theme', defaultTheme.trim());
res.json({ defaultTheme: defaultTheme.trim() });
} catch (err) {
console.error('Update default theme error:', err);
log.branding.error('Update default theme error:', err);
res.status(500).json({ error: 'Could not update default theme' });
}
});

View File

@@ -4,6 +4,7 @@ import { rateLimit } from 'express-rate-limit';
import { getDb } from '../config/database.js';
import { authenticateToken } from '../middleware/auth.js';
import { sendFederationInviteEmail } from '../config/mailer.js';
import { log } from '../config/logger.js';
// M13: rate limit the unauthenticated federation receive endpoint
const federationReceiveLimiter = rateLimit({
@@ -38,7 +39,7 @@ export function wellKnownHandler(req, res) {
federation_api: '/api/federation',
public_key: getPublicKey(),
software: 'Redlight',
version: '1.2.0',
version: '1.2.1',
});
}
@@ -119,9 +120,18 @@ router.post('/invite', authenticateToken, async (req, res) => {
throw new Error(data.error || `Remote server responded with ${response.status}`);
}
// Track outbound invite for deletion propagation
try {
await db.run(
`INSERT INTO federation_outbound_invites (room_uid, remote_domain) VALUES (?, ?)
ON CONFLICT(room_uid, remote_domain) DO NOTHING`,
[room.uid, domain]
);
} catch { /* table may not exist yet on upgrade */ }
res.json({ success: true, invite_id: inviteId });
} catch (err) {
console.error('Federation invite error:', err);
log.federation.error('Federation invite error:', err);
res.status(500).json({ error: err.message || 'Failed to send federation invite' });
}
});
@@ -219,13 +229,13 @@ router.post('/receive', federationReceiveLimiter, async (req, res) => {
targetUser.email, targetUser.name, from_user,
room_name, message || null, inboxUrl, appName
).catch(mailErr => {
console.warn('Federation invite mail failed (non-fatal):', mailErr.message);
log.federation.warn('Federation invite mail failed (non-fatal):', mailErr.message);
});
}
res.json({ success: true });
} catch (err) {
console.error('Federation receive error:', err);
log.federation.error('Federation receive error:', err);
res.status(500).json({ error: 'Failed to process federation invitation' });
}
});
@@ -242,7 +252,7 @@ router.get('/invitations', authenticateToken, async (req, res) => {
);
res.json({ invitations });
} catch (err) {
console.error('List federation invitations error:', err);
log.federation.error('List invitations error:', err);
res.status(500).json({ error: 'Failed to load invitations' });
}
});
@@ -301,7 +311,7 @@ router.post('/invitations/:id/accept', authenticateToken, async (req, res) => {
res.json({ success: true, join_url: invitation.join_url });
} catch (err) {
console.error('Accept invitation error:', err);
log.federation.error('Accept invitation error:', err);
res.status(500).json({ error: 'Failed to accept invitation' });
}
});
@@ -323,7 +333,7 @@ router.delete('/invitations/:id', authenticateToken, async (req, res) => {
res.json({ success: true });
} catch (err) {
console.error('Decline invitation error:', err);
log.federation.error('Decline invitation error:', err);
res.status(500).json({ error: 'Failed to decline invitation' });
}
});
@@ -338,7 +348,7 @@ router.get('/federated-rooms', authenticateToken, async (req, res) => {
);
res.json({ rooms });
} catch (err) {
console.error('List federated rooms error:', err);
log.federation.error('List federated rooms error:', err);
res.status(500).json({ error: 'Failed to load federated rooms' });
}
});
@@ -355,9 +365,104 @@ router.delete('/federated-rooms/:id', authenticateToken, async (req, res) => {
await db.run('DELETE FROM federated_rooms WHERE id = ?', [room.id]);
res.json({ success: true });
} catch (err) {
console.error('Delete federated room error:', err);
log.federation.error('Delete federated room error:', err);
res.status(500).json({ error: 'Failed to remove room' });
}
});
// ── POST /api/federation/room-sync — Remote instances query room settings ───
// Called by federated instances to pull current room info for one or more UIDs.
// Signed request from remote, no auth token needed.
router.post('/room-sync', federationReceiveLimiter, async (req, res) => {
try {
if (!isFederationEnabled()) {
return res.status(400).json({ error: 'Federation is not configured on this instance' });
}
const signature = req.headers['x-federation-signature'];
const originDomain = req.headers['x-federation-origin'];
const payload = req.body || {};
if (!signature || !originDomain) {
return res.status(401).json({ error: 'Missing federation signature or origin' });
}
// Verify signature using the remote instance's public key
const { publicKey } = await discoverInstance(originDomain);
if (!publicKey || !verifyPayload(payload, signature, publicKey)) {
return res.status(403).json({ error: 'Invalid federation signature' });
}
const { room_uids } = payload;
if (!Array.isArray(room_uids) || room_uids.length === 0 || room_uids.length > 100) {
return res.status(400).json({ error: 'room_uids must be an array of 1-100 UIDs' });
}
const db = getDb();
const result = {};
for (const uid of room_uids) {
if (typeof uid !== 'string' || uid.length > 100) continue;
const room = await db.get('SELECT uid, name, max_participants, record_meeting FROM rooms WHERE uid = ?', [uid]);
if (room) {
result[uid] = {
room_name: room.name,
max_participants: room.max_participants ?? 0,
allow_recording: room.record_meeting ?? 1,
deleted: false,
};
} else {
result[uid] = { deleted: true };
}
}
res.json({ rooms: result });
} catch (err) {
log.federation.error('Room-sync error:', err);
res.status(500).json({ error: 'Failed to process room sync request' });
}
});
// ── POST /api/federation/room-deleted — Receive deletion notification ───────
// Origin instance pushes this to notify that a room has been deleted.
router.post('/room-deleted', federationReceiveLimiter, async (req, res) => {
try {
if (!isFederationEnabled()) {
return res.status(400).json({ error: 'Federation is not configured on this instance' });
}
const signature = req.headers['x-federation-signature'];
const originDomain = req.headers['x-federation-origin'];
const payload = req.body || {};
if (!signature || !originDomain) {
return res.status(401).json({ error: 'Missing federation signature or origin' });
}
const { publicKey } = await discoverInstance(originDomain);
if (!publicKey || !verifyPayload(payload, signature, publicKey)) {
return res.status(403).json({ error: 'Invalid federation signature' });
}
const { room_uid } = payload;
if (!room_uid || typeof room_uid !== 'string') {
return res.status(400).json({ error: 'room_uid is required' });
}
const db = getDb();
// Mark all federated_rooms with this meet_id (room_uid) from this origin as deleted
await db.run(
`UPDATE federated_rooms SET deleted = 1, updated_at = CURRENT_TIMESTAMP
WHERE meet_id = ? AND from_user LIKE ?`,
[room_uid, `%@${originDomain}`]
);
log.federation.info(`Room ${room_uid} marked as deleted (origin: ${originDomain})`);
res.json({ success: true });
} catch (err) {
log.federation.error('Room-deleted error:', err);
res.status(500).json({ error: 'Failed to process deletion notification' });
}
});
export default router;

View File

@@ -1,6 +1,7 @@
import { Router } from 'express';
import { authenticateToken } from '../middleware/auth.js';
import { getDb } from '../config/database.js';
import { log } from '../config/logger.js';
import {
getRecordings,
getRecordingByRecordId,
@@ -65,7 +66,7 @@ router.get('/', authenticateToken, async (req, res) => {
res.json({ recordings: formatted });
} catch (err) {
console.error('Get recordings error:', err);
log.recordings.error(`Get recordings error: ${err.message}`);
res.status(500).json({ error: 'Recordings could not be loaded', recordings: [] });
}
});
@@ -117,7 +118,7 @@ router.get('/room/:uid', authenticateToken, async (req, res) => {
res.json({ recordings: formatted });
} catch (err) {
console.error('Get room recordings error:', err);
log.recordings.error(`Get room recordings error: ${err.message}`);
res.status(500).json({ error: 'Recordings could not be loaded', recordings: [] });
}
});
@@ -147,7 +148,7 @@ router.delete('/:recordID', authenticateToken, async (req, res) => {
await deleteRecording(req.params.recordID);
res.json({ message: 'Recording deleted' });
} catch (err) {
console.error('Delete recording error:', err);
log.recordings.error(`Delete recording error: ${err.message}`);
res.status(500).json({ error: 'Recording could not be deleted' });
}
});
@@ -178,7 +179,7 @@ router.put('/:recordID/publish', authenticateToken, async (req, res) => {
await publishRecording(req.params.recordID, publish);
res.json({ message: publish ? 'Recording published' : 'Recording unpublished' });
} catch (err) {
console.error('Publish recording error:', err);
log.recordings.error(`Publish recording error: ${err.message}`);
res.status(500).json({ error: 'Recording could not be updated' });
}
});

View File

@@ -6,6 +6,7 @@ import { fileURLToPath } from 'url';
import { rateLimit } from 'express-rate-limit';
import { getDb } from '../config/database.js';
import { authenticateToken } from '../middleware/auth.js';
import { log } from '../config/logger.js';
import {
createMeeting,
joinMeeting,
@@ -13,6 +14,12 @@ import {
getMeetingInfo,
isMeetingRunning,
} from '../config/bbb.js';
import {
isFederationEnabled,
getFederationDomain,
signPayload,
discoverInstance,
} from '../config/federation.js';
// L6: constant-time string comparison for access/moderator codes
function timingSafeEqual(a, b) {
@@ -72,7 +79,7 @@ router.get('/', authenticateToken, async (req, res) => {
res.json({ rooms: [...ownRooms, ...sharedRooms] });
} catch (err) {
console.error('List rooms error:', err);
log.rooms.error(`List rooms error: ${err.message}`);
res.status(500).json({ error: 'Rooms could not be loaded' });
}
});
@@ -94,7 +101,7 @@ router.get('/users/search', authenticateToken, async (req, res) => {
`, [searchTerm, searchTerm, searchTerm, req.user.id]);
res.json({ users });
} catch (err) {
console.error('Search users error:', err);
log.rooms.error(`Search users error: ${err.message}`);
res.status(500).json({ error: 'User search failed' });
}
});
@@ -133,7 +140,7 @@ router.get('/:uid', authenticateToken, async (req, res) => {
res.json({ room, sharedUsers });
} catch (err) {
console.error('Get room error:', err);
log.rooms.error(`Get room error: ${err.message}`);
res.status(500).json({ error: 'Room could not be loaded' });
}
});
@@ -205,7 +212,7 @@ router.post('/', authenticateToken, async (req, res) => {
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);
log.rooms.error(`Create room error: ${err.message}`);
res.status(500).json({ error: 'Room could not be created' });
}
});
@@ -288,7 +295,7 @@ router.put('/:uid', authenticateToken, async (req, res) => {
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);
log.rooms.error(`Update room error: ${err.message}`);
res.status(500).json({ error: 'Room could not be updated' });
}
});
@@ -307,10 +314,43 @@ router.delete('/:uid', authenticateToken, async (req, res) => {
return res.status(403).json({ error: 'No permission' });
}
// Notify federated instances about deletion (fire-and-forget)
if (isFederationEnabled()) {
try {
const outbound = await db.all(
'SELECT remote_domain FROM federation_outbound_invites WHERE room_uid = ?',
[room.uid]
);
for (const { remote_domain } of outbound) {
const payload = {
room_uid: room.uid,
timestamp: new Date().toISOString(),
};
const signature = signPayload(payload);
discoverInstance(remote_domain).then(({ baseUrl: remoteApi }) => {
fetch(`${remoteApi}/room-deleted`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Federation-Signature': signature,
'X-Federation-Origin': getFederationDomain(),
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(10_000),
}).catch(err => log.federation.warn(`Delete notify to ${remote_domain} failed: ${err.message}`));
}).catch(err => log.federation.warn(`Discovery for ${remote_domain} failed: ${err.message}`));
}
// Clean up outbound records
await db.run('DELETE FROM federation_outbound_invites WHERE room_uid = ?', [room.uid]);
} catch (fedErr) {
log.federation.warn(`Delete notification error (non-fatal): ${fedErr.message}`);
}
}
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);
log.rooms.error(`Delete room error: ${err.message}`);
res.status(500).json({ error: 'Room could not be deleted' });
}
});
@@ -330,7 +370,7 @@ router.get('/:uid/shares', authenticateToken, async (req, res) => {
`, [room.id]);
res.json({ shares });
} catch (err) {
console.error('Get shares error:', err);
log.rooms.error(`Get shares error: ${err.message}`);
res.status(500).json({ error: 'Error loading shares' });
}
});
@@ -364,7 +404,7 @@ router.post('/:uid/shares', authenticateToken, async (req, res) => {
`, [room.id]);
res.json({ shares });
} catch (err) {
console.error('Share room error:', err);
log.rooms.error(`Share room error: ${err.message}`);
res.status(500).json({ error: 'Error sharing room' });
}
});
@@ -386,7 +426,7 @@ router.delete('/:uid/shares/:userId', authenticateToken, async (req, res) => {
`, [room.id]);
res.json({ shares });
} catch (err) {
console.error('Remove share error:', err);
log.rooms.error(`Remove share error: ${err.message}`);
res.status(500).json({ error: 'Error removing share' });
}
});
@@ -421,7 +461,7 @@ router.post('/:uid/start', authenticateToken, async (req, res) => {
const joinUrl = await joinMeeting(room.uid, displayName, true, avatarURL);
res.json({ joinUrl });
} catch (err) {
console.error('Start meeting error:', err);
log.rooms.error(`Start meeting error: ${err.message}`);
res.status(500).json({ error: 'Meeting could not be started' });
}
});
@@ -455,7 +495,7 @@ router.post('/:uid/join', authenticateToken, async (req, res) => {
const joinUrl = await joinMeeting(room.uid, req.user.display_name || req.user.name, isModerator, avatarURL);
res.json({ joinUrl });
} catch (err) {
console.error('Join meeting error:', err);
log.rooms.error(`Join meeting error: ${err.message}`);
res.status(500).json({ error: 'Could not join meeting' });
}
});
@@ -482,7 +522,7 @@ router.post('/:uid/end', authenticateToken, async (req, res) => {
await endMeeting(room.uid);
res.json({ message: 'Meeting ended' });
} catch (err) {
console.error('End meeting error:', err);
log.rooms.error(`End meeting error: ${err.message}`);
res.status(500).json({ error: 'Meeting could not be ended' });
}
});
@@ -519,7 +559,7 @@ router.get('/:uid/public', async (req, res) => {
running,
});
} catch (err) {
console.error('Public room info error:', err);
log.rooms.error(`Public room info error: ${err.message}`);
res.status(500).json({ error: 'Room info could not be loaded' });
}
});
@@ -574,7 +614,7 @@ router.post('/:uid/guest-join', guestJoinLimiter, async (req, res) => {
const joinUrl = await joinMeeting(room.uid, name.trim(), isModerator, guestAvatarURL);
res.json({ joinUrl });
} catch (err) {
console.error('Guest join error:', err);
log.rooms.error(`Guest join error: ${err.message}`);
res.status(500).json({ error: 'Guest join failed' });
}
});
@@ -665,7 +705,7 @@ router.post('/:uid/presentation', authenticateToken, async (req, res) => {
const updated = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
res.json({ room: updated });
} catch (err) {
console.error('Presentation upload error:', err);
log.rooms.error(`Presentation upload error: ${err.message}`);
res.status(500).json({ error: 'Presentation could not be uploaded' });
}
});
@@ -687,7 +727,7 @@ router.delete('/:uid/presentation', authenticateToken, async (req, res) => {
const updated = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
res.json({ room: updated });
} catch (err) {
console.error('Presentation delete error:', err);
log.rooms.error(`Presentation delete error: ${err.message}`);
res.status(500).json({ error: 'Presentation could not be removed' });
}
});