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

@@ -1,5 +1,6 @@
import crypto from 'crypto';
import xml2js from 'xml2js';
import { log, fmtDuration, fmtStatus, fmtMethod, fmtReturncode, sanitizeBBBParams } from './logger.js';
const BBB_URL = process.env.BBB_URL || 'https://your-bbb-server.com/bigbluebutton/api/';
const BBB_SECRET = process.env.BBB_SECRET || '';
@@ -18,39 +19,7 @@ function buildUrl(apiCall, params = {}) {
async function apiCall(apiCallName, params = {}, xmlBody = null) {
const url = buildUrl(apiCallName, params);
// Logging: compact key=value style, filter sensitive params
function formatUTC(d) {
const pad = n => String(n).padStart(2, '0');
const Y = d.getUTCFullYear();
const M = pad(d.getUTCMonth() + 1);
const D = pad(d.getUTCDate());
const h = pad(d.getUTCHours());
const m = pad(d.getUTCMinutes());
const s = pad(d.getUTCSeconds());
return `${Y}-${M}-${D} ${h}:${m}:${s} UTC`;
}
const SENSITIVE_KEYS = [/pass/i, /pwd/i, /password/i, /token/i, /jwt/i, /secret/i, /authorization/i, /auth/i, /api[_-]?key/i];
const isSensitive = key => SENSITIVE_KEYS.some(rx => rx.test(key));
function sanitizeParams(p) {
try {
const out = [];
for (const k of Object.keys(p || {})) {
if (k.toLowerCase() === 'checksum') continue; // never log checksum
if (isSensitive(k)) {
out.push(`${k}=[FILTERED]`);
} else {
let v = p[k];
if (typeof v === 'string' && v.length > 100) v = v.slice(0, 100) + '...[truncated]';
out.push(`${k}=${String(v)}`);
}
}
return out.join('&') || '-';
} catch (e) {
return '-';
}
}
const method = xmlBody ? 'POST' : 'GET';
const start = Date.now();
try {
@@ -65,38 +34,20 @@ async function apiCall(apiCallName, params = {}, xmlBody = null) {
trim: true,
});
// Compact log: time=... method=GET path=getMeetings format=xml status=200 duration=12.34 bbb_returncode=SUCCESS params=meetingID=123
try {
const tokens = [];
tokens.push(`time=${formatUTC(new Date())}`);
tokens.push(`method=${xmlBody ? 'POST' : 'GET'}`);
// include standard BBB api base path
let apiBasePath = '/bigbluebutton/api';
try {
const u = new URL(BBB_URL);
apiBasePath = (u.pathname || '/bigbluebutton/api').replace(/\/$/, '');
} catch (e) {
// keep default
}
// ensure single slash separation
const fullPath = `${apiBasePath}/${apiCallName}`.replace(/\/\/+/, '/');
tokens.push(`path=${fullPath}`);
tokens.push(`format=xml`);
tokens.push(`status=${response.status}`);
tokens.push(`duration=${(duration).toFixed(2)}`);
const returnCode = result && result.response && result.response.returncode ? result.response.returncode : '-';
tokens.push(`returncode=${returnCode}`);
const safeParams = sanitizeParams(params);
tokens.push(`params=${safeParams}`);
console.info(tokens.join(' '));
} catch (e) {
// ignore logging errors
}
const returncode = result?.response?.returncode || '-';
const paramStr = sanitizeBBBParams(params);
// Greenlight-style: method action → status returncode (duration) params
log.bbb.info(
`${fmtMethod(method)} ${apiCallName}${fmtStatus(response.status)} ${fmtReturncode(returncode)} (${fmtDuration(duration)}) ${paramStr}`
);
return result.response;
} catch (error) {
const duration = Date.now() - start;
console.error(`BBB API error (${apiCallName}) status=error duration=${(duration).toFixed(2)} err=${error.message}`);
log.bbb.error(
`${fmtMethod(method)} ${apiCallName} ✗ FAILED (${fmtDuration(duration)}) ${error.message}`
);
throw error;
}
}

View File

@@ -1,6 +1,7 @@
import bcrypt from 'bcryptjs';
import path from 'path';
import { fileURLToPath } from 'url';
import { log } from './logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -113,10 +114,10 @@ export function getDb() {
export async function initDatabase() {
// Create the right adapter
if (isPostgres) {
console.log('📦 Using PostgreSQL database');
log.db.info('Using PostgreSQL database');
db = new PostgresAdapter();
} else {
console.log('📦 Using SQLite database');
log.db.info('Using SQLite database');
db = new SqliteAdapter();
}
await db.init();
@@ -367,6 +368,43 @@ export async function initDatabase() {
}
}
// Federation sync: add deleted + updated_at to federated_rooms
if (!(await db.columnExists('federated_rooms', 'deleted'))) {
await db.exec('ALTER TABLE federated_rooms ADD COLUMN deleted INTEGER DEFAULT 0');
}
if (!(await db.columnExists('federated_rooms', 'updated_at'))) {
if (isPostgres) {
await db.exec('ALTER TABLE federated_rooms ADD COLUMN updated_at TIMESTAMP DEFAULT NOW()');
} else {
await db.exec('ALTER TABLE federated_rooms ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP');
}
}
// Track outbound federation invites for deletion propagation
if (isPostgres) {
await db.exec(`
CREATE TABLE IF NOT EXISTS federation_outbound_invites (
id SERIAL PRIMARY KEY,
room_uid TEXT NOT NULL,
remote_domain TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(room_uid, remote_domain)
);
CREATE INDEX IF NOT EXISTS idx_fed_out_room_uid ON federation_outbound_invites(room_uid);
`);
} else {
await db.exec(`
CREATE TABLE IF NOT EXISTS federation_outbound_invites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
room_uid TEXT NOT NULL,
remote_domain TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(room_uid, remote_domain)
);
CREATE INDEX IF NOT EXISTS idx_fed_out_room_uid ON federation_outbound_invites(room_uid);
`);
}
// ── Default admin ───────────────────────────────────────────────────────
const adminEmail = process.env.ADMIN_EMAIL || 'admin@example.com';
const adminPassword = process.env.ADMIN_PASSWORD || 'admin123';
@@ -378,6 +416,6 @@ export async function initDatabase() {
'INSERT INTO users (name, email, password_hash, role, email_verified) VALUES (?, ?, ?, ?, 1)',
['Administrator', adminEmail, hash, 'admin']
);
console.log(`Default admin created: ${adminEmail}`);
log.db.info(`Default admin created: ${adminEmail}`);
}
}

View File

@@ -2,6 +2,7 @@ import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { log } from './logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -19,14 +20,14 @@ if (FEDERATION_DOMAIN) {
}
if (!privateKeyPem) {
console.log('Generating new Ed25519 federation key pair...');
log.federation.info('Generating new Ed25519 key pair...');
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});
privateKeyPem = privateKey;
fs.writeFileSync(keyPath, privateKeyPem, 'utf8');
console.log(`Saved new federation private key to ${keyPath}`);
log.federation.info(`Saved new private key to ${keyPath}`);
}
// Derive public key from the loaded private key
@@ -83,7 +84,7 @@ export function verifyPayload(payload, signature, remotePublicKeyPem) {
const data = Buffer.from(JSON.stringify(payload));
return crypto.verify(null, data, remotePublicKeyPem, Buffer.from(signature, 'base64'));
} catch (e) {
console.error('Signature verification error:', e.message);
log.federation.error(`Signature verification error: ${e.message}`);
return false;
}
}
@@ -131,7 +132,7 @@ export async function discoverInstance(domain) {
discoveryCache.set(domain, result);
return result;
} catch (error) {
console.error(`Federation discovery failed for ${domain}:`, error.message);
log.federation.error(`Discovery failed for ${domain}: ${error.message}`);
throw new Error(`Could not discover Redlight instance at ${domain}: ${error.message}`);
}
}

157
server/config/logger.js Normal file
View File

@@ -0,0 +1,157 @@
/**
* Centralized logger for Redlight server.
*
* Produces clean, colorized, tagged log lines inspired by Greenlight/Rails lograge style.
*
* Format: TIMESTAMP LEVEL [TAG] message
* Example: 2026-03-01 12:00:00 INFO [BBB] GET getMeetings → 200 SUCCESS (45ms)
*/
// ── ANSI colors ─────────────────────────────────────────────────────────────
const C = {
reset: '\x1b[0m',
bold: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
gray: '\x1b[90m',
};
const USE_COLOR = process.env.NO_COLOR ? false : true;
const c = (color, text) => USE_COLOR ? `${color}${text}${C.reset}` : text;
// ── Timestamp ───────────────────────────────────────────────────────────────
function ts() {
const d = new Date();
const pad = n => String(n).padStart(2, '0');
const ms = String(d.getMilliseconds()).padStart(3, '0');
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}.${ms}`;
}
// ── Level formatting ────────────────────────────────────────────────────────
const LEVEL_STYLE = {
DEBUG: { color: C.gray, label: 'DEBUG' },
INFO: { color: C.green, label: ' INFO' },
WARN: { color: C.yellow, label: ' WARN' },
ERROR: { color: C.red, label: 'ERROR' },
};
// ── Tag colors ──────────────────────────────────────────────────────────────
const TAG_COLORS = {
BBB: C.magenta,
HTTP: C.cyan,
Federation: C.blue,
FedSync: C.blue,
DB: C.yellow,
Auth: C.green,
Server: C.white,
Mailer: C.cyan,
Redis: C.magenta,
Admin: C.yellow,
Rooms: C.green,
Recordings: C.cyan,
Branding: C.white,
};
function formatLine(level, tag, message) {
const lvl = LEVEL_STYLE[level] || LEVEL_STYLE.INFO;
const tagColor = TAG_COLORS[tag] || C.white;
const timestamp = c(C.gray, ts());
const levelStr = c(lvl.color, lvl.label);
const tagStr = c(tagColor, `[${tag}]`);
return `${timestamp} ${levelStr} ${tagStr} ${message}`;
}
// ── Public API ──────────────────────────────────────────────────────────────
/**
* Create a tagged logger.
* @param {string} tag - e.g. 'BBB', 'HTTP', 'Federation'
*/
export function createLogger(tag) {
return {
debug: (msg, ...args) => console.debug(formatLine('DEBUG', tag, msg), ...args),
info: (msg, ...args) => console.info(formatLine('INFO', tag, msg), ...args),
warn: (msg, ...args) => console.warn(formatLine('WARN', tag, msg), ...args),
error: (msg, ...args) => console.error(formatLine('ERROR', tag, msg), ...args),
};
}
// ── Pre-built loggers for common subsystems ─────────────────────────────────
export const log = {
bbb: createLogger('BBB'),
http: createLogger('HTTP'),
federation: createLogger('Federation'),
fedSync: createLogger('FedSync'),
db: createLogger('DB'),
auth: createLogger('Auth'),
server: createLogger('Server'),
mailer: createLogger('Mailer'),
redis: createLogger('Redis'),
admin: createLogger('Admin'),
rooms: createLogger('Rooms'),
recordings: createLogger('Recordings'),
branding: createLogger('Branding'),
};
// ── Helpers ─────────────────────────────────────────────────────────────────
/** Format duration with unit and color. */
export function fmtDuration(ms) {
const num = Number(ms);
if (num < 100) return c(C.green, `${num.toFixed(0)}ms`);
if (num < 1000) return c(C.yellow, `${num.toFixed(0)}ms`);
return c(C.red, `${(num / 1000).toFixed(2)}s`);
}
/** Format HTTP status with color. */
export function fmtStatus(status) {
const s = Number(status);
if (s < 300) return c(C.green, String(s));
if (s < 400) return c(C.cyan, String(s));
if (s < 500) return c(C.yellow, String(s));
return c(C.red, String(s));
}
/** Format HTTP method with color. */
export function fmtMethod(method) {
const m = String(method).toUpperCase();
const colors = { GET: C.green, POST: C.cyan, PUT: C.yellow, PATCH: C.yellow, DELETE: C.red };
return c(colors[m] || C.white, m.padEnd(6));
}
/** Format BBB returncode with color. */
export function fmtReturncode(code) {
if (code === 'SUCCESS') return c(C.green, code);
if (code === 'FAILED') return c(C.red, code);
return c(C.yellow, code || '-');
}
// ── Sensitive value filtering ───────────────────────────────────────────────
const SENSITIVE_KEYS = [/pass/i, /pwd/i, /password/i, /token/i, /jwt/i, /secret/i, /authorization/i, /api[_-]?key/i];
export function isSensitiveKey(key) {
return SENSITIVE_KEYS.some(rx => rx.test(key));
}
/**
* Sanitize BBB params for logging: filter sensitive values, truncate long strings, omit checksum.
*/
export function sanitizeBBBParams(params) {
const parts = [];
for (const k of Object.keys(params || {})) {
if (k.toLowerCase() === 'checksum') continue;
if (isSensitiveKey(k)) {
parts.push(`${k}=${c(C.dim, '[FILTERED]')}`);
} else {
let v = params[k];
if (typeof v === 'string' && v.length > 80) v = v.slice(0, 80) + '…';
parts.push(`${c(C.gray, k)}=${v}`);
}
}
return parts.join(' ') || '-';
}

View File

@@ -1,4 +1,5 @@
import nodemailer from 'nodemailer';
import { log } from './logger.js';
let transporter;
@@ -20,7 +21,7 @@ export function initMailer() {
const pass = process.env.SMTP_PASS;
if (!host || !user || !pass) {
console.warn('⚠️ SMTP not configured email verification disabled');
log.mailer.warn('SMTP not configured email verification disabled');
return false;
}
@@ -34,7 +35,7 @@ export function initMailer() {
socketTimeout: 15_000, // 15 s of inactivity before abort
});
console.log('SMTP mailer configured');
log.mailer.info('SMTP mailer configured');
return true;
}

View File

@@ -1,4 +1,5 @@
import Redis from 'ioredis';
import { log } from './logger.js';
const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
@@ -13,12 +14,12 @@ const redis = new Redis(REDIS_URL, {
redis.on('error', (err) => {
// Suppress ECONNREFUSED noise after initial failure — only warn
if (err.code !== 'ECONNREFUSED') {
console.warn('⚠️ DragonflyDB error:', err.message);
log.redis.warn(`DragonflyDB error: ${err.message}`);
}
});
redis.on('connect', () => {
console.log('🐉 DragonflyDB connected');
log.redis.info('DragonflyDB connected');
});
export default redis;

View File

@@ -3,6 +3,7 @@ import express from 'express';
import cors from 'cors';
import path from 'path';
import { fileURLToPath } from 'url';
import { log } from './config/logger.js';
import requestResponseLogger from './middleware/logging.js';
import { initDatabase } from './config/database.js';
import { initMailer } from './config/mailer.js';
@@ -12,6 +13,7 @@ import recordingRoutes from './routes/recordings.js';
import adminRoutes from './routes/admin.js';
import brandingRoutes from './routes/branding.js';
import federationRoutes, { wellKnownHandler } from './routes/federation.js';
import { startFederationSync } from './jobs/federationSync.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -62,11 +64,14 @@ async function start() {
}
app.listen(PORT, () => {
console.log(`🔴 Redlight server running on http://localhost:${PORT}`);
log.server.info(`Redlight server running on http://localhost:${PORT}`);
});
// Start periodic federation sync job (checks remote room settings every 60s)
startFederationSync();
}
start().catch(err => {
console.error('❌ Failed to start server:', err);
log.server.error(`Failed to start server: ${err.message}`);
process.exit(1);
});

View File

@@ -0,0 +1,154 @@
import { getDb } from '../config/database.js';
import { log, fmtDuration } from '../config/logger.js';
import {
isFederationEnabled,
getFederationDomain,
signPayload,
discoverInstance,
parseAddress,
} from '../config/federation.js';
const SYNC_INTERVAL_MS = 60_000; // 1 minute
let syncTimer = null;
/**
* Periodic federation sync job.
* Groups federated rooms by origin domain, then batch-queries each origin
* for current room settings. Updates local records if settings changed or
* if the room was deleted on the origin.
*/
async function runSync() {
if (!isFederationEnabled()) return;
const syncStart = Date.now();
let totalUpdated = 0;
let totalDeleted = 0;
let totalRooms = 0;
try {
const db = getDb();
// Fetch all non-deleted federated rooms
const rooms = await db.all(
'SELECT id, meet_id, from_user, room_name, max_participants, allow_recording FROM federated_rooms WHERE deleted = 0'
);
if (rooms.length === 0) return;
totalRooms = rooms.length;
// Group by origin domain
const byDomain = new Map();
for (const room of rooms) {
if (!room.meet_id) continue; // no room UID, can't sync
const { domain } = parseAddress(room.from_user);
if (!domain) continue;
if (!byDomain.has(domain)) byDomain.set(domain, []);
byDomain.get(domain).push(room);
}
// Query each origin domain
for (const [domain, domainRooms] of byDomain) {
try {
const roomUids = [...new Set(domainRooms.map(r => r.meet_id))];
const payload = {
room_uids: roomUids,
timestamp: new Date().toISOString(),
};
const signature = signPayload(payload);
const { baseUrl: remoteApi } = await discoverInstance(domain);
const response = await fetch(`${remoteApi}/room-sync`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Federation-Signature': signature,
'X-Federation-Origin': getFederationDomain(),
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) {
log.fedSync.warn(`${domain} responded with status ${response.status}`);
continue;
}
const data = await response.json();
const remoteRooms = data.rooms || {};
// Update local records
for (const localRoom of domainRooms) {
const remote = remoteRooms[localRoom.meet_id];
if (!remote) continue; // UID not in response, skip
if (remote.deleted) {
// Room was deleted on origin
await db.run(
'UPDATE federated_rooms SET deleted = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[localRoom.id]
);
totalDeleted++;
log.fedSync.info(`Room ${localRoom.meet_id} deleted on origin ${domain}`);
} else {
// Check if settings changed
const changed =
localRoom.room_name !== remote.room_name ||
(localRoom.max_participants ?? 0) !== (remote.max_participants ?? 0) ||
(localRoom.allow_recording ?? 1) !== (remote.allow_recording ?? 1);
if (changed) {
await db.run(
`UPDATE federated_rooms
SET room_name = ?, max_participants = ?, allow_recording = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
[remote.room_name, remote.max_participants ?? 0, remote.allow_recording ?? 1, localRoom.id]
);
totalUpdated++;
log.fedSync.info(`Room ${localRoom.meet_id} settings updated from ${domain}`);
}
}
}
} catch (err) {
log.fedSync.warn(`Sync with ${domain} failed: ${err.message}`);
}
}
// Summary log (only if something happened)
if (totalUpdated > 0 || totalDeleted > 0) {
log.fedSync.info(
`Sync complete: ${totalRooms} rooms, ${totalUpdated} updated, ${totalDeleted} deleted (${fmtDuration(Date.now() - syncStart)})`
);
}
} catch (err) {
log.fedSync.error(`Sync job failed: ${err.message}`);
}
}
/**
* Start the periodic federation sync job.
*/
export function startFederationSync() {
if (!isFederationEnabled()) {
log.fedSync.info('Disabled (federation not configured)');
return;
}
// Run first sync after a short delay to let the server fully start
setTimeout(() => {
runSync();
syncTimer = setInterval(runSync, SYNC_INTERVAL_MS);
log.fedSync.info('Started (interval: 60s)');
}, 5_000);
}
/**
* Stop the periodic federation sync job.
*/
export function stopFederationSync() {
if (syncTimer) {
clearInterval(syncTimer);
syncTimer = null;
}
}

View File

@@ -2,9 +2,10 @@ import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';
import { getDb } from '../config/database.js';
import redis from '../config/redis.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;
@@ -29,7 +30,7 @@ export async function authenticateToken(req, res, next) {
}
} catch (redisErr) {
// Graceful degradation: if Redis is unavailable, allow the request
console.warn('Redis blacklist check skipped:', redisErr.message);
log.auth.warn(`Redis blacklist check skipped: ${redisErr.message}`);
}
}

View File

@@ -1,118 +1,25 @@
import util from 'util';
const SENSITIVE_KEYS = [/pass/i, /pwd/i, /password/i, /token/i, /jwt/i, /secret/i, /authorization/i, /auth/i, /api[_-]?key/i];
const MAX_LOG_BODY_LENGTH = 200000; // 200 KB
function isSensitiveKey(key) {
return SENSITIVE_KEYS.some(rx => rx.test(key));
}
function filterValue(key, value, depth = 0) {
if (depth > 5) return '[MAX_DEPTH]';
if (key && isSensitiveKey(key)) return '[FILTERED]';
if (value === null || value === undefined) return value;
if (typeof value === 'string') {
if (value.length > MAX_LOG_BODY_LENGTH) return '[TOO_LARGE]';
return value;
}
if (typeof value === 'number' || typeof value === 'boolean') return value;
if (Array.isArray(value)) return value.map(v => filterValue(null, v, depth + 1));
if (typeof value === 'object') {
const out = {};
for (const k of Object.keys(value)) {
out[k] = filterValue(k, value[k], depth + 1);
}
return out;
}
return typeof value;
}
function filterHeaders(headers) {
const out = {};
for (const k of Object.keys(headers || {})) {
if (/^authorization$/i.test(k) || /^cookie$/i.test(k)) {
out[k] = '[FILTERED]';
continue;
}
if (isSensitiveKey(k)) {
out[k] = '[FILTERED]';
continue;
}
out[k] = headers[k];
}
return out;
}
function formatUTC(d) {
const pad = n => String(n).padStart(2, '0');
const Y = d.getUTCFullYear();
const M = pad(d.getUTCMonth() + 1);
const D = pad(d.getUTCDate());
const h = pad(d.getUTCHours());
const m = pad(d.getUTCMinutes());
const s = pad(d.getUTCSeconds());
return `${Y}-${M}-${D} ${h}:${m}:${s} UTC`;
}
import { log, fmtDuration, fmtStatus, fmtMethod } from '../config/logger.js';
export default function requestResponseLogger(req, res, next) {
try {
const start = Date.now();
const { method, originalUrl } = req;
const ip = req.ip || req.headers['x-forwarded-for'] || req.socket.remoteAddress;
const start = Date.now();
const { method, originalUrl } = req;
const reqHeaders = filterHeaders(req.headers);
res.on('finish', () => {
try {
const duration = Date.now() - start;
const status = res.statusCode;
const contentType = (res.getHeader?.('content-type') || '').toString().toLowerCase();
const format = contentType.includes('json') ? 'json' : contentType.includes('html') ? 'html' : '';
const formatStr = format ? ` ${format}` : '';
let reqBody = '[not-logged]';
const contentType = (req.headers['content-type'] || '').toLowerCase();
if (contentType.includes('multipart/form-data')) {
reqBody = '[multipart/form-data]';
} else if (req.body) {
try {
reqBody = filterValue(null, req.body);
} catch (e) {
reqBody = '[unserializable]';
}
// METHOD /path → status (duration)
log.http.info(
`${fmtMethod(method)} ${originalUrl}${fmtStatus(status)}${formatStr} (${fmtDuration(duration)})`
);
} catch {
// never break the request pipeline
}
// Capture response body by wrapping res.send
const oldSend = res.send.bind(res);
let responseBody = undefined;
res.send = function sendOverWrite(body) {
responseBody = body;
return oldSend(body);
};
res.on('finish', () => {
try {
const duration = Date.now() - start; // ms
const resContentType = (res.getHeader && (res.getHeader('content-type') || '')).toString().toLowerCase();
// Compact key=value log (no bodies, sensitive data filtered)
const tokens = [];
tokens.push(`time=${formatUTC(new Date())}`);
tokens.push(`method=${method}`);
tokens.push(`path=${originalUrl.replace(/\s/g, '%20')}`);
const fmt = resContentType.includes('json') ? 'json' : (resContentType.includes('html') ? 'html' : 'other');
tokens.push(`format=${fmt}`);
tokens.push(`status=${res.statusCode}`);
tokens.push(`duration=${(duration).toFixed(2)}`);
// Optional: content-length if available
try {
const cl = res.getHeader && (res.getHeader('content-length') || res.getHeader('Content-Length'));
if (cl) tokens.push(`length=${String(cl)}`);
} catch (e) {
// ignore
}
console.info(tokens.join(' '));
} catch (e) {
console.error('RequestLogger error:', e);
}
});
} catch (e) {
console.error('RequestLogger setup failure:', e);
}
});
return next();
}

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