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

@@ -0,0 +1,3 @@
ALTER TABLE users
DROP COLUMN locked_until,
DROP COLUMN failed_login_attempts;

View File

@@ -0,0 +1,3 @@
ALTER TABLE users
ADD COLUMN failed_login_attempts INTEGER NOT NULL DEFAULT 0,
ADD COLUMN locked_until TIMESTAMPTZ;

View File

@@ -6,6 +6,25 @@ RETURNING *;
-- name: GetUserByUsername :one
SELECT * FROM users WHERE username = $1;
-- name: ResetLoginFailures :exec
UPDATE users
SET failed_login_attempts = 0,
locked_until = NULL
WHERE username = $1;
-- name: RecordFailedLogin :one
UPDATE users
SET failed_login_attempts = CASE
WHEN failed_login_attempts + 1 >= 3 THEN 3
ELSE failed_login_attempts + 1
END,
locked_until = CASE
WHEN failed_login_attempts + 1 >= 3 THEN NOW() + INTERVAL '5 minutes'
ELSE locked_until
END
WHERE username = $1
RETURNING failed_login_attempts, locked_until;
-- name: GetDeliveriesByDate :many
SELECT * FROM deliveries WHERE date = $1;

View File

@@ -33,8 +33,10 @@ type Delivery struct {
}
type User struct {
ID pgtype.UUID `db:"id" json:"id"`
Username string `db:"username" json:"username"`
PasswordHash string `db:"password_hash" json:"password_hash"`
CreatedAt pgtype.Timestamp `db:"created_at" json:"created_at"`
ID pgtype.UUID `db:"id" json:"id"`
Username string `db:"username" json:"username"`
PasswordHash string `db:"password_hash" json:"password_hash"`
CreatedAt pgtype.Timestamp `db:"created_at" json:"created_at"`
FailedLoginAttempts int32 `db:"failed_login_attempts" json:"failed_login_attempts"`
LockedUntil pgtype.Timestamptz `db:"locked_until" json:"locked_until"`
}

View File

@@ -18,6 +18,8 @@ type Querier interface {
GetDeliveryByID(ctx context.Context, id pgtype.UUID) (Delivery, error)
GetDeliveryCount(ctx context.Context) ([]GetDeliveryCountRow, error)
GetUserByUsername(ctx context.Context, username string) (User, error)
RecordFailedLogin(ctx context.Context, username string) (RecordFailedLoginRow, error)
ResetLoginFailures(ctx context.Context, username string) error
UpdateDelivery(ctx context.Context, arg UpdateDeliveryParams) error
UpdateDeliveryStatus(ctx context.Context, arg UpdateDeliveryStatusParams) error
}

View File

@@ -91,7 +91,7 @@ func (q *Queries) CreateDelivery(ctx context.Context, arg CreateDeliveryParams)
const createUser = `-- name: CreateUser :one
INSERT INTO users (username, password_hash)
VALUES ($1, $2)
RETURNING id, username, password_hash, created_at
RETURNING id, username, password_hash, created_at, failed_login_attempts, locked_until
`
type CreateUserParams struct {
@@ -107,6 +107,8 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
&i.Username,
&i.PasswordHash,
&i.CreatedAt,
&i.FailedLoginAttempts,
&i.LockedUntil,
)
return i, err
}
@@ -231,7 +233,7 @@ func (q *Queries) GetDeliveryCount(ctx context.Context) ([]GetDeliveryCountRow,
}
const getUserByUsername = `-- name: GetUserByUsername :one
SELECT id, username, password_hash, created_at FROM users WHERE username = $1
SELECT id, username, password_hash, created_at, failed_login_attempts, locked_until FROM users WHERE username = $1
`
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) {
@@ -242,10 +244,50 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User,
&i.Username,
&i.PasswordHash,
&i.CreatedAt,
&i.FailedLoginAttempts,
&i.LockedUntil,
)
return i, err
}
const recordFailedLogin = `-- name: RecordFailedLogin :one
UPDATE users
SET failed_login_attempts = CASE
WHEN failed_login_attempts + 1 >= 3 THEN 3
ELSE failed_login_attempts + 1
END,
locked_until = CASE
WHEN failed_login_attempts + 1 >= 3 THEN NOW() + INTERVAL '5 minutes'
ELSE locked_until
END
WHERE username = $1
RETURNING failed_login_attempts, locked_until
`
type RecordFailedLoginRow struct {
FailedLoginAttempts int32 `db:"failed_login_attempts" json:"failed_login_attempts"`
LockedUntil pgtype.Timestamptz `db:"locked_until" json:"locked_until"`
}
func (q *Queries) RecordFailedLogin(ctx context.Context, username string) (RecordFailedLoginRow, error) {
row := q.db.QueryRow(ctx, recordFailedLogin, username)
var i RecordFailedLoginRow
err := row.Scan(&i.FailedLoginAttempts, &i.LockedUntil)
return i, err
}
const resetLoginFailures = `-- name: ResetLoginFailures :exec
UPDATE users
SET failed_login_attempts = 0,
locked_until = NULL
WHERE username = $1
`
func (q *Queries) ResetLoginFailures(ctx context.Context, username string) error {
_, err := q.db.Exec(ctx, resetLoginFailures, username)
return err
}
const updateDelivery = `-- name: UpdateDelivery :exec
UPDATE deliveries SET
date = $1,