diff --git a/cmd/backend/app.go b/cmd/backend/app.go index d9cfa49..f36ac80 100644 --- a/cmd/backend/app.go +++ b/cmd/backend/app.go @@ -122,9 +122,10 @@ func (a *App) Run(p RunParams) { shortlinkRepo = repos.NewShortlinkRepo(sqlDb, tracer) eventRepo = repos.NewEventRepo(kafka) - userCache = cache.NewCacheInmemSharded[models.UserDTO](cache.ShardingTypeInteger) - jwtCache = cache.NewCacheInmemSharded[string](cache.ShardingTypeJWT) - linksCache = cache.NewCacheInmem[string, string]() + userCache = cache.NewCacheInmemSharded[models.UserDTO](cache.ShardingTypeInteger) + loginAttemptsCache = cache.NewCacheInmem[string, int]() + jwtCache = cache.NewCacheInmemSharded[string](cache.ShardingTypeJWT) + linksCache = cache.NewCacheInmem[string, string]() ) // Periodically trigger cache cleanup @@ -140,20 +141,22 @@ func (a *App) Run(p RunParams) { userCache.CheckExpired() jwtCache.CheckExpired() linksCache.CheckExpired() + loginAttemptsCache.CheckExpired() } } }() userService = services.NewUserService( services.UserServiceDeps{ - Jwt: jwtUtil, - Password: passwordUtil, - UserRepo: userRepo, - UserCache: userCache, - JwtCache: jwtCache, - EventRepo: *eventRepo, - ActionTokenRepo: actionTokenRepo, - Logger: logger, + Jwt: jwtUtil, + Password: passwordUtil, + UserRepo: userRepo, + UserCache: userCache, + LoginAttemptsCache: loginAttemptsCache, + JwtCache: jwtCache, + EventRepo: *eventRepo, + ActionTokenRepo: actionTokenRepo, + Logger: logger, }, ) shortlinkService = services.NewShortlinkSevice( diff --git a/internal/core/repos/action_token.go b/internal/core/repos/action_token.go index 7eda5f0..58bf389 100644 --- a/internal/core/repos/action_token.go +++ b/internal/core/repos/action_token.go @@ -26,7 +26,7 @@ type actionTokenRepo struct { func (a *actionTokenRepo) CreateActionToken(ctx context.Context, dto models.ActionTokenDTO) (*models.ActionTokenDTO, error) { query := ` insert into - action_tokens (user_id, value, target, expiration) + action_tokens (user_id, value, target, expires_at) values ($1, $2, $3, $4) returning id;` row := a.db.QueryRowContext(ctx, query, dto.UserId, dto.Value, dto.Target, dto.Expiration) @@ -51,7 +51,7 @@ func (a *actionTokenRepo) GetActionToken(ctx context.Context, value string, targ select id, user_id from action_tokens where value=$1 and target=$2 - and CURRENT_TIMESTAMP < expiration;` + and CURRENT_TIMESTAMP < expires_at;` row := a.db.QueryRowContext(ctx, query, value, target) err := row.Scan(&dto.Id, &dto.UserId) diff --git a/internal/core/repos/user_repo.go b/internal/core/repos/user_repo.go index 748606b..65b3bf8 100644 --- a/internal/core/repos/user_repo.go +++ b/internal/core/repos/user_repo.go @@ -93,7 +93,7 @@ func (u *userRepo) GetUserById(ctx context.Context, id string) (*models.UserDTO, query := ` select id, email, secret, full_name, email_verified - from users where id = $1 and activated;` + from users where id = $1 and active;` row := u.db.QueryRowContext(ctx, query, id) dto := &models.UserDTO{} @@ -113,7 +113,7 @@ func (u *userRepo) GetUserByEmail(ctx context.Context, login string) (*models.Us defer span.End() query := `select id, email, secret, full_name, email_verified - from users where email = $1 and activated;` + from users where email = $1 and active;` row := u.db.QueryRowContext(ctx, query, login) dto := &models.UserDTO{} diff --git a/internal/core/services/user_service.go b/internal/core/services/user_service.go index fffe4bb..dc52eb0 100644 --- a/internal/core/services/user_service.go +++ b/internal/core/services/user_service.go @@ -49,14 +49,15 @@ func NewUserService(deps UserServiceDeps) UserService { } type UserServiceDeps struct { - Jwt utils.JwtUtil - Password utils.PasswordUtil - UserRepo repos.UserRepo - UserCache cache.Cache[string, models.UserDTO] - JwtCache cache.Cache[string, string] - EventRepo repos.EventRepo - ActionTokenRepo repos.ActionTokenRepo - Logger logger.Logger + Jwt utils.JwtUtil + Password utils.PasswordUtil + UserRepo repos.UserRepo + UserCache cache.Cache[string, models.UserDTO] + JwtCache cache.Cache[string, string] + LoginAttemptsCache cache.Cache[string, int] + EventRepo repos.EventRepo + ActionTokenRepo repos.ActionTokenRepo + Logger logger.Logger } type userService struct { @@ -108,6 +109,22 @@ func (u *userService) CreateUser(ctx context.Context, params UserCreateParams) ( } func (u *userService) AuthenticateUser(ctx context.Context, email, password string) (string, error) { + attempts, ok := u.deps.LoginAttemptsCache.Get(email) + if ok && attempts >= 4 { + return "", fmt.Errorf("too many bad login attempts") + } + + token, err := u.authenticateUser(ctx, email, password) + if err != nil { + u.deps.LoginAttemptsCache.Set(email, attempts+1, cache.Expiration{Ttl: 30 * time.Second}) + return "", err + } + + u.deps.LoginAttemptsCache.Del(email) + return token, nil +} + +func (u *userService) authenticateUser(ctx context.Context, email, password string) (string, error) { user, err := u.deps.UserRepo.GetUserByEmail(ctx, email) if err != nil { return "", err diff --git a/sql/01_user.sql b/sql/01_user.sql index 9206ec9..1b38857 100644 --- a/sql/01_user.sql +++ b/sql/01_user.sql @@ -4,11 +4,13 @@ create table if not exists users ( secret varchar(256) not null, full_name varchar(256) not null, email_verified boolean not null default false, - active boolean, + active boolean default true, created_at timestamp, updated_at timestamp ); +alter table users alter column active set default true; + create index if not exists idx_users_email on users(email); create or replace trigger trg_user_created diff --git a/sql/02_shortlinks.sql b/sql/02_shortlinks.sql index 4f6df69..bb0611a 100644 --- a/sql/02_shortlinks.sql +++ b/sql/02_shortlinks.sql @@ -4,14 +4,15 @@ create table if not exists shortlinks ( expires_at timestamp not null, created_at timestamp, updated_at timestamp +); create or replace trigger trg_shortlink_created before insert on shortlinks for each row - when new is distinct from old execute function trg_proc_row_created(); create or replace trigger trg_shortlink_updated before update on shortlinks - for each row when new is distinct from old + for each row + when (new is distinct from old) execute function trg_proc_row_updated(); \ No newline at end of file diff --git a/sql/03_action_token.sql b/sql/03_action_token.sql index e8adf1b..e714816 100644 --- a/sql/03_action_token.sql +++ b/sql/03_action_token.sql @@ -1,26 +1,25 @@ create table if not exists action_tokens ( - id int primary key generated always as identity, + id int generated always as identity, user_id int references users(id), value text not null, target text not null, expires_at timestamp not null, created_at timestamp, - updated_at timestamp - + updated_at timestamp, + constraint pk_action_tokens_id primary key(id), - constraint check chk_action_tokens_target target in ('verify', 'restore') + constraint chk_action_tokens_target check(target in ('verify', 'restore')) ); -create index if not exists idx_action_tokens_value on actions_tokens(value); +create index if not exists idx_action_tokens_value on action_tokens(value); create or replace trigger trg_action_token_created before insert on action_tokens for each row - when new is distinct from old execute function trg_proc_row_created(); create or replace trigger trg_action_token_updated before update on action_tokens for each row - when new is distinct from old + when (new is distinct from old) execute function trg_proc_row_updated(); \ No newline at end of file