feat: add GeoIP country lookup with embedded DB-IP Lite database (PLAN.md 4.3)

Embeds a DB-IP Lite country MMDB (~5MB) in the binary via go:embed,
keeping the single-binary deployment story clean. Country codes are
stored alongside login attempts and sessions, shown in the dashboard
(Top IPs, Top Countries card, Recent/Active Sessions, session detail).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 15:27:46 +01:00
parent 8fff893d25
commit 94f1f1c266
28 changed files with 353 additions and 71 deletions

View File

@@ -23,17 +23,17 @@ func TestRecordLoginAttempt(t *testing.T) {
ctx := context.Background()
// First attempt creates a new record.
if err := store.RecordLoginAttempt(ctx, "root", "toor", "10.0.0.1"); err != nil {
if err := store.RecordLoginAttempt(ctx, "root", "toor", "10.0.0.1", ""); err != nil {
t.Fatalf("first attempt: %v", err)
}
// Second attempt with same credentials increments count.
if err := store.RecordLoginAttempt(ctx, "root", "toor", "10.0.0.1"); err != nil {
if err := store.RecordLoginAttempt(ctx, "root", "toor", "10.0.0.1", ""); err != nil {
t.Fatalf("second attempt: %v", err)
}
// Different IP is a separate record.
if err := store.RecordLoginAttempt(ctx, "root", "toor", "10.0.0.2"); err != nil {
if err := store.RecordLoginAttempt(ctx, "root", "toor", "10.0.0.2", ""); err != nil {
t.Fatalf("different IP: %v", err)
}
@@ -62,7 +62,7 @@ func TestCreateAndEndSession(t *testing.T) {
store := newTestStore(t)
ctx := context.Background()
id, err := store.CreateSession(ctx, "10.0.0.1", "root", "")
id, err := store.CreateSession(ctx, "10.0.0.1", "root", "", "")
if err != nil {
t.Fatalf("creating session: %v", err)
}
@@ -100,7 +100,7 @@ func TestUpdateHumanScore(t *testing.T) {
store := newTestStore(t)
ctx := context.Background()
id, err := store.CreateSession(ctx, "10.0.0.1", "root", "")
id, err := store.CreateSession(ctx, "10.0.0.1", "root", "", "")
if err != nil {
t.Fatalf("creating session: %v", err)
}
@@ -123,7 +123,7 @@ func TestAppendSessionLog(t *testing.T) {
store := newTestStore(t)
ctx := context.Background()
id, err := store.CreateSession(ctx, "10.0.0.1", "root", "")
id, err := store.CreateSession(ctx, "10.0.0.1", "root", "", "")
if err != nil {
t.Fatalf("creating session: %v", err)
}
@@ -159,7 +159,7 @@ func TestDeleteRecordsBefore(t *testing.T) {
}
// Insert a recent login attempt.
if err := store.RecordLoginAttempt(ctx, "new", "new", "2.2.2.2"); err != nil {
if err := store.RecordLoginAttempt(ctx, "new", "new", "2.2.2.2", ""); err != nil {
t.Fatalf("insert recent attempt: %v", err)
}
@@ -178,7 +178,7 @@ func TestDeleteRecordsBefore(t *testing.T) {
}
// Insert a recent session.
if _, err := store.CreateSession(ctx, "2.2.2.2", "new", ""); err != nil {
if _, err := store.CreateSession(ctx, "2.2.2.2", "new", "", ""); err != nil {
t.Fatalf("insert recent session: %v", err)
}
@@ -214,7 +214,7 @@ func TestNewSQLiteStoreCreatesFile(t *testing.T) {
// Verify we can use the store.
ctx := context.Background()
if err := store.RecordLoginAttempt(ctx, "test", "test", "127.0.0.1"); err != nil {
if err := store.RecordLoginAttempt(ctx, "test", "test", "127.0.0.1", ""); err != nil {
t.Fatalf("recording attempt: %v", err)
}
}