package store import ( "context" "database/sql" "time" ) type LoginAttempt struct { FailCount int WindowStart time.Time LockedUntil *time.Time } func (s *Store) GetLoginAttempt(ctx context.Context, ip string) (*LoginAttempt, error) { var failCount int var windowStart string var lockedUntil sql.NullString err := s.db.QueryRowContext(ctx, `SELECT fail_count, window_start, locked_until FROM login_attempts WHERE ip = ?`, ip, ).Scan(&failCount, &windowStart, &lockedUntil) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } out := &LoginAttempt{ FailCount: failCount, WindowStart: parseTime(windowStart), } if lockedUntil.Valid && lockedUntil.String != "" { t := parseTime(lockedUntil.String) out.LockedUntil = &t } return out, nil } func (s *Store) SetLoginAttempt(ctx context.Context, ip string, attempt LoginAttempt) error { var locked string if attempt.LockedUntil != nil { locked = attempt.LockedUntil.Format("2006-01-02 15:04:05") } _, err := s.db.ExecContext(ctx, ` INSERT INTO login_attempts(ip, fail_count, window_start, locked_until) VALUES(?,?,?,?) ON CONFLICT(ip) DO UPDATE SET fail_count = excluded.fail_count, window_start = excluded.window_start, locked_until = excluded.locked_until `, ip, attempt.FailCount, attempt.WindowStart.Format("2006-01-02 15:04:05"), nullIfEmpty(locked)) return err } func (s *Store) ClearLoginAttempt(ctx context.Context, ip string) error { _, err := s.db.ExecContext(ctx, `DELETE FROM login_attempts WHERE ip = ?`, ip) return err } func nullIfEmpty(s string) any { if s == "" { return nil } return s }