feat(logging): implement centralized logging system and replace console errors with structured logs
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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user