feat: add getBaseUrl function for consistent base URL generation across routes
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m28s
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:
@@ -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')}`;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'");
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.')}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user