Allow /stats from LAN/private IP ranges

This commit is contained in:
2026-03-26 13:34:33 +01:00
parent c5e9896e65
commit 0ec3222873
3 changed files with 40 additions and 2 deletions

View File

@@ -11,6 +11,7 @@
- `lastHour`: total visits and unique visitors in the last 60 minutes. - `lastHour`: total visits and unique visitors in the last 60 minutes.
- `last5Days`: daily visits and unique visitors for the last 5 UTC calendar days. - `last5Days`: daily visits and unique visitors for the last 5 UTC calendar days.
- Wired visit recording to `GET /api/catalog` so app opens contribute to stats. - Wired visit recording to `GET /api/catalog` so app opens contribute to stats.
- Updated `/stats` access control to allow localhost and LAN/private IP clients, while still blocking public internet IPs.
## 0.1.0 - 2026-03-25 ## 0.1.0 - 2026-03-25
- Created initial frontend-only React + TypeScript project scaffold with Vite. - Created initial frontend-only React + TypeScript project scaffold with Vite.

View File

@@ -26,7 +26,7 @@ A React + TypeScript app with a local Node + SQLite backend for planning what to
- Visits grouped by country - Visits grouped by country
- Last hour totals (`visits`, `uniqueVisitors`) - Last hour totals (`visits`, `uniqueVisitors`)
- Last 5 calendar days (`visits`, `uniqueVisitors` per day) - Last 5 calendar days (`visits`, `uniqueVisitors` per day)
- `/stats` is localhost-only at backend level (non-local requests receive `403`). - `/stats` is restricted at backend level to localhost and LAN/private IP ranges only (public requests receive `403`).
## Project Structure ## Project Structure
- `frontend/`: Vite + React app (`frontend/src`, `frontend/index.html`) - `frontend/`: Vite + React app (`frontend/src`, `frontend/index.html`)

View File

@@ -236,7 +236,44 @@ function getRequestIp(request) {
function isLocalRequest(request) { function isLocalRequest(request) {
const ip = getRequestIp(request); const ip = getRequestIp(request);
return ip === '127.0.0.1' || ip === '::1' || ip === ''; if (!ip || ip === '127.0.0.1' || ip === '::1') {
return true;
}
if (ip.includes(':')) {
const normalized = ip.toLowerCase();
return (
normalized.startsWith('fc') ||
normalized.startsWith('fd') ||
normalized.startsWith('fe80')
);
}
const parts = ip.split('.');
if (parts.length !== 4) {
return false;
}
const octets = parts.map((part) => Number(part));
if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
return false;
}
const [a, b] = octets;
if (a === 10) {
return true;
}
if (a === 172 && b >= 16 && b <= 31) {
return true;
}
if (a === 192 && b === 168) {
return true;
}
if (a === 169 && b === 254) {
return true;
}
return false;
} }
function hashVisitorId(ip, userAgent) { function hashVisitorId(ip, userAgent) {