Adds a layered set of protections around the admin login and the app edge.
Trust proxy (server/src/utils/trustProxy.js)
- Configurable via TRUST_PROXY; pin to the newt agent ("ptero") LAN IP so
X-Forwarded-For is trusted ONLY from that peer. A blanket "true" is
rejected (coerced to 1) to prevent XFF spoofing that would dodge every
IP-based control. DEBUG_TRUST_PROXY logs peer/XFF/req.ip to re-verify the
proxy IP without a redeploy. Documents the Omada static-reservation
assumption.
Login throttling (server/src/middleware/loginProtection.js, rateLimit.js)
- express-slow-down progressive delay + the existing hard rate cap + a
separate per-IP exponential backoff that persists across the rate window.
All failures return one generic message (no user/pass disclosure).
Honeypot (login form + auth.controller)
- Hidden, plausibly-named field ("company"); a filled value fails
generically and is scored as an unambiguous bot.
Optional per-user TOTP 2FA (speakeasy/qrcode)
- totp_secret/totp_enabled columns (+ idempotent migration). Self-service
Account page: enroll via QR, confirm a code to enable, code-gated disable.
- Login is two-step for enrolled users: after the password, a short-lived
signed challenge (stage:'totp', not a session) is required before the
real session is issued.
Bot / scanner scoring + IP ban (server/src/middleware/botScore.js)
- Weighted CMS-scanner paths (this app uses none). Junk paths 404 FIRST,
unconditionally — independent of score/ban state, so a scanner rotating
through fresh Cloudflare IPs gets no free pass. /wp-admin/install.php is
the top-weighted near-1-hit ban (worst offender in prod logs). Per-IP
score with quiet-period decay temp-bans an IP from ALL routes once past a
(deliberately low) threshold, to protect /admin from credential stuffing.
Failed logins and honeypot hits feed the same score.
- Periodic sweep evicts stale, unbanned, quiet entries so the in-memory
store can't grow unbounded; the interval is unref'd and cleared on
graceful shutdown.
Tests: node --test suite (40) covering trust-proxy parsing + live req.ip
(incl. pinned-IP), rate limiter + exponential backoff, honeypot rejection,
TOTP verify (enabled/disabled) + challenge-isn't-a-session, bot-score
threshold/decay/ban + junk-404-independence + install.php + store sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
85 lines
3.5 KiB
JavaScript
85 lines
3.5 KiB
JavaScript
import { Routes, Route, Navigate, Outlet } from 'react-router-dom'
|
|
import { AuthProvider } from './contexts/AuthContext.jsx'
|
|
import { SiteProvider } from './contexts/SiteContext.jsx'
|
|
import MaintenanceGate from './components/MaintenanceGate.jsx'
|
|
import RequireAuth from './components/RequireAuth.jsx'
|
|
|
|
// Public
|
|
import Portal from './routes/public/Portal.jsx'
|
|
import Website from './routes/public/Website.jsx'
|
|
import News from './routes/public/News.jsx'
|
|
import Screenshots from './routes/public/Screenshots.jsx'
|
|
import FiveOnFriday from './routes/public/FiveOnFriday.jsx'
|
|
import Newsletter from './routes/public/Newsletter.jsx'
|
|
import NewsletterIssue from './routes/public/NewsletterIssue.jsx'
|
|
import About from './routes/public/About.jsx'
|
|
import Status from './routes/public/Status.jsx'
|
|
import Wiki from './routes/wiki/Wiki.jsx'
|
|
import WikiArticle from './routes/wiki/WikiArticle.jsx'
|
|
|
|
// Admin
|
|
import AdminLogin from './routes/admin/AdminLogin.jsx'
|
|
import AdminLayout from './routes/admin/AdminLayout.jsx'
|
|
import Dashboard from './routes/admin/views/Dashboard.jsx'
|
|
import PostsAdmin from './routes/admin/views/PostsAdmin.jsx'
|
|
import WikiAdmin from './routes/admin/views/WikiAdmin.jsx'
|
|
import HeroEditor from './routes/admin/views/HeroEditor.jsx'
|
|
import SettingsAdmin from './routes/admin/views/SettingsAdmin.jsx'
|
|
import ActivityAdmin from './routes/admin/views/ActivityAdmin.jsx'
|
|
import UsersAdmin from './routes/admin/views/UsersAdmin.jsx'
|
|
import AccountAdmin from './routes/admin/views/AccountAdmin.jsx'
|
|
|
|
export default function App() {
|
|
return (
|
|
<AuthProvider>
|
|
<SiteProvider>
|
|
<Routes>
|
|
{/* Public site — gated by maintenance mode (admins preview through it) */}
|
|
<Route
|
|
element={
|
|
<MaintenanceGate>
|
|
<Outlet />
|
|
</MaintenanceGate>
|
|
}
|
|
>
|
|
<Route path="/" element={<Portal />} />
|
|
<Route path="/site" element={<Website />} />
|
|
<Route path="/site/news" element={<News />} />
|
|
<Route path="/site/screenshots" element={<Screenshots />} />
|
|
<Route path="/site/five-on-friday" element={<FiveOnFriday />} />
|
|
<Route path="/site/newsletter" element={<Newsletter />} />
|
|
<Route path="/site/newsletter/:id" element={<NewsletterIssue />} />
|
|
<Route path="/site/about" element={<About />} />
|
|
<Route path="/site/status" element={<Status />} />
|
|
<Route path="/wiki" element={<Wiki />} />
|
|
<Route path="/wiki/:slug" element={<WikiArticle />} />
|
|
</Route>
|
|
|
|
{/* Admin */}
|
|
<Route path="/admin/login" element={<AdminLogin />} />
|
|
<Route
|
|
path="/admin"
|
|
element={
|
|
<RequireAuth>
|
|
<AdminLayout />
|
|
</RequireAuth>
|
|
}
|
|
>
|
|
<Route index element={<Dashboard />} />
|
|
<Route path="posts" element={<PostsAdmin />} />
|
|
<Route path="wiki" element={<WikiAdmin />} />
|
|
<Route path="hero" element={<HeroEditor />} />
|
|
<Route path="settings" element={<SettingsAdmin />} />
|
|
<Route path="activity" element={<ActivityAdmin />} />
|
|
<Route path="users" element={<UsersAdmin />} />
|
|
<Route path="account" element={<AccountAdmin />} />
|
|
<Route path="*" element={<Navigate to="/admin" replace />} />
|
|
</Route>
|
|
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
</Routes>
|
|
</SiteProvider>
|
|
</AuthProvider>
|
|
)
|
|
}
|