Refactor code and improve internationalization support
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
- Updated import statements to remove invisible characters. - Standardized comments to use a consistent hyphen format. - Adjusted username validation error messages for consistency. - Enhanced email sending functions to include language support. - Added email internationalization configuration for dynamic translations. - Updated calendar and federation routes to include language in user queries. - Improved user feedback messages in German and English for clarity.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Router } from 'express';
|
||||
import { Router } from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { getDb } from '../config/database.js';
|
||||
@@ -26,7 +26,7 @@ router.post('/users', authenticateToken, requireAdmin, async (req, res) => {
|
||||
|
||||
const usernameRegex = /^[a-zA-Z0-9_-]{3,30}$/;
|
||||
if (!usernameRegex.test(name)) {
|
||||
return res.status(400).json({ error: 'Username may only contain letters, numbers, _ and - (3–30 chars)' });
|
||||
return res.status(400).json({ error: 'Username may only contain letters, numbers, _ and - (3-30 chars)' });
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
@@ -211,7 +211,7 @@ router.post('/invites', authenticateToken, requireAdmin, async (req, res) => {
|
||||
|
||||
if (isMailerConfigured()) {
|
||||
try {
|
||||
await sendInviteEmail(email.toLowerCase(), inviteUrl, appName);
|
||||
await sendInviteEmail(email.toLowerCase(), inviteUrl, appName, 'en');
|
||||
} catch (mailErr) {
|
||||
log.admin.warn(`Invite email failed (non-fatal): ${mailErr.message}`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Router } from 'express';
|
||||
import { Router } from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
@@ -37,7 +37,7 @@ const EMAIL_RE = /^[^\s@]{1,64}@[^\s@]{1,253}\.[^\s@]{2,}$/;
|
||||
// Simple format check for theme/language IDs (actual validation happens on the frontend)
|
||||
const SAFE_ID_RE = /^[a-zA-Z0-9_-]{1,50}$/;
|
||||
|
||||
// Allowlist for CSS color values – only permits hsl(), hex (#rgb/#rrggbb) and plain names
|
||||
// Allowlist for CSS color values - only permits hsl(), hex (#rgb/#rrggbb) and plain names
|
||||
const SAFE_COLOR_RE = /^(?:#[0-9a-fA-F]{3,8}|hsl\(\d{1,3},\s*\d{1,3}%,\s*\d{1,3}%\)|[a-zA-Z]{1,30})$/;
|
||||
|
||||
const MIN_PASSWORD_LENGTH = 8;
|
||||
@@ -145,7 +145,7 @@ router.post('/register', registerLimiter, async (req, res) => {
|
||||
|
||||
const usernameRegex = /^[a-zA-Z0-9_-]{3,30}$/;
|
||||
if (!usernameRegex.test(username)) {
|
||||
return res.status(400).json({ error: 'Username may only contain letters, numbers, _ and - (3–30 chars)' });
|
||||
return res.status(400).json({ error: 'Username may only contain letters, numbers, _ and - (3-30 chars)' });
|
||||
}
|
||||
|
||||
// M1: email format
|
||||
@@ -200,7 +200,7 @@ router.post('/register', registerLimiter, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
await sendVerificationEmail(email.toLowerCase(), display_name, verifyUrl, appName);
|
||||
await sendVerificationEmail(email.toLowerCase(), display_name, verifyUrl, appName, 'en');
|
||||
} catch (mailErr) {
|
||||
log.auth.error(`Verification mail failed: ${mailErr.message}`);
|
||||
// Account is created but email failed — user can resend from login page
|
||||
@@ -210,7 +210,7 @@ router.post('/register', registerLimiter, async (req, res) => {
|
||||
return res.status(201).json({ needsVerification: true, message: 'Verification email has been sent' });
|
||||
}
|
||||
|
||||
// No SMTP configured – register and login immediately (legacy behaviour)
|
||||
// No SMTP configured - register and login immediately (legacy behaviour)
|
||||
const result = await db.run(
|
||||
'INSERT INTO users (name, display_name, email, password_hash, email_verified) VALUES (?, ?, ?, ?, 1)',
|
||||
[username, display_name, email.toLowerCase(), hash]
|
||||
@@ -278,7 +278,7 @@ router.post('/resend-verification', resendVerificationLimiter, async (req, res)
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const user = await db.get('SELECT id, name, display_name, email_verified, verification_resend_at FROM users WHERE email = ?', [email.toLowerCase()]);
|
||||
const user = await db.get('SELECT id, name, display_name, language, email_verified, verification_resend_at FROM users WHERE email = ?', [email.toLowerCase()]);
|
||||
|
||||
if (!user || user.email_verified) {
|
||||
// Don't reveal whether account exists
|
||||
@@ -313,7 +313,7 @@ router.post('/resend-verification', resendVerificationLimiter, async (req, res)
|
||||
}
|
||||
|
||||
try {
|
||||
await sendVerificationEmail(email.toLowerCase(), user.display_name || user.name, verifyUrl, appName);
|
||||
await sendVerificationEmail(email.toLowerCase(), user.display_name || user.name, verifyUrl, appName, user.language || 'en');
|
||||
} catch (mailErr) {
|
||||
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.' });
|
||||
@@ -335,7 +335,7 @@ router.post('/login', loginLimiter, async (req, res) => {
|
||||
return res.status(400).json({ error: 'Email and password are required' });
|
||||
}
|
||||
|
||||
// M1: basic email format check – invalid format can never match a real account
|
||||
// M1: basic email format check - invalid format can never match a real account
|
||||
if (!EMAIL_RE.test(email)) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
@@ -361,7 +361,7 @@ router.post('/login', loginLimiter, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/auth/logout – revoke JWT via DragonflyDB blacklist
|
||||
// POST /api/auth/logout - revoke JWT via DragonflyDB blacklist
|
||||
router.post('/logout', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
@@ -432,7 +432,7 @@ router.put('/profile', authenticateToken, profileLimiter, async (req, res) => {
|
||||
if (name && name !== req.user.name) {
|
||||
const usernameRegex = /^[a-zA-Z0-9_-]{3,30}$/;
|
||||
if (!usernameRegex.test(name)) {
|
||||
return res.status(400).json({ error: 'Username may only contain letters, numbers, _ and - (3–30 chars)' });
|
||||
return res.status(400).json({ error: 'Username may only contain letters, numbers, _ and - (3-30 chars)' });
|
||||
}
|
||||
const existingUsername = await db.get('SELECT id FROM users WHERE LOWER(name) = LOWER(?) AND id != ?', [name, req.user.id]);
|
||||
if (existingUsername) {
|
||||
@@ -504,7 +504,7 @@ router.post('/avatar', authenticateToken, avatarLimiter, async (req, res) => {
|
||||
return res.status(400).json({ error: 'Only image files are allowed' });
|
||||
}
|
||||
|
||||
// M15: stream-level size limit – abort as soon as 2 MB is exceeded
|
||||
// M15: stream-level size limit - abort as soon as 2 MB is exceeded
|
||||
const MAX_AVATAR_SIZE = 2 * 1024 * 1024;
|
||||
const buffer = await new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Router } from 'express';
|
||||
import { Router } from 'express';
|
||||
import crypto from 'crypto';
|
||||
import { getDb } from '../config/database.js';
|
||||
import { authenticateToken } from '../middleware/auth.js';
|
||||
@@ -289,7 +289,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 FROM users WHERE id = ?', [user_id]);
|
||||
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 inboxUrl = `${appUrl}/federation/inbox`;
|
||||
@@ -301,7 +301,7 @@ router.post('/events/:id/share', authenticateToken, async (req, res) => {
|
||||
(targetUser.display_name && targetUser.display_name !== '') ? targetUser.display_name : targetUser.name,
|
||||
fromDisplay,
|
||||
event.title, event.start_time, event.end_time, event.description,
|
||||
inboxUrl, appName
|
||||
inboxUrl, appName, targetUser.language || 'en'
|
||||
).catch(mailErr => {
|
||||
log.server.warn('Calendar local share mail failed (non-fatal):', mailErr.message);
|
||||
});
|
||||
@@ -423,7 +423,7 @@ router.delete('/local-invitations/:id', authenticateToken, async (req, res) => {
|
||||
if (inv.status === 'pending') {
|
||||
await db.run("UPDATE calendar_local_invitations SET status = 'declined' WHERE id = ?", [inv.id]);
|
||||
} else {
|
||||
// Accepted/declined – remove the share too if it was accepted
|
||||
// Accepted/declined - remove the share too if it was accepted
|
||||
if (inv.status === 'accepted') {
|
||||
await db.run('DELETE FROM calendar_event_shares WHERE event_id = ? AND user_id = ?', [inv.event_id, req.user.id]);
|
||||
}
|
||||
@@ -587,7 +587,7 @@ router.post(['/receive-event', '/calendar-event'], calendarFederationLimiter, as
|
||||
// Find local user
|
||||
const { username } = parseAddress(to_user);
|
||||
const db = getDb();
|
||||
const targetUser = await db.get('SELECT id, name, email FROM users WHERE LOWER(name) = LOWER(?)', [username]);
|
||||
const targetUser = await db.get('SELECT id, name, email, language FROM users WHERE LOWER(name) = LOWER(?)', [username]);
|
||||
if (!targetUser) return res.status(404).json({ error: 'User not found on this instance' });
|
||||
|
||||
// Check duplicate (already in invitations or already accepted into calendar)
|
||||
@@ -619,7 +619,7 @@ router.post(['/receive-event', '/calendar-event'], calendarFederationLimiter, as
|
||||
sendCalendarInviteEmail(
|
||||
targetUser.email, targetUser.name, from_user,
|
||||
title, start_time, end_time, description || null,
|
||||
inboxUrl, appName
|
||||
inboxUrl, appName, targetUser.language || 'en'
|
||||
).catch(mailErr => {
|
||||
log.server.warn('Calendar invite mail failed (non-fatal):', mailErr.message);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Router } from 'express';
|
||||
import { Router } from 'express';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { rateLimit } from 'express-rate-limit';
|
||||
import { getDb } from '../config/database.js';
|
||||
@@ -220,14 +220,14 @@ router.post('/receive', federationReceiveLimiter, async (req, res) => {
|
||||
} catch { /* column may not exist on very old installs */ }
|
||||
}
|
||||
|
||||
// Send notification email (truly fire-and-forget – never blocks the response)
|
||||
// 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 inboxUrl = `${appUrl}/federation/inbox`;
|
||||
const appName = process.env.APP_NAME || 'Redlight';
|
||||
sendFederationInviteEmail(
|
||||
targetUser.email, targetUser.name, from_user,
|
||||
room_name, message || null, inboxUrl, appName
|
||||
room_name, message || null, inboxUrl, appName, targetUser.language || 'en'
|
||||
).catch(mailErr => {
|
||||
log.federation.warn('Federation invite mail failed (non-fatal):', mailErr.message);
|
||||
});
|
||||
@@ -559,7 +559,7 @@ router.post('/calendar-event-deleted', federationReceiveLimiter, async (req, res
|
||||
try {
|
||||
// Users with pending/declined invitations
|
||||
const invUsers = await db.all(
|
||||
`SELECT u.email, u.name, ci.title, ci.from_user
|
||||
`SELECT u.email, u.name, u.language, ci.title, ci.from_user
|
||||
FROM calendar_invitations ci
|
||||
JOIN users u ON ci.to_user_id = u.id
|
||||
WHERE ci.event_uid = ? AND ci.from_user LIKE ?`,
|
||||
@@ -567,7 +567,7 @@ router.post('/calendar-event-deleted', federationReceiveLimiter, async (req, res
|
||||
);
|
||||
// Users who already accepted (event in their calendar)
|
||||
const calUsers = await db.all(
|
||||
`SELECT u.email, u.name, ce.title, ce.federated_from AS from_user
|
||||
`SELECT u.email, u.name, u.language, ce.title, ce.federated_from AS from_user
|
||||
FROM calendar_events ce
|
||||
JOIN users u ON ce.user_id = u.id
|
||||
WHERE ce.uid = ? AND ce.federated_from LIKE ?`,
|
||||
@@ -603,7 +603,7 @@ router.post('/calendar-event-deleted', federationReceiveLimiter, async (req, res
|
||||
if (affectedUsers.length > 0) {
|
||||
const appName = process.env.APP_NAME || 'Redlight';
|
||||
for (const u of affectedUsers) {
|
||||
sendCalendarEventDeletedEmail(u.email, u.name, u.from_user, u.title, appName)
|
||||
sendCalendarEventDeletedEmail(u.email, u.name, u.from_user, u.title, appName, u.language || 'en')
|
||||
.catch(mailErr => {
|
||||
log.federation.warn(`Calendar deletion mail to ${u.email} failed (non-fatal): ${mailErr.message}`);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Router } from 'express';
|
||||
import { Router } from 'express';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
@@ -648,7 +648,7 @@ router.post('/:uid/presentation', authenticateToken, async (req, res) => {
|
||||
const room = await db.get('SELECT * FROM rooms WHERE uid = ? AND user_id = ?', [req.params.uid, req.user.id]);
|
||||
if (!room) return res.status(404).json({ error: 'Room not found or no permission' });
|
||||
|
||||
// M16: stream-level size limit – abort as soon as 50 MB is exceeded
|
||||
// M16: stream-level size limit - abort as soon as 50 MB is exceeded
|
||||
const MAX_PRESENTATION_SIZE = 50 * 1024 * 1024;
|
||||
const buffer = await new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
|
||||
Reference in New Issue
Block a user