Files
apskel-pos-backend/internal/repository/customer_login_attempt_repository.go
T

52 lines
1.8 KiB
Go
Raw Normal View History

package repository
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
const customerLoginAttemptKeyPrefix = "customer_login:attempts:"
// CustomerLoginAttemptRepository counts customer login attempts per phone number, so a
// password cannot be guessed by trying many.
type CustomerLoginAttemptRepository interface {
// Hit counts one attempt for the phone number. It returns the attempts counted in
// the current window, this one included, and how long the window still runs. A
// window starts at the first attempt after the previous one ended.
Hit(ctx context.Context, phoneNumber string, window time.Duration) (int64, time.Duration, error)
// Reset forgets the attempts of the phone number.
Reset(ctx context.Context, phoneNumber string) error
}
type customerLoginAttemptRepository struct {
client *redis.Client
}
func NewCustomerLoginAttemptRepository(client *redis.Client) CustomerLoginAttemptRepository {
return &customerLoginAttemptRepository{client: client}
}
func (r *customerLoginAttemptRepository) Hit(ctx context.Context, phoneNumber string, window time.Duration) (int64, time.Duration, error) {
key := customerLoginAttemptKeyPrefix + phoneNumber
// One transaction, so the key never exists without its expiry: concurrent attempts
// all count, and a crash cannot leave a number locked for good.
pipe := r.client.TxPipeline()
pipe.SetNX(ctx, key, 0, window)
count := pipe.Incr(ctx, key)
left := pipe.PTTL(ctx, key)
if _, err := pipe.Exec(ctx); err != nil {
return 0, 0, fmt.Errorf("count login attempt: %w", err)
}
return count.Val(), left.Val(), nil
}
func (r *customerLoginAttemptRepository) Reset(ctx context.Context, phoneNumber string) error {
if err := r.client.Del(ctx, customerLoginAttemptKeyPrefix+phoneNumber).Err(); err != nil {
return fmt.Errorf("reset login attempts: %w", err)
}
return nil
}