Collect distinct visitor IP addresses

This commit is contained in:
Noah Petherbridge 2024-04-25 18:55:02 -07:00
parent 382c6df96c
commit ff2eb285eb
5 changed files with 114 additions and 0 deletions

View File

@ -46,13 +46,20 @@ func LoginRequired(handler http.Handler) http.Handler {
} }
// Ping LastLoginAt for long lived sessions, but not if impersonated. // Ping LastLoginAt for long lived sessions, but not if impersonated.
var pingLastLoginAt bool
if time.Since(user.LastLoginAt) > config.LastLoginAtCooldown && !session.Impersonated(r) { if time.Since(user.LastLoginAt) > config.LastLoginAtCooldown && !session.Impersonated(r) {
user.LastLoginAt = time.Now() user.LastLoginAt = time.Now()
pingLastLoginAt = true
if err := user.Save(); err != nil { if err := user.Save(); err != nil {
log.Error("LoginRequired: couldn't refresh LastLoginAt for user %s: %s", user.Username, err) 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? // Ask the user for their birthdate?
if AgeGate(user, w, r) { if AgeGate(user, w, r) {
return return
@ -115,6 +122,11 @@ func CertRequired(handler http.Handler) http.Handler {
return 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? // Are they banned?
if currentUser.Status == models.UserStatusBanned { if currentUser.Status == models.UserStatusBanned {
session.LogoutUser(w, r) session.LogoutUser(w, r)

View File

@ -54,6 +54,7 @@ func DeleteUser(user *models.User) error {
{"Profile Fields", DeleteProfile}, {"Profile Fields", DeleteProfile},
{"User Notes", DeleteUserNotes}, {"User Notes", DeleteUserNotes},
{"Change Logs", DeleteChangeLogs}, {"Change Logs", DeleteChangeLogs},
{"IP Addresses", DeleteIPAddresses},
} }
for _, item := range todo { for _, item := range todo {
if err := item.Fn(user.ID); err != nil { if err := item.Fn(user.ID); err != nil {
@ -350,3 +351,13 @@ func DeleteChangeLogs(userID uint64) error {
).Delete(&models.ChangeLog{}) ).Delete(&models.ChangeLog{})
return result.Error 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
}

View File

@ -42,6 +42,7 @@ func ExportModels(zw *zip.Writer, user *models.User) error {
{"UserNote", ExportUserNoteTable}, {"UserNote", ExportUserNoteTable},
{"ChangeLog", ExportChangeLogTable}, {"ChangeLog", ExportChangeLogTable},
{"TwoFactor", ExportTwoFactorTable}, {"TwoFactor", ExportTwoFactorTable},
{"IPAddress", ExportIPAddressTable},
} }
for _, item := range todo { for _, item := range todo {
log.Info("Exporting data model: %s", item.Step) 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) 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)
}

View File

@ -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
}

View File

@ -32,4 +32,5 @@ func AutoMigrate() {
DB.AutoMigrate(&UserNote{}) DB.AutoMigrate(&UserNote{})
DB.AutoMigrate(&TwoFactor{}) DB.AutoMigrate(&TwoFactor{})
DB.AutoMigrate(&ChangeLog{}) DB.AutoMigrate(&ChangeLog{})
DB.AutoMigrate(&IPAddress{})
} }