The server had no authentication: every /api route was open, CORS allowed any origin, and the identity written to the audit log came from the request body — the UI sent a hardcoded pf_user: 'admin', which any client could have set to anything it liked. Accounts live in pf.app_user with scrypt hashes from node's own crypto, so there is no native build step and the parameters travel with each hash. Sessions are express-session over connect-pg-simple in pf.session: a restart no longer signs everyone out, and a session can be revoked by deleting its row, which is how disable-user cuts off access immediately rather than at cookie expiry. Everything under /api except login/logout/me now requires a session, and the React app is mounted only once there is one — its load effects call the API on mount, so a logged-out mount would just fire a burst of 401s. A session that expires while the app is open lands back on the login screen: auth.jsx wraps fetch once rather than teaching every call site to check. Identity is now read from the session for pf_user, created_by and closed_by, and the body values are ignored. Hardened for an internet-facing deployment: trust proxy so req.ip and secure-cookie detection are right behind TLS termination, httpOnly + SameSite=Lax + Secure cookies, ten login failures per IP per fifteen minutes, one error message for unknown, wrong and disabled alike, and a fresh session id on success. CORS is off entirely unless CORS_ORIGIN names an origin — a wildcard alongside a session cookie would be CSRF by construction. The server refuses to boot without SESSION_SECRET rather than falling back to a guessable default. pf.sh grows add-user, passwd, list-users, disable-user and enable-user; passwords are read on stdin and hashed before they reach psql, so no plaintext in argv or shell history. install.sh generates the secret, applies 02_auth.sql, and creates the first account. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
28 lines
862 B
JavaScript
28 lines
862 B
JavaScript
import { StrictMode } from 'react'
|
|
import { createRoot } from 'react-dom/client'
|
|
import { ThemeProvider } from './theme.jsx'
|
|
import { AuthProvider } from './auth.jsx'
|
|
import useAuth from './auth.jsx'
|
|
import Login from './views/Login.jsx'
|
|
import './index.css'
|
|
import App from './App.jsx'
|
|
|
|
// App is mounted only once there is a session — its load effects call /api
|
|
// straight away, and mounting it logged-out would just fire a burst of 401s.
|
|
function Gate() {
|
|
const { user, checking } = useAuth()
|
|
if (checking) return <div className="flex items-center justify-center h-screen text-sm text-gray-400">Loading…</div>
|
|
if (!user) return <Login />
|
|
return <App />
|
|
}
|
|
|
|
createRoot(document.getElementById('root')).render(
|
|
<StrictMode>
|
|
<ThemeProvider>
|
|
<AuthProvider>
|
|
<Gate />
|
|
</AuthProvider>
|
|
</ThemeProvider>
|
|
</StrictMode>,
|
|
)
|