4aea069295
Build & Push Docker Image / build (push) Successful in 4m6s
Bug fixes:
- bbb.js: replace undefined t('defaultWelcome') call that threw a
ReferenceError when a room had an empty welcome message, breaking
meeting creation. Default welcome and the guest-invite hint are now
localised via the i18n system (new "bbb" namespace in de/en).
- auth.js: app name was read from the never-written 'branding' settings
key, so custom names never appeared in verification emails or the TOTP
issuer. Now resolved through a shared getAppName() helper.
- auth.js: lowercase the email in the registration duplicate check so
case-variant duplicates return a clean 409 instead of a 500 (UNIQUE
violation).
- federation.js: select the user's "language" column so federation
invite emails respect the recipient's language instead of always
defaulting to English.
- calendar.js: a set reminder could not be cleared. COALESCE treated an
explicit reminder_minutes: null as "keep existing"; use a direct
assignment that distinguishes "omitted" (keep) from "null" (clear).
- index.js / analytics.js: exclude the BBB learning-analytics callback
from the global 100kb body limit and give it its own 5mb limit, since
analytics payloads for large meetings can be several MB.
Cleanup:
- Add server/config/appName.js as the single source of truth for the
app name (admin setting -> APP_NAME env -> 'Redlight') and use it in
auth, admin, rooms, calendar and federation, replacing the previous
mix of wrong DB key, direct app_name reads and bare process.env reads.
- Localise the BBB default welcome message in the room owner's language.
- Remove two unused safeAppName variables in mailer.js.
129 lines
5.4 KiB
JavaScript
129 lines
5.4 KiB
JavaScript
import 'dotenv/config';
|
|
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';
|
|
import authRoutes from './routes/auth.js';
|
|
import roomRoutes from './routes/rooms.js';
|
|
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 calendarRoutes from './routes/calendar.js';
|
|
import caldavRoutes from './routes/caldav.js';
|
|
import notificationRoutes from './routes/notifications.js';
|
|
import oauthRoutes from './routes/oauth.js';
|
|
import analyticsRoutes from './routes/analytics.js';
|
|
import { startFederationSync } from './jobs/federationSync.js';
|
|
import { startCalendarReminders } from './jobs/calendarReminders.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3001;
|
|
|
|
// Trust proxy - configurable via TRUST_PROXY env var (default: 1 = one local reverse proxy)
|
|
// Use a number to trust that many hops, or a string like 'loopback' / an IP/CIDR.
|
|
const rawTrustProxy = process.env.TRUST_PROXY ?? 'loopback';
|
|
const trustProxy = /^\d+$/.test(rawTrustProxy) ? parseInt(rawTrustProxy, 10) : rawTrustProxy;
|
|
app.set('trust proxy', trustProxy);
|
|
|
|
// ── Security headers ───────────────────────────────────────────────────────
|
|
app.use((req, res, next) => {
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
|
if (process.env.NODE_ENV === 'production') {
|
|
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
}
|
|
next();
|
|
});
|
|
|
|
// Middleware
|
|
// M10: restrict CORS in production; deny cross-origin by default
|
|
const corsOptions = process.env.APP_URL
|
|
? { origin: process.env.APP_URL, credentials: true }
|
|
: { origin: false };
|
|
app.use(cors(corsOptions));
|
|
// Global JSON body limit kept tight as a hardening measure. The BBB learning
|
|
// analytics callback can send much larger payloads, so it is excluded here and
|
|
// gets its own, more generous limit on the route itself (see routes/analytics.js).
|
|
const jsonParser = express.json({ limit: '100kb' });
|
|
app.use((req, res, next) => {
|
|
if (req.path.startsWith('/api/analytics/callback/')) return next();
|
|
return jsonParser(req, res, next);
|
|
});
|
|
// Request/Response logging (filters sensitive fields)
|
|
app.use(requestResponseLogger);
|
|
|
|
// Initialize database & start server
|
|
async function start() {
|
|
await initDatabase();
|
|
initMailer();
|
|
|
|
// Serve uploaded files (avatars are served via /api/auth/avatar/:filename)
|
|
const uploadsPath = path.join(__dirname, '..', 'uploads');
|
|
app.use('/uploads/branding', express.static(path.join(uploadsPath, 'branding')));
|
|
// Presentations are served via /api/rooms/presentations/:filename?token=… (HMAC-protected)
|
|
|
|
// API Routes
|
|
app.use('/api/auth', authRoutes);
|
|
app.use('/api/rooms', roomRoutes);
|
|
app.use('/api/recordings', recordingRoutes);
|
|
app.use('/api/admin', adminRoutes);
|
|
app.use('/api/branding', brandingRoutes);
|
|
app.use('/api/federation', federationRoutes);
|
|
app.use('/api/calendar', calendarRoutes);
|
|
app.use('/api/notifications', notificationRoutes);
|
|
app.use('/api/oauth', oauthRoutes);
|
|
app.use('/api/analytics', analyticsRoutes);
|
|
// CalDAV — mounted outside /api so calendar clients use a clean path
|
|
app.use('/caldav', caldavRoutes);
|
|
// Mount calendar federation receive also under /api/federation for remote instances
|
|
app.use('/api/federation', calendarRoutes);
|
|
app.get('/.well-known/redlight', wellKnownHandler);
|
|
|
|
// ── CalDAV service discovery (RFC 6764) ──────────────────────────────────
|
|
// Clients probe /.well-known/caldav then PROPFIND / before they know the
|
|
// real CalDAV mount point. Redirect them to /caldav/ for all HTTP methods.
|
|
app.all('/.well-known/caldav', (req, res) => {
|
|
res.redirect(301, '/caldav/');
|
|
});
|
|
// Some clients (e.g. Thunderbird) send PROPFIND / directly at the server root.
|
|
// Express doesn't register non-standard methods, so intercept via middleware.
|
|
app.use('/', (req, res, next) => {
|
|
if (req.method === 'PROPFIND' && req.path === '/') {
|
|
return res.redirect(301, '/caldav/');
|
|
}
|
|
next();
|
|
});
|
|
|
|
// Serve static files in production
|
|
if (process.env.NODE_ENV === 'production') {
|
|
app.use(express.static(path.join(__dirname, '..', 'dist')));
|
|
app.get('/*splat', (req, res) => {
|
|
res.sendFile(path.join(__dirname, '..', 'dist', 'index.html'));
|
|
});
|
|
}
|
|
|
|
app.listen(PORT, () => {
|
|
log.server.info(`Redlight server running on http://localhost:${PORT}`);
|
|
});
|
|
|
|
// Start periodic federation sync job (checks remote room settings every 60s)
|
|
startFederationSync();
|
|
// Start calendar reminder job (sends in-app + browser notifications before events)
|
|
startCalendarReminders();
|
|
}
|
|
|
|
start().catch(err => {
|
|
log.server.error(`Failed to start server: ${err.message}`);
|
|
process.exit(1);
|
|
});
|