implement account lockout after 3 failed login attempts with 5-minute cooldown period
Some checks failed
Build and Push Docker Images / build-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (push) Has been cancelled

This commit is contained in:
Egor Pozharov
2026-04-29 17:00:37 +06:00
parent 459b60c9aa
commit a3929bec8d
11 changed files with 150 additions and 12 deletions

View File

@@ -1,6 +1,9 @@
package auth
import "errors"
import (
"errors"
"time"
)
var (
ErrInvalidCredentials = errors.New("invalid credentials")
@@ -10,3 +13,11 @@ var (
ErrCredentialsEmpty = errors.New("username and password cannot be empty")
ErrPasswordTooShort = errors.New("password must be at least 6 characters long")
)
type AccountLockedError struct {
LockedUntil time.Time
}
func (e AccountLockedError) Error() string {
return "account temporarily locked"
}

View File

@@ -1,6 +1,7 @@
package auth
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
@@ -35,6 +36,16 @@ func (h *Handler) Login(c *gin.Context) {
token, err := h.authService.Login(c.Request.Context(), req.Username, req.Password)
if err != nil {
var lockErr AccountLockedError
if errors.As(err, &lockErr) {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Слишком много неверных попыток. Попробуйте через 5 минут",
"code": "account_temporarily_locked",
"locked_until": lockErr.LockedUntil.Format("2006-01-02T15:04:05Z07:00"),
})
return
}
switch err {
case ErrUserNotFound, ErrInvalidCredentials:
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})

View File

@@ -71,10 +71,33 @@ func (s *Service) Login(ctx context.Context, username, password string) (string,
return "", err
}
now := time.Now()
if user.LockedUntil.Valid {
if user.LockedUntil.Time.After(now) {
return "", AccountLockedError{LockedUntil: user.LockedUntil.Time}
}
if err := s.queries.ResetLoginFailures(ctx, username); err != nil {
return "", err
}
}
if !s.VerifyPassword(user.PasswordHash, password) {
failedLogin, err := s.queries.RecordFailedLogin(ctx, username)
if err != nil {
return "", err
}
if failedLogin.LockedUntil.Valid && failedLogin.LockedUntil.Time.After(now) {
return "", AccountLockedError{LockedUntil: failedLogin.LockedUntil.Time}
}
return "", ErrInvalidCredentials
}
if user.FailedLoginAttempts > 0 || user.LockedUntil.Valid {
if err := s.queries.ResetLoginFailures(ctx, username); err != nil {
return "", err
}
}
token, err := GenerateToken(uuid.UUID(user.ID.Bytes), s.secret, s.expiry)
if err != nil {
return "", err