Admin geography (#33): offline IP→country + login heatmap (DB-IP Lite, vendored maxminddb) #37

Merged
Cordy merged 6 commits from feat/admin-geoip-33 into main 2026-08-25 18:40:55 +00:00
Owner

Lights up the staff-console Geography tab with real login data, looked up in-process against a local .mmdb — no request-time network, nothing leaves the VPS (ADR-0002).

What it does

  • initGeoIP opens a country .mmdb at boot from GEOIP_DB; unset/missing ⇒ disabled and harmless (country = "").
  • On register and login, the server records a login event tagged with the caller's country.
  • GET /v1/admin/heatmap aggregates those events by country (logins + distinct accounts) for the selected window.
  • Client IP comes from X-Forwarded-Fortrustworthy here because the server binds 127.0.0.1:7421 and only Caddy can reach it, so no external caller can spoof it.
  • Country names are normalised to the Natural Earth spellings the admin map (world-atlas) matches on (US → "United States of America", CZ → "Czechia", etc.).

Why server-side + DB-IP Lite

Chosen over a Caddy-edge lookup because the backend is loopback-only behind the stock apt Caddy (so XFF is trustworthy and we avoid replacing Caddy with a custom xcaddy build), and over MaxMind GeoLite2 because DB-IP Lite has no license key / 90-day expiry / geoipupdate silent-failure risk. The .mmdb is placed on the VPS separately (install-geoip op) and refreshed monthly.

No frontend change

admin.html's vGeo already calls /v1/admin/heatmap and only falls back to fixtures on error, so registering the endpoint switches Geography to live data automatically.

Dependency

github.com/oschwald/maxminddb-golang@v1.12.0, vendored so builds stay hermetic (no proxy.golang.org at build time). Second exception to stdlib-only after modernc.org/sqlite.

Tests

geoip_test.go: XFF/RemoteAddr parsing, nil-db safety, name normalisation, and heatmap aggregation (Switzerland {2,2}, Germany {1,1}) + staff-gating.

Still pending in #5

The dedicated Funnel tab still needs the small admin.html division guard before /v1/admin/funnel can be registered; that's the last piece.

Lights up the staff-console Geography tab with real login data, looked up in-process against a local `.mmdb` — no request-time network, nothing leaves the VPS (ADR-0002). ## What it does - `initGeoIP` opens a country `.mmdb` at boot from `GEOIP_DB`; unset/missing ⇒ disabled and harmless (country = ""). - On register and login, the server records a `login` event tagged with the caller's country. - `GET /v1/admin/heatmap` aggregates those events by country (logins + distinct accounts) for the selected window. - Client IP comes from `X-Forwarded-For` — **trustworthy here** because the server binds `127.0.0.1:7421` and only Caddy can reach it, so no external caller can spoof it. - Country names are normalised to the Natural Earth spellings the admin map (world-atlas) matches on (US → "United States of America", CZ → "Czechia", etc.). ## Why server-side + DB-IP Lite Chosen over a Caddy-edge lookup because the backend is loopback-only behind the *stock apt* Caddy (so XFF is trustworthy and we avoid replacing Caddy with a custom `xcaddy` build), and over MaxMind GeoLite2 because DB-IP Lite has no license key / 90-day expiry / `geoipupdate` silent-failure risk. The `.mmdb` is placed on the VPS separately (`install-geoip` op) and refreshed monthly. ## No frontend change admin.html's `vGeo` already calls `/v1/admin/heatmap` and only falls back to fixtures on error, so registering the endpoint switches Geography to live data automatically. ## Dependency `github.com/oschwald/maxminddb-golang@v1.12.0`, **vendored** so builds stay hermetic (no `proxy.golang.org` at build time). Second exception to stdlib-only after `modernc.org/sqlite`. ## Tests `geoip_test.go`: XFF/RemoteAddr parsing, nil-db safety, name normalisation, and heatmap aggregation (Switzerland {2,2}, Germany {1,1}) + staff-gating. ## Still pending in #5 The dedicated **Funnel** tab still needs the small admin.html division guard before `/v1/admin/funnel` can be registered; that's the last piece.
Server-side lookup against a local DB-IP Lite (or GeoLite2) .mmdb, path from
GEOIP_DB; disabled and harmless when unset. Client IP comes from X-Forwarded-For,
which is trustworthy here because the server binds 127.0.0.1:7421 and only Caddy
can reach it. Country name is normalised to the Natural Earth spellings the
admin map (world-atlas) uses. Nothing leaves the VPS — the .mmdb is local and
lookups are in-process (ADR-0002).
geoip: wire initGeoIP + heatmap route + login events; vendor maxminddb (#33)
All checks were successful
ci / test-and-build (pull_request) Successful in 1m12s
d1abffbffe
one-shot: drop vendor/ (x/sys bloat), keep go.mod/go.sum — self-removing
All checks were successful
ci / test-and-build (pull_request) Successful in 1m7s
06d6d83d48
Cordy merged commit c6c76622e8 into main 2026-08-25 18:40:55 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/cairn-license-server#37
No description provided.