From ff2eb285ebeaec9d4ea99a0667f2c312a571e377 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Thu, 25 Apr 2024 18:55:02 -0700 Subject: [PATCH] Collect distinct visitor IP addresses --- pkg/middleware/authentication.go | 12 +++++ pkg/models/deletion/delete_user.go | 11 +++++ pkg/models/exporting/models.go | 16 +++++++ pkg/models/ip_addresses.go | 74 ++++++++++++++++++++++++++++++ pkg/models/models.go | 1 + 5 files changed, 114 insertions(+) create mode 100644 pkg/models/ip_addresses.go diff --git a/pkg/middleware/authentication.go b/pkg/middleware/authentication.go index f02d96b..6c33f5f 100644 --- a/pkg/middleware/authentication.go +++ b/pkg/middleware/authentication.go @@ -46,13 +46,20 @@ func LoginRequired(handler http.Handler) http.Handler { } // Ping LastLoginAt for long lived sessions, but not if impersonated. + var pingLastLoginAt bool if time.Since(user.LastLoginAt) > config.LastLoginAtCooldown && !session.Impersonated(r) { user.LastLoginAt = time.Now() + pingLastLoginAt = true if err := user.Save(); err != nil { log.Error("LoginRequired: couldn't refresh LastLoginAt for user %s: %s", user.Username, err) } } + // Log the last visit of their current IP address. + if err := models.PingIPAddress(r, user, pingLastLoginAt); err != nil { + log.Error("LoginRequired: couldn't ping user %s IP address: %s", user.Username, err) + } + // Ask the user for their birthdate? if AgeGate(user, w, r) { return @@ -115,6 +122,11 @@ func CertRequired(handler http.Handler) http.Handler { return } + // Log the last visit of their current IP address. + if err := models.PingIPAddress(r, currentUser, false); err != nil { + log.Error("CertRequired: couldn't ping user %s IP address: %s", currentUser.Username, err) + } + // Are they banned? if currentUser.Status == models.UserStatusBanned { session.LogoutUser(w, r) diff --git a/pkg/models/deletion/delete_user.go b/pkg/models/deletion/delete_user.go index 1654624..771ea83 100644 --- a/pkg/models/deletion/delete_user.go +++ b/pkg/models/deletion/delete_user.go @@ -54,6 +54,7 @@ func DeleteUser(user *models.User) error { {"Profile Fields", DeleteProfile}, {"User Notes", DeleteUserNotes}, {"Change Logs", DeleteChangeLogs}, + {"IP Addresses", DeleteIPAddresses}, } for _, item := range todo { if err := item.Fn(user.ID); err != nil { @@ -350,3 +351,13 @@ func DeleteChangeLogs(userID uint64) error { ).Delete(&models.ChangeLog{}) return result.Error } + +// DeleteIPAddresses scrubs data for deleting a user. +func DeleteIPAddresses(userID uint64) error { + log.Error("DeleteUser: DeleteIPAddresses(%d)", userID) + result := models.DB.Where( + "user_id = ?", + userID, + ).Delete(&models.IPAddress{}) + return result.Error +} diff --git a/pkg/models/exporting/models.go b/pkg/models/exporting/models.go index c80389a..15fd80c 100644 --- a/pkg/models/exporting/models.go +++ b/pkg/models/exporting/models.go @@ -42,6 +42,7 @@ func ExportModels(zw *zip.Writer, user *models.User) error { {"UserNote", ExportUserNoteTable}, {"ChangeLog", ExportChangeLogTable}, {"TwoFactor", ExportTwoFactorTable}, + {"IPAddress", ExportIPAddressTable}, } for _, item := range todo { log.Info("Exporting data model: %s", item.Step) @@ -428,3 +429,18 @@ func ExportTwoFactorTable(zw *zip.Writer, user *models.User) error { return ZipJson(zw, "two_factor.json", items) } + +func ExportIPAddressTable(zw *zip.Writer, user *models.User) error { + var ( + items = []*models.IPAddress{} + query = models.DB.Model(&models.IPAddress{}).Where( + "user_id = ?", + user.ID, + ).Find(&items) + ) + if query.Error != nil { + return query.Error + } + + return ZipJson(zw, "ip_addresses.json", items) +} diff --git a/pkg/models/ip_addresses.go b/pkg/models/ip_addresses.go new file mode 100644 index 0000000..540b330 --- /dev/null +++ b/pkg/models/ip_addresses.go @@ -0,0 +1,74 @@ +package models + +import ( + "net/http" + "time" + + "code.nonshy.com/nonshy/website/pkg/log" + "code.nonshy.com/nonshy/website/pkg/utility" +) + +// IPAddress table to log which networks users have logged in from. +type IPAddress struct { + ID uint64 `gorm:"primaryKey"` + UserID uint64 `gorm:"index"` + IPAddress string `gorm:"index"` + NumberVisits uint64 // count of times their LastLoginAt pinged from this address + CreatedAt time.Time // first time seen + UpdatedAt time.Time // last time seen +} + +// PingIPAddress logs or upserts the user's current IP address into the IPAddress table. +func PingIPAddress(r *http.Request, user *User, incrementVisit bool) error { + var ( + addr = utility.IPAddress(r) + ip *IPAddress + ) + + // Have we seen it before? + ip, err := LoadUserIPAddress(user, addr) + if err != nil { + // Insert it. + log.Debug("User %s IP %s seen for the first time", user.Username, addr) + ip = &IPAddress{ + UserID: user.ID, + IPAddress: addr, + CreatedAt: time.Now(), + } + + result := DB.Create(ip) + if result.Error != nil { + return result.Error + } + } + + // Are we refreshing the NumberVisits count? Note: this happens each + // time the main website will refresh the user LastLoginAt. + if incrementVisit || ip.NumberVisits == 0 { + ip.NumberVisits++ + } + + // Ping the update. + ip.UpdatedAt = time.Now() + return ip.Save() +} + +func LoadUserIPAddress(user *User, ipAddr string) (*IPAddress, error) { + var ip = &IPAddress{} + var result = DB.Model(&IPAddress{}).Where( + "user_id = ? AND ip_address = ?", + user.ID, ipAddr, + ).First(&ip) + return ip, result.Error +} + +// Save photo. +func (ip *IPAddress) Save() error { + result := DB.Save(ip) + return result.Error +} + +// Delete the DB entry. +func (ip *IPAddress) Delete() error { + return DB.Delete(ip).Error +} diff --git a/pkg/models/models.go b/pkg/models/models.go index 197a248..6647cc1 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -32,4 +32,5 @@ func AutoMigrate() { DB.AutoMigrate(&UserNote{}) DB.AutoMigrate(&TwoFactor{}) DB.AutoMigrate(&ChangeLog{}) + DB.AutoMigrate(&IPAddress{}) }