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:
+59
-9
@@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { rateLimit } from 'express-rate-limit';
|
||||
import { rateLimit, MemoryStore } from 'express-rate-limit';
|
||||
import { RedisStore } from 'rate-limit-redis';
|
||||
import * as OTPAuth from 'otpauth';
|
||||
import { getDb } from '../config/database.js';
|
||||
@@ -23,15 +23,65 @@ if (!process.env.JWT_SECRET) {
|
||||
const JWT_SECRET = process.env.JWT_SECRET;
|
||||
|
||||
// ── Rate Limiting ────────────────────────────────────────────────────────────
|
||||
function makeRedisStore(prefix) {
|
||||
try {
|
||||
return new RedisStore({
|
||||
sendCommand: (...args) => redis.call(...args),
|
||||
prefix,
|
||||
});
|
||||
} catch {
|
||||
return undefined; // falls back to in-memory if Redis unavailable
|
||||
// Shared Redis store with in-memory fallback. Without the fallback, a dead
|
||||
// Redis connection makes every rate-limited endpoint (incl. login) return 500 —
|
||||
// rate limiting must degrade, not take auth down with it.
|
||||
class ResilientStore {
|
||||
constructor(prefix) {
|
||||
try {
|
||||
this.redisStore = new RedisStore({
|
||||
sendCommand: (...args) => redis.call(...args),
|
||||
prefix,
|
||||
});
|
||||
} catch {
|
||||
this.redisStore = null;
|
||||
}
|
||||
this.memoryStore = new MemoryStore();
|
||||
}
|
||||
init(options) {
|
||||
try {
|
||||
// RedisStore.init returns the SCRIPT LOAD promise; catch it so a dead
|
||||
// Redis connection surfaces as a warning instead of an unhandled
|
||||
// rejection that kills the process.
|
||||
const initResult = this.redisStore?.init?.(options);
|
||||
if (typeof initResult?.catch === 'function') {
|
||||
initResult.catch(err => log.auth.warn(`Rate-limit Redis init failed: ${err.message}`));
|
||||
}
|
||||
} catch (err) {
|
||||
log.auth.warn(`Rate-limit Redis init failed: ${err.message}`);
|
||||
}
|
||||
this.memoryStore.init(options);
|
||||
}
|
||||
async increment(key) {
|
||||
if (this.redisStore && redis.status === 'ready') {
|
||||
try {
|
||||
return await this.redisStore.increment(key);
|
||||
} catch (err) {
|
||||
log.auth.warn(`Rate-limit Redis store failed, using in-memory fallback: ${err.message}`);
|
||||
}
|
||||
}
|
||||
return this.memoryStore.increment(key);
|
||||
}
|
||||
async decrement(key) {
|
||||
if (this.redisStore && redis.status === 'ready') {
|
||||
try {
|
||||
return await this.redisStore.decrement(key);
|
||||
} catch { /* fall through to memory */ }
|
||||
}
|
||||
return this.memoryStore.decrement(key);
|
||||
}
|
||||
async resetKey(key) {
|
||||
if (this.redisStore && redis.status === 'ready') {
|
||||
try {
|
||||
return await this.redisStore.resetKey(key);
|
||||
} catch { /* fall through to memory */ }
|
||||
}
|
||||
return this.memoryStore.resetKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
function makeRedisStore(prefix) {
|
||||
return new ResilientStore(prefix);
|
||||
}
|
||||
|
||||
// ── Validation helpers ─────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user