package account import ( "context" "database/sql" "fmt" "time" "github.com/go-jet/jet/v2/postgres" "github.com/google/uuid" "scrabble/backend/internal/postgres/jet/backend/table" ) // Reasons recorded on a retained_identities row: what detached the credential from its // account. The row is written just before the live identities row is removed, preserving // the legal dossier (which email/vk/tg was linked, and when) even as the identity frees // for reuse. See docs/ARCHITECTURE.md §9.1. const ( retainUnlink = "unlink" retainChange = "change" retainDelete = "delete" ) // retainIdentityTx appends a retention-journal row for one identity being detached, inside // tx. linkedAt is the identity's original creation time; detached_at defaults to now(). It // must run in the same transaction as the identity removal, so the dossier and the live // state can never diverge. func retainIdentityTx(ctx context.Context, tx *sql.Tx, accountID uuid.UUID, kind, externalID string, confirmed bool, linkedAt time.Time, reason string) error { id, err := uuid.NewV7() if err != nil { return fmt.Errorf("account: new retained id: %w", err) } ins := table.RetainedIdentities.INSERT( table.RetainedIdentities.RetainedID, table.RetainedIdentities.AccountID, table.RetainedIdentities.Kind, table.RetainedIdentities.ExternalID, table.RetainedIdentities.Confirmed, table.RetainedIdentities.LinkedAt, table.RetainedIdentities.Reason, ).VALUES(id, accountID, kind, externalID, confirmed, linkedAt, reason) if _, err := ins.ExecContext(ctx, tx); err != nil { return fmt.Errorf("account: retain identity (%s, %s): %w", kind, externalID, err) } return nil } // StampLastLogin records the account's last cold-load time and client IP, but only when // the stored value is missing or older than an hour — so it costs at most one write per // account per hour (its caller, the profile fetch, runs once per cold app-load). It is a // best-effort audit signal that feeds the account-deletion dossier. func (s *Store) StampLastLogin(ctx context.Context, accountID uuid.UUID, ip string) error { now := time.Now().UTC() upd := table.Accounts.UPDATE(table.Accounts.LastLoginAt, table.Accounts.LastLoginIP). SET(postgres.TimestampzT(now), postgres.String(ip)). WHERE( table.Accounts.AccountID.EQ(postgres.UUID(accountID)). AND( table.Accounts.LastLoginAt.IS_NULL(). OR(table.Accounts.LastLoginAt.LT(postgres.TimestampzT(now.Add(-time.Hour)))), ), ) if _, err := upd.ExecContext(ctx, s.db); err != nil { return fmt.Errorf("account: stamp last login %s: %w", accountID, err) } return nil }