feat(account): stamp last-login time + IP on cold app-load
The profile GET (fetched once per cold app-load by the SPA) stamps accounts .last_login_at/.last_login_ip, throttled to at most once per hour per account (best-effort, never blocks the read). IP from the gateway-forwarded X-Forwarded-For. Feeds the account-deletion dossier. Integration test covers the throttle.
This commit is contained in:
@@ -4,6 +4,7 @@ package inttest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -11,6 +12,18 @@ import (
|
||||
"scrabble/backend/internal/account"
|
||||
)
|
||||
|
||||
// lastLoginIP reads an account's stamped last-login IP ("" when unset).
|
||||
func lastLoginIP(t *testing.T, accountID uuid.UUID) string {
|
||||
t.Helper()
|
||||
var ip sql.NullString
|
||||
err := testDB.QueryRowContext(context.Background(),
|
||||
"SELECT last_login_ip FROM accounts WHERE account_id = $1", accountID).Scan(&ip)
|
||||
if err != nil {
|
||||
t.Fatalf("read last_login_ip %s: %v", accountID, err)
|
||||
}
|
||||
return ip.String
|
||||
}
|
||||
|
||||
// retainedRow is one row of the retention journal, read directly for assertions.
|
||||
type retainedRow struct {
|
||||
kind, externalID, reason string
|
||||
@@ -90,3 +103,26 @@ func TestChangeEmailJournalsOldAddress(t *testing.T) {
|
||||
t.Fatalf("retained rows = %+v, want one change email %q", got, oldAddr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStampLastLoginThrottles: the first cold-load stamp writes the IP; a second within
|
||||
// the hour is a no-op (throttled), so it costs at most one write per account per hour.
|
||||
func TestStampLastLoginThrottles(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := account.NewStore(testDB)
|
||||
acc, err := store.ProvisionByIdentity(ctx, account.KindTelegram, "tg-"+uuid.NewString())
|
||||
if err != nil {
|
||||
t.Fatalf("provision: %v", err)
|
||||
}
|
||||
if err := store.StampLastLogin(ctx, acc.ID, "1.2.3.4"); err != nil {
|
||||
t.Fatalf("first stamp: %v", err)
|
||||
}
|
||||
if got := lastLoginIP(t, acc.ID); got != "1.2.3.4" {
|
||||
t.Fatalf("first stamp ip = %q, want 1.2.3.4", got)
|
||||
}
|
||||
if err := store.StampLastLogin(ctx, acc.ID, "9.9.9.9"); err != nil {
|
||||
t.Fatalf("second stamp: %v", err)
|
||||
}
|
||||
if got := lastLoginIP(t, acc.ID); got != "1.2.3.4" {
|
||||
t.Fatalf("throttled ip = %q, want unchanged 1.2.3.4", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user