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

@@ -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' });
}
});