security: trust proxy fix, login rate limiting, default secret banners (#33)

* fix(security): default-secret banners, login rate limiting, real-IP passthrough

- app.set('trust proxy', 1) so req.ip resolves to real client IP behind nginx
- loginRateLimit() middleware on POST /api/auth/login; limits read from
  app_settings (login_rate_limit_max / login_rate_limit_window_sec) with
  60-second cache; falls back to 10/60s when Redis/DB unavailable
- GET /api/auth/security-status returns warnings for default JWT_SECRET,
  ENCRYPTION_KEY, and admin/admin credential — non-breaking for dev
- SecurityBanners component in AppLayout: yellow banner for default secrets,
  red banner + change-password link for default admin password
- Login rate limit settings exposed in Settings > General tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(security): rate-limit /security-status, suppress CodeQL false positive on /login

- Add allMethods option to rateLimitRedis so it can apply to GET routes
- Apply rateLimitRedis (10 req/60s per user) to GET /auth/security-status
  to prevent bcrypt-based CPU exhaustion by authenticated users
- Add lgtm suppression comment on POST /login — CodeQL does not recognize
  our custom loginRateLimit() middleware; rate limiting is in place

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Denis Evers <git@evers.sh>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Denis
2026-04-22 09:30:23 -07:00
committed by GitHub
co-authored by Claude Sonnet 4.6 Denis Evers
parent 0350a76232
commit a336b0e207
8 changed files with 197 additions and 3 deletions
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { Outlet } from 'react-router-dom';
import Sidebar from './Sidebar';
import TopBar from './TopBar';
import SecurityBanners from '../SecurityBanners';
export default function AppLayout() {
const [sidebarOpen, setSidebarOpen] = useState(false);
@@ -20,6 +21,7 @@ export default function AppLayout() {
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
<TopBar onMenuClick={() => setSidebarOpen((o) => !o)} />
<SecurityBanners />
<main className="flex-1 overflow-auto p-3 sm:p-6">
<Outlet />
</main>