feat: add getBaseUrl function for consistent base URL generation across routes
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m28s

feat(calendar): display local timezone in calendar view
feat(i18n): add timezone label to German and English translations
This commit is contained in:
2026-03-04 09:44:02 +01:00
parent 61274d31f1
commit 43d94181f9
11 changed files with 54 additions and 28 deletions

View File

@@ -57,3 +57,14 @@ export function generateToken(userId) {
const jti = uuidv4();
return jwt.sign({ userId, jti }, JWT_SECRET, { expiresIn: '7d' });
}
/**
* Build the public base URL for the application.
* Prefers APP_URL env var. Falls back to X-Forwarded-Proto + Host header
* so that links are correct behind a TLS-terminating reverse proxy.
*/
export function getBaseUrl(req) {
if (process.env.APP_URL) return process.env.APP_URL.replace(/\/+$/, '');
const proto = req.get('x-forwarded-proto')?.split(',')[0]?.trim() || req.protocol;
return `${proto}://${req.get('host')}`;
}

View File

@@ -2,7 +2,7 @@
import bcrypt from 'bcryptjs';
import { v4 as uuidv4 } from 'uuid';
import { getDb } from '../config/database.js';
import { authenticateToken, requireAdmin } from '../middleware/auth.js';
import { authenticateToken, requireAdmin, getBaseUrl } from '../middleware/auth.js';
import { isMailerConfigured, sendInviteEmail } from '../config/mailer.js';
import { log } from '../config/logger.js';
import {
@@ -208,7 +208,7 @@ router.post('/invites', authenticateToken, requireAdmin, async (req, res) => {
);
// Send invite email if SMTP is configured
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
const baseUrl = getBaseUrl(req);
const inviteUrl = `${baseUrl}/register?invite=${token}`;
// Load app name

View File

@@ -9,7 +9,7 @@ import { rateLimit } from 'express-rate-limit';
import { RedisStore } from 'rate-limit-redis';
import { getDb } from '../config/database.js';
import redis from '../config/redis.js';
import { authenticateToken, generateToken } from '../middleware/auth.js';
import { authenticateToken, generateToken, getBaseUrl } from '../middleware/auth.js';
import { isMailerConfigured, sendVerificationEmail } from '../config/mailer.js';
import { log } from '../config/logger.js';
@@ -189,7 +189,7 @@ router.post('/register', registerLimiter, async (req, res) => {
}
// Build verification URL
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
const baseUrl = getBaseUrl(req);
const verifyUrl = `${baseUrl}/verify-email?token=${verificationToken}`;
// Load app name from branding settings
@@ -303,7 +303,7 @@ router.post('/resend-verification', resendVerificationLimiter, async (req, res)
[verificationToken, expires, now, user.id]
);
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
const baseUrl = getBaseUrl(req);
const verifyUrl = `${baseUrl}/verify-email?token=${verificationToken}`;
const brandingSetting = await db.get("SELECT value FROM settings WHERE key = 'branding'");

View File

@@ -14,6 +14,7 @@ import { Router, text } from 'express';
import crypto from 'crypto';
import { getDb } from '../config/database.js';
import { log } from '../config/logger.js';
import { getBaseUrl } from '../middleware/auth.js';
const router = Router();
@@ -239,12 +240,8 @@ function validateCalDAVUser(req, res, next) {
next();
}
// ── Base URL helper ────────────────────────────────────────────────────────
function baseUrl(req) {
const proto = req.get('x-forwarded-proto') || req.protocol;
const host = req.get('x-forwarded-host') || req.get('host');
return `${proto}://${host}`;
}
// ── Base URL helper (uses shared getBaseUrl from auth.js) ──────────────────
const baseUrl = getBaseUrl;
// ── PROPFIND response builders ─────────────────────────────────────────────

View File

@@ -1,7 +1,7 @@
import { Router } from 'express';
import crypto from 'crypto';
import { getDb } from '../config/database.js';
import { authenticateToken } from '../middleware/auth.js';
import { authenticateToken, getBaseUrl } from '../middleware/auth.js';
import { log } from '../config/logger.js';
import { sendCalendarInviteEmail } from '../config/mailer.js';
import {
@@ -304,7 +304,7 @@ router.post('/events/:id/share', authenticateToken, async (req, res) => {
// Send notification email (fire-and-forget)
const targetUser = await db.get('SELECT name, display_name, email, language FROM users WHERE id = ?', [user_id]);
if (targetUser?.email) {
const appUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
const appUrl = getBaseUrl(req);
const inboxUrl = `${appUrl}/federation/inbox`;
const appName = process.env.APP_NAME || 'Redlight';
const senderUser = await db.get('SELECT name, display_name FROM users WHERE id = ?', [req.user.id]);
@@ -469,7 +469,7 @@ router.get('/events/:id/ics', authenticateToken, async (req, res) => {
}
// Build room join URL if linked
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
const baseUrl = getBaseUrl(req);
let location = '';
if (event.room_uid) {
location = `${baseUrl}/join/${event.room_uid}`;
@@ -506,7 +506,7 @@ router.post('/events/:id/federation', authenticateToken, async (req, res) => {
const event = await db.get('SELECT * FROM calendar_events WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]);
if (!event) return res.status(404).json({ error: 'Event not found or no permission' });
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
const baseUrl = getBaseUrl(req);
let joinUrl = null;
if (event.room_uid) {
joinUrl = `${baseUrl}/join/${event.room_uid}`;
@@ -626,7 +626,7 @@ router.post(['/receive-event', '/calendar-event'], calendarFederationLimiter, as
// Send notification email (fire-and-forget)
if (targetUser.email) {
const appUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
const appUrl = getBaseUrl(req);
const inboxUrl = `${appUrl}/federation/inbox`;
const appName = process.env.APP_NAME || 'Redlight';
sendCalendarInviteEmail(

View File

@@ -2,7 +2,7 @@
import { v4 as uuidv4 } from 'uuid';
import { rateLimit } from 'express-rate-limit';
import { getDb } from '../config/database.js';
import { authenticateToken } from '../middleware/auth.js';
import { authenticateToken, getBaseUrl } from '../middleware/auth.js';
import { sendFederationInviteEmail, sendCalendarEventDeletedEmail } from '../config/mailer.js';
import { log } from '../config/logger.js';
import { createNotification } from '../config/notifications.js';
@@ -84,7 +84,7 @@ router.post('/invite', authenticateToken, async (req, res) => {
// Build guest join URL for the remote user
// If the room has an access code, embed it so the recipient can join without manual entry
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
const baseUrl = getBaseUrl(req);
const joinUrl = room.access_code
? `${baseUrl}/join/${room.uid}?ac=${encodeURIComponent(room.access_code)}`
: `${baseUrl}/join/${room.uid}`;
@@ -236,7 +236,7 @@ router.post('/receive', federationReceiveLimiter, async (req, res) => {
// Send notification email (truly fire-and-forget - never blocks the response)
if (targetUser.email) {
const appUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
const appUrl = getBaseUrl(req);
const inboxUrl = `${appUrl}/federation/inbox`;
const appName = process.env.APP_NAME || 'Redlight';
sendFederationInviteEmail(

View File

@@ -19,7 +19,7 @@ import { Router } from 'express';
import { rateLimit } from 'express-rate-limit';
import { v4 as uuidv4 } from 'uuid';
import { getDb } from '../config/database.js';
import { generateToken } from '../middleware/auth.js';
import { generateToken, getBaseUrl } from '../middleware/auth.js';
import { log } from '../config/logger.js';
import {
getOAuthConfig,
@@ -91,7 +91,7 @@ router.get('/authorize', async (req, res) => {
const state = await createOAuthState('oidc', codeVerifier, returnTo);
// Build callback URL
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
const baseUrl = getBaseUrl(req);
const redirectUri = `${baseUrl}/api/oauth/callback`;
// Build authorization URL
@@ -119,7 +119,7 @@ router.get('/callback', callbackLimiter, async (req, res) => {
const { code, state, error: oauthError, error_description } = req.query;
// Build frontend error redirect helper
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
const baseUrl = getBaseUrl(req);
const errorRedirect = (msg) =>
res.redirect(`${baseUrl}/oauth/callback?error=${encodeURIComponent(msg)}`);
@@ -253,7 +253,7 @@ router.get('/callback', callbackLimiter, async (req, res) => {
res.redirect(`${baseUrl}/oauth/callback?token=${encodeURIComponent(token)}&return_to=${encodeURIComponent(returnTo)}`);
} catch (err) {
log.auth.error(`OAuth callback error: ${err.message}`);
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
const baseUrl = getBaseUrl(req);
res.redirect(`${baseUrl}/oauth/callback?error=${encodeURIComponent('OAuth authentication failed. Please try again.')}`);
}
});

View File

@@ -5,7 +5,7 @@ import path from 'path';
import { fileURLToPath } from 'url';
import { rateLimit } from 'express-rate-limit';
import { getDb } from '../config/database.js';
import { authenticateToken } from '../middleware/auth.js';
import { authenticateToken, getBaseUrl } from '../middleware/auth.js';
import { log } from '../config/logger.js';
import { createNotification } from '../config/notifications.js';
import {
@@ -49,7 +49,7 @@ const router = Router();
// Build avatar URL for a user (uploaded image or generated initials)
function getUserAvatarURL(req, user) {
const baseUrl = `${req.protocol}://${req.get('host')}`;
const baseUrl = getBaseUrl(req);
if (user.avatar_image) {
return `${baseUrl}/api/auth/avatar/${user.avatar_image}`;
}
@@ -475,7 +475,7 @@ router.post('/:uid/start', authenticateToken, async (req, res) => {
}
}
const baseUrl = `${req.protocol}://${req.get('host')}`;
const baseUrl = getBaseUrl(req);
const loginURL = `${baseUrl}/join/${room.uid}`;
const presentationUrl = room.presentation_file
? `${baseUrl}/uploads/presentations/${room.presentation_file}`
@@ -623,7 +623,7 @@ router.post('/:uid/guest-join', guestJoinLimiter, async (req, res) => {
// If meeting not running but anyone_can_start, create it
if (!running && room.anyone_can_start) {
const baseUrl = `${req.protocol}://${req.get('host')}`;
const baseUrl = getBaseUrl(req);
const loginURL = `${baseUrl}/join/${room.uid}`;
await createMeeting(room, baseUrl, loginURL);
}
@@ -634,7 +634,7 @@ router.post('/:uid/guest-join', guestJoinLimiter, async (req, res) => {
isModerator = true;
}
const baseUrl = `${req.protocol}://${req.get('host')}`;
const baseUrl = getBaseUrl(req);
const guestAvatarURL = `${baseUrl}/api/auth/avatar/initials/${encodeURIComponent(name.trim())}`;
const joinUrl = await joinMeeting(room.uid, name.trim(), isModerator, guestAvatarURL);
res.json({ joinUrl });