Harden server security, rework landing page and refresh branding
Build & Push Docker Image / build (push) Successful in 4m3s
Build & Push Docker Image / build (push) Successful in 4m3s
Security: - rooms: rate-limit /invite-email (SMTP spam relay), validate share target user exists, guard timingSafeEqual against length mismatch in the presentation route (500 -> 403) - analytics: verify callback token before parsing the 5mb body so unauthenticated callers cannot buffer large payloads - caldav: rate-limit failed Basic-Auth attempts (token brute force), lowercase email lookup, case-insensitive principal check - auth: fall back to the in-memory rate-limit store when Redis is unavailable; previously every rate-limited endpoint (incl. login) returned 500 when the Redis connection was down UI/copy: - Home: factual hero copy and feature cards (6 instead of 9), fix double-rendered feature icon, remove fake stats row and pill badge; keep the background gradient and card layout - i18n: consistent informal tone, drop trailing exclamation marks from status toasts, remove emoji from transactional emails - new favicon (logo.svg), restore theme-based default brand logo Chore: - gitignore SQLite WAL/SHM files Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+16
-2
@@ -12,12 +12,25 @@
|
||||
|
||||
import { Router, text } from 'express';
|
||||
import crypto from 'crypto';
|
||||
import { rateLimit } from 'express-rate-limit';
|
||||
import { getDb } from '../config/database.js';
|
||||
import { log } from '../config/logger.js';
|
||||
import { getBaseUrl } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Brute-force protection for the Basic-Auth tokens: only failed requests count
|
||||
// (skipSuccessfulRequests), so legitimate sync clients polling frequently are
|
||||
// not throttled.
|
||||
router.use(rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 30,
|
||||
skipSuccessfulRequests: true,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: 'Too many failed CalDAV requests. Please try again later.',
|
||||
}));
|
||||
|
||||
// ── Body parsing for XML and iCalendar payloads ────────────────────────────
|
||||
router.use(text({ type: ['application/xml', 'text/xml', 'text/calendar', 'application/octet-stream'] }));
|
||||
|
||||
@@ -194,9 +207,10 @@ async function caldavAuth(req, res, next) {
|
||||
const token = decoded.slice(colonIdx + 1);
|
||||
|
||||
const db = getDb();
|
||||
// Emails are stored lowercased
|
||||
const user = await db.get(
|
||||
'SELECT id, name, display_name, email FROM users WHERE email = ?',
|
||||
[email],
|
||||
[email.toLowerCase()],
|
||||
);
|
||||
if (!user) {
|
||||
res.set('WWW-Authenticate', 'Basic realm="Redlight CalDAV"');
|
||||
@@ -243,7 +257,7 @@ function setDAVHeaders(res) {
|
||||
// ── CalDAV username authorization ──────────────────────────────────────────
|
||||
// Ensures the :username param matches the authenticated user's email
|
||||
function validateCalDAVUser(req, res, next) {
|
||||
if (req.params.username && decodeURIComponent(req.params.username) !== req.caldavUser.email) {
|
||||
if (req.params.username && decodeURIComponent(req.params.username).toLowerCase() !== req.caldavUser.email) {
|
||||
return res.status(403).end();
|
||||
}
|
||||
next();
|
||||
|
||||
Reference in New Issue
Block a user