Allow /stats from LAN/private IP ranges
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user