package authService import ( "context" "fmt" "time" "git.farahty.com/nimer/go-mongo/app" "git.farahty.com/nimer/go-mongo/models" "github.com/redis/go-redis/v9" "go.mongodb.org/mongo-driver/bson" ) const maxAttempts = 5 const lockoutDuration = 15 * time.Minute func Login(ctx context.Context, loginInput *models.LoginInput, redisClient redis.UniversalClient) (*models.LoginResponse, error) { if locked, _ := isLockedOut(loginInput.Identity, redisClient); locked { return nil, fmt.Errorf("too many failed attempts, please try again later") } filter := bson.D{ { Key: "$or", Value: bson.A{ bson.D{{Key: "phone", Value: loginInput.Identity}}, bson.D{{Key: "email", Value: loginInput.Identity}}, }, }, } user, err := app.FindOne[models.User](ctx, "users", filter) if err != nil { logFailedLoginAttempt(ctx, loginInput.Identity, redisClient) // optional return nil, err } if !user.CheckPassword(loginInput.Password) { logFailedLoginAttempt(ctx, loginInput.Identity, redisClient) // optional return nil, fmt.Errorf("invalid identity or password") } clearFailedAttempts(ctx, loginInput.Identity, redisClient) return successLogin(ctx, user) } func isLockedOut(identity string, redisClient redis.UniversalClient) (bool, error) { key := fmt.Sprintf("login:fail:%s", identity) attempts, err := redisClient.Get(context.Background(), key).Int() if err != nil && err != redis.Nil { return false, err } return attempts >= maxAttempts, nil } func logFailedLoginAttempt(ctx context.Context, identity string, redisClient redis.UniversalClient) { key := fmt.Sprintf("login:fail:%s", identity) redisClient.Incr(ctx, key) redisClient.Expire(ctx, key, lockoutDuration) } func clearFailedAttempts(ctx context.Context, identity string, redisClient redis.UniversalClient) { key := fmt.Sprintf("login:fail:%s", identity) redisClient.Del(ctx, key) }