52 lines
1.8 KiB
Go
52 lines
1.8 KiB
Go
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
|
||
|
|
}
|