From cdd9627af379bd96df3bbbba6a761741deea7c78 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Fri, 21 Feb 2025 12:08:50 +0300 Subject: [PATCH 1/5] add triggers for users table --- cmd/backend/server/handlers/user_create.go | 12 +++--- internal/core/models/user.go | 6 +-- internal/core/repos/user_repo.go | 24 ++++++------ internal/core/services/user_service.go | 10 ++--- sql/01_user.sql | 44 ++++++++++++++++++---- 5 files changed, 63 insertions(+), 33 deletions(-) diff --git a/cmd/backend/server/handlers/user_create.go b/cmd/backend/server/handlers/user_create.go index a65b78f..fed1762 100644 --- a/cmd/backend/server/handlers/user_create.go +++ b/cmd/backend/server/handlers/user_create.go @@ -16,9 +16,9 @@ type createUserInput struct { } type createUserOutput struct { - Id string `json:"id"` - Email string `json:"email"` - Name string `json:"name"` + Id string `json:"id"` + Email string `json:"email"` + FullName string `json:"fullName"` } func NewUserCreateHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { @@ -37,9 +37,9 @@ func NewUserCreateHandler(log logger.Logger, userService services.UserService) g } return createUserOutput{ - Id: user.Id, - Email: user.Email, - Name: user.Name, + Id: user.Id, + Email: user.Email, + FullName: user.FullName, }, nil }, ) diff --git a/internal/core/models/user.go b/internal/core/models/user.go index dbd6e65..505198f 100644 --- a/internal/core/models/user.go +++ b/internal/core/models/user.go @@ -5,10 +5,10 @@ type UserDTO struct { Email string EmailVerified bool Secret string - Name string + FullName string } type UserUpdateDTO struct { - Secret string - Name string + Secret string + FullName string } diff --git a/internal/core/repos/user_repo.go b/internal/core/repos/user_repo.go index fe4936e..b98af47 100644 --- a/internal/core/repos/user_repo.go +++ b/internal/core/repos/user_repo.go @@ -38,8 +38,8 @@ func (u *userRepo) CreateUser(ctx context.Context, dto models.UserDTO) (*models. _, span := u.tracer.Start(ctx, "postgres::CreateUser") defer span.End() - query := `insert into users (email, secret, name) values ($1, $2, $3) returning id;` - row := u.db.QueryRowContext(ctx, query, dto.Email, dto.Secret, dto.Name) + query := `insert into users (email, secret, full_name) values ($1, $2, $3) returning id;` + row := u.db.QueryRowContext(ctx, query, dto.Email, dto.Secret, dto.FullName) id := "" if err := row.Scan(&id); err != nil { @@ -47,10 +47,10 @@ func (u *userRepo) CreateUser(ctx context.Context, dto models.UserDTO) (*models. } return &models.UserDTO{ - Id: id, - Email: dto.Email, - Secret: dto.Secret, - Name: dto.Name, + Id: id, + Email: dto.Email, + Secret: dto.Secret, + FullName: dto.FullName, }, nil } @@ -58,8 +58,8 @@ func (u *userRepo) UpdateUser(ctx context.Context, userId string, dto models.Use _, span := u.tracer.Start(ctx, "postgres::UpdateUser") defer span.End() - query := `update users set secret=$1, name=$2 where id = $3;` - _, err := u.db.ExecContext(ctx, query, dto.Secret, dto.Name, userId) + query := `update users set secret=$1, full_name=$2 where id = $3;` + _, err := u.db.ExecContext(ctx, query, dto.Secret, dto.FullName, userId) if err != nil { return err } @@ -84,11 +84,11 @@ func (u *userRepo) GetUserById(ctx context.Context, id string) (*models.UserDTO, _, span := u.tracer.Start(ctx, "postgres::GetUserById") defer span.End() - query := `select id, email, secret, name, email_verified from users where id = $1;` + query := `select id, email, secret, full_name, email_verified from users where id = $1;` row := u.db.QueryRowContext(ctx, query, id) dto := &models.UserDTO{} - err := row.Scan(&dto.Id, &dto.Email, &dto.Secret, &dto.Name, &dto.EmailVerified) + err := row.Scan(&dto.Id, &dto.Email, &dto.Secret, &dto.FullName, &dto.EmailVerified) if err == nil { return dto, nil } @@ -103,11 +103,11 @@ func (u *userRepo) GetUserByEmail(ctx context.Context, login string) (*models.Us _, span := u.tracer.Start(ctx, "postgres::GetUserByEmail") defer span.End() - query := `select id, email, secret, name, email_verified from users where email = $1;` + query := `select id, email, secret, full_name, email_verified from users where email = $1;` row := u.db.QueryRowContext(ctx, query, login) dto := &models.UserDTO{} - err := row.Scan(&dto.Id, &dto.Email, &dto.Secret, &dto.Name, &dto.EmailVerified) + err := row.Scan(&dto.Id, &dto.Email, &dto.Secret, &dto.FullName, &dto.EmailVerified) if err == nil { return dto, nil } diff --git a/internal/core/services/user_service.go b/internal/core/services/user_service.go index 6a47cf3..2305726 100644 --- a/internal/core/services/user_service.go +++ b/internal/core/services/user_service.go @@ -84,9 +84,9 @@ func (u *userService) CreateUser(ctx context.Context, params UserCreateParams) ( } user := models.UserDTO{ - Email: params.Email, - Secret: string(secret), - Name: params.Name, + Email: params.Email, + Secret: string(secret), + FullName: params.Name, } result, err := u.deps.UserRepo.CreateUser(ctx, user) @@ -257,8 +257,8 @@ func (u *userService) updatePassword(ctx context.Context, user models.UserDTO, n } if err = u.deps.UserRepo.UpdateUser(ctx, user.Id, models.UserUpdateDTO{ - Secret: newSecret, - Name: user.Name, + Secret: newSecret, + FullName: user.FullName, }); err != nil { return err } diff --git a/sql/01_user.sql b/sql/01_user.sql index 740201d..6d3d73b 100644 --- a/sql/01_user.sql +++ b/sql/01_user.sql @@ -1,11 +1,41 @@ create table if not exists users ( - id int generated always as identity, - email text unique not null, - secret text not null, - name text not null, + id integer primary key generated always as identity, + email varchar(256) unique not null, + secret varchar(256) not null, + full_name varchar(256) not null, email_verified boolean not null default false, - - primary key (id) + created_at timestamp, + updated_at timestamp ); -create index if not exists users_email_idx on users(email); \ No newline at end of file +create index if not exists users_email_idx on users(email); + +create or replace function set_created_at() +returns trigger as $$ +begin + new.created_at = now(); + new.updated_at = now(); + return new; +end; +$$ language plpgsql; + +create or replace trigger on_user_created + before insert on users + for each row + execute function set_created_at(); + +create or replace function set_updated_at() +returns trigger as $$ +begin + if new is distinct from old then + new.updated_at = now(); + end if; + return new; +end; +$$ language plpgsql; + +create or replace trigger on_user_updated + before update on users + for each row + when(new is distinct from old) + execute function set_updated_at(); \ No newline at end of file From 162e7e2f5023fa4f4e475e5fcaafb445bc7b0d3e Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Fri, 21 Feb 2025 16:43:23 +0300 Subject: [PATCH 2/5] convert email to lowercase --- internal/core/services/user_service.go | 3 ++- sql/01_user.sql | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/core/services/user_service.go b/internal/core/services/user_service.go index 2305726..8900a80 100644 --- a/internal/core/services/user_service.go +++ b/internal/core/services/user_service.go @@ -8,6 +8,7 @@ import ( "backend/pkg/logger" "context" "fmt" + "strings" "time" "github.com/google/uuid" @@ -84,7 +85,7 @@ func (u *userService) CreateUser(ctx context.Context, params UserCreateParams) ( } user := models.UserDTO{ - Email: params.Email, + Email: strings.ToLower(params.Email), Secret: string(secret), FullName: params.Name, } diff --git a/sql/01_user.sql b/sql/01_user.sql index 6d3d73b..14b8a43 100644 --- a/sql/01_user.sql +++ b/sql/01_user.sql @@ -8,7 +8,7 @@ create table if not exists users ( updated_at timestamp ); -create index if not exists users_email_idx on users(email); +create index if not exists idx_users_email on users(email); create or replace function set_created_at() returns trigger as $$ @@ -19,7 +19,7 @@ begin end; $$ language plpgsql; -create or replace trigger on_user_created +create or replace trigger trg_user_created before insert on users for each row execute function set_created_at(); @@ -34,7 +34,7 @@ begin end; $$ language plpgsql; -create or replace trigger on_user_updated +create or replace trigger trg_user_updated before update on users for each row when(new is distinct from old) From 3d1fe25dcb00ca040646a021d8d7c8e1f344aa69 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Fri, 21 Feb 2025 17:35:25 +0300 Subject: [PATCH 3/5] improve sql queries and tables --- internal/core/models/action_token.go | 8 ++---- internal/core/repos/shortlink_repo.go | 14 +++++----- internal/core/repos/user_repo.go | 21 +++++++++----- internal/core/services/shortlink_service.go | 8 +++--- internal/core/services/user_service.go | 8 +++--- sql/00_common.sql | 18 ++++++++++++ sql/01_user.sql | 24 ++-------------- sql/02_shortlinks.sql | 20 ++++++++++--- sql/03_action_token.sql | 31 ++++++++++++++++----- 9 files changed, 93 insertions(+), 59 deletions(-) create mode 100644 sql/00_common.sql diff --git a/internal/core/models/action_token.go b/internal/core/models/action_token.go index c86b106..d226a14 100644 --- a/internal/core/models/action_token.go +++ b/internal/core/models/action_token.go @@ -2,13 +2,11 @@ package models import "time" -type ActionTokenTarget int +type ActionTokenTarget string const ( - _ ActionTokenTarget = iota - ActionTokenTargetForgotPassword - ActionTokenTargetLogin2FA - ActionTokenVerifyEmail + ActionTokenTargetRestorePassword ActionTokenTarget = "restore" + ActionTokenTargetVerifyEmail ActionTokenTarget = "verify" ) type ActionTokenDTO struct { diff --git a/internal/core/repos/shortlink_repo.go b/internal/core/repos/shortlink_repo.go index e48c501..2db06ce 100644 --- a/internal/core/repos/shortlink_repo.go +++ b/internal/core/repos/shortlink_repo.go @@ -11,9 +11,9 @@ import ( ) type ShortlinkDTO struct { - Id string - Url string - Expiration time.Time + Id string + Url string + ExpiresAt time.Time } type ShortlinkRepo interface { @@ -35,8 +35,8 @@ func (u *shortlinkRepo) AddShortlink(ctx context.Context, dto ShortlinkDTO) erro _, span := u.tracer.Start(ctx, "postgres::AddShortlink") defer span.End() - query := `insert into shortlinks (id, url, expiration) values ($1, $2, $3);` - _, err := u.db.ExecContext(ctx, query, dto.Id, dto.Url, dto.Expiration) + query := `insert into shortlinks (url, expires_at) values ($1, $2);` + _, err := u.db.ExecContext(ctx, query, dto.Url, dto.ExpiresAt) return err } @@ -44,14 +44,14 @@ func (u *shortlinkRepo) GetShortlink(ctx context.Context, id string) (*Shortlink _, span := u.tracer.Start(ctx, "postgres::GetShortlink") defer span.End() - query := `select url, expiration from shortlinks where id = $1;` + query := `select url, expires_at from shortlinks where id = $1;` row := u.db.QueryRowContext(ctx, query, id) if err := row.Err(); err != nil { return nil, err } dto := &ShortlinkDTO{Id: id} - err := row.Scan(&dto.Url, &dto.Expiration) + err := row.Scan(&dto.Url, &dto.ExpiresAt) if err == nil { return dto, nil } diff --git a/internal/core/repos/user_repo.go b/internal/core/repos/user_repo.go index b98af47..f66ae11 100644 --- a/internal/core/repos/user_repo.go +++ b/internal/core/repos/user_repo.go @@ -10,16 +10,10 @@ import ( "go.opentelemetry.io/otel/trace" ) -// type userDAO struct { -// Id string `json:"id"` -// Login string `json:"login"` -// Secret string `json:"secret"` -// Name string `json:"name"` -// } - type UserRepo interface { CreateUser(ctx context.Context, dto models.UserDTO) (*models.UserDTO, error) UpdateUser(ctx context.Context, userId string, dto models.UserUpdateDTO) error + DeactivateUser(ctx context.Context, userId string) error SetUserEmailVerified(ctx context.Context, userId string) error GetUserById(ctx context.Context, id string) (*models.UserDTO, error) GetUserByEmail(ctx context.Context, login string) (*models.UserDTO, error) @@ -67,6 +61,19 @@ func (u *userRepo) UpdateUser(ctx context.Context, userId string, dto models.Use return nil } +func (u *userRepo) DeactivateUser(ctx context.Context, userId string) error { + _, span := u.tracer.Start(ctx, "postgres::DeactivateUser") + defer span.End() + + query := `update users set active=false where id = $1;` + _, err := u.db.ExecContext(ctx, query, userId) + if err != nil { + return err + } + + return nil +} + func (u *userRepo) SetUserEmailVerified(ctx context.Context, userId string) error { _, span := u.tracer.Start(ctx, "postgres::SetUserEmailVerified") defer span.End() diff --git a/internal/core/services/shortlink_service.go b/internal/core/services/shortlink_service.go index 5228349..1dbdbbb 100644 --- a/internal/core/services/shortlink_service.go +++ b/internal/core/services/shortlink_service.go @@ -48,9 +48,9 @@ func (s *shortlinkService) CreateShortlink(ctx context.Context, url string) (str expiration := time.Now().Add(7 * 24 * time.Hour) dto := repos.ShortlinkDTO{ - Id: id, - Url: url, - Expiration: expiration, + Id: id, + Url: url, + ExpiresAt: expiration, } if err := s.repo.AddShortlink(ctx, dto); err != nil { return "", err @@ -73,7 +73,7 @@ func (s *shortlinkService) GetShortlink(ctx context.Context, id string) (string, if link == nil { return "", ErrShortlinkNotexist } - if time.Now().After(link.Expiration) { + if time.Now().After(link.ExpiresAt) { return "", ErrShortlinkExpired } diff --git a/internal/core/services/user_service.go b/internal/core/services/user_service.go index 8900a80..673b10d 100644 --- a/internal/core/services/user_service.go +++ b/internal/core/services/user_service.go @@ -133,7 +133,7 @@ func (u *userService) AuthenticateUser(ctx context.Context, email, password stri } func (u *userService) VerifyEmail(ctx context.Context, actionToken string) error { - token, err := u.deps.ActionTokenRepo.GetActionToken(ctx, actionToken, models.ActionTokenVerifyEmail) + token, err := u.deps.ActionTokenRepo.GetActionToken(ctx, actionToken, models.ActionTokenTargetVerifyEmail) if err != nil { return err } @@ -162,7 +162,7 @@ func (u *userService) SendEmailForgotPassword(ctx context.Context, email string) models.ActionTokenDTO{ UserId: user.Id, Value: uuid.New().String(), - Target: models.ActionTokenTargetForgotPassword, + Target: models.ActionTokenTargetRestorePassword, Expiration: time.Now().Add(15 * time.Minute), }, ) @@ -179,7 +179,7 @@ func (u *userService) sendEmailVerifyUser(ctx context.Context, userId, email str models.ActionTokenDTO{ UserId: userId, Value: uuid.New().String(), - Target: models.ActionTokenVerifyEmail, + Target: models.ActionTokenTargetVerifyEmail, Expiration: time.Now().Add(1 * time.Hour), }, ) @@ -207,7 +207,7 @@ func (u *userService) SendEmailVerifyUser(ctx context.Context, email string) err } func (u *userService) ChangePasswordWithToken(ctx context.Context, actionToken, newPassword string) error { - token, err := u.deps.ActionTokenRepo.GetActionToken(ctx, actionToken, models.ActionTokenTargetForgotPassword) + token, err := u.deps.ActionTokenRepo.GetActionToken(ctx, actionToken, models.ActionTokenTargetRestorePassword) if err != nil { return err } diff --git a/sql/00_common.sql b/sql/00_common.sql new file mode 100644 index 0000000..c70d27c --- /dev/null +++ b/sql/00_common.sql @@ -0,0 +1,18 @@ +create or replace function trg_proc_row_updated() +returns trigger as $$ +begin + if new is distinct from old then + new.updated_at = now(); + end if; + return new; +end; +$$ language plpgsql; + +create or replace function trg_proc_row_created() +returns trigger as $$ +begin + new.created_at = now(); + new.updated_at = now(); + return new; +end; +$$ language plpgsql; \ No newline at end of file diff --git a/sql/01_user.sql b/sql/01_user.sql index 14b8a43..9206ec9 100644 --- a/sql/01_user.sql +++ b/sql/01_user.sql @@ -4,38 +4,20 @@ 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, created_at timestamp, updated_at timestamp ); create index if not exists idx_users_email on users(email); -create or replace function set_created_at() -returns trigger as $$ -begin - new.created_at = now(); - new.updated_at = now(); - return new; -end; -$$ language plpgsql; - create or replace trigger trg_user_created before insert on users for each row - execute function set_created_at(); - -create or replace function set_updated_at() -returns trigger as $$ -begin - if new is distinct from old then - new.updated_at = now(); - end if; - return new; -end; -$$ language plpgsql; + execute function trg_proc_row_created(); create or replace trigger trg_user_updated before update on users for each row when(new is distinct from old) - execute function set_updated_at(); \ No newline at end of file + execute function trg_proc_row_updated(); \ No newline at end of file diff --git a/sql/02_shortlinks.sql b/sql/02_shortlinks.sql index 92f0699..4f6df69 100644 --- a/sql/02_shortlinks.sql +++ b/sql/02_shortlinks.sql @@ -1,5 +1,17 @@ create table if not exists shortlinks ( - id text primary key, - url text, - expiration date -); \ No newline at end of file + id int generated always as identity, + url text not null, + 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 + 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 697f750..e8adf1b 100644 --- a/sql/03_action_token.sql +++ b/sql/03_action_token.sql @@ -1,9 +1,26 @@ create table if not exists action_tokens ( - id int generated always as identity, - user_id int, - value text, - target int, - expiration timestamp, + id int primary key 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 - primary key(id) -); \ No newline at end of file + constraint pk_action_tokens_id primary key(id), + constraint check chk_action_tokens_target target in ('verify', 'restore') +); + +create index if not exists idx_action_tokens_value on actions_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 + execute function trg_proc_row_updated(); \ No newline at end of file From a27c8669fc0a2a75debff97218fcc47ec48a990d Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Fri, 21 Feb 2025 17:54:09 +0300 Subject: [PATCH 4/5] small user service refactoring --- .../handlers/user_send_restore_password.go | 2 +- .../server/handlers/user_send_verify.go | 2 +- internal/core/repos/user_repo.go | 7 +- internal/core/services/user_service.go | 131 ++++++++++-------- 4 files changed, 78 insertions(+), 64 deletions(-) diff --git a/cmd/backend/server/handlers/user_send_restore_password.go b/cmd/backend/server/handlers/user_send_restore_password.go index a73f849..36f4d85 100644 --- a/cmd/backend/server/handlers/user_send_restore_password.go +++ b/cmd/backend/server/handlers/user_send_restore_password.go @@ -16,7 +16,7 @@ type inputSendRestorePassword struct { func NewUserSendRestorePasswordHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { return httpserver.WrapGin(log, func(ctx context.Context, input inputSendRestorePassword) (interface{}, error) { - err := userService.SendEmailForgotPassword(ctx, input.Email) + err := userService.RequestRestorePassword(ctx, input.Email) return nil, err }, ) diff --git a/cmd/backend/server/handlers/user_send_verify.go b/cmd/backend/server/handlers/user_send_verify.go index 6648b97..0bb5553 100644 --- a/cmd/backend/server/handlers/user_send_verify.go +++ b/cmd/backend/server/handlers/user_send_verify.go @@ -16,7 +16,7 @@ type inputSendVerify struct { func NewUserSendVerifyEmailHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { return httpserver.WrapGin(log, func(ctx context.Context, input inputSendVerify) (interface{}, error) { - err := userService.SendEmailVerifyUser(ctx, input.Email) + err := userService.RequestVerifyUser(ctx, input.Email) return nil, err }, ) diff --git a/internal/core/repos/user_repo.go b/internal/core/repos/user_repo.go index f66ae11..748606b 100644 --- a/internal/core/repos/user_repo.go +++ b/internal/core/repos/user_repo.go @@ -91,7 +91,9 @@ func (u *userRepo) GetUserById(ctx context.Context, id string) (*models.UserDTO, _, span := u.tracer.Start(ctx, "postgres::GetUserById") defer span.End() - query := `select id, email, secret, full_name, email_verified from users where id = $1;` + query := ` + select id, email, secret, full_name, email_verified + from users where id = $1 and activated;` row := u.db.QueryRowContext(ctx, query, id) dto := &models.UserDTO{} @@ -110,7 +112,8 @@ func (u *userRepo) GetUserByEmail(ctx context.Context, login string) (*models.Us _, span := u.tracer.Start(ctx, "postgres::GetUserByEmail") defer span.End() - query := `select id, email, secret, full_name, email_verified from users where email = $1;` + query := `select id, email, secret, full_name, email_verified + from users where email = $1 and activated;` 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 673b10d..fffe4bb 100644 --- a/internal/core/services/user_service.go +++ b/internal/core/services/user_service.go @@ -32,13 +32,16 @@ type UserService interface { CreateUser(ctx context.Context, params UserCreateParams) (*models.UserDTO, error) AuthenticateUser(ctx context.Context, login, password string) (string, error) ValidateAuthToken(ctx context.Context, tokenStr string) (*models.UserDTO, error) + + // TODO: implement user deactivation flow + // DeactivateUser(ctx context.Context, userId string) error + VerifyEmail(ctx context.Context, actionToken string) error - - SendEmailForgotPassword(ctx context.Context, userId string) error - SendEmailVerifyUser(ctx context.Context, email string) error - ChangePassword(ctx context.Context, userId, oldPassword, newPassword string) error ChangePasswordWithToken(ctx context.Context, actionToken, newPassword string) error + + RequestRestorePassword(ctx context.Context, email string) error + RequestVerifyUser(ctx context.Context, email string) error } func NewUserService(deps UserServiceDeps) UserService { @@ -132,6 +135,16 @@ func (u *userService) AuthenticateUser(ctx context.Context, email, password stri return jwt, nil } +func (u *userService) DeactivateUser(ctx context.Context, userId string) error { + err := u.deps.UserRepo.DeactivateUser(ctx, userId) + if err != nil { + return err + } + + u.deps.UserCache.Del(userId) + return nil +} + func (u *userService) VerifyEmail(ctx context.Context, actionToken string) error { token, err := u.deps.ActionTokenRepo.GetActionToken(ctx, actionToken, models.ActionTokenTargetVerifyEmail) if err != nil { @@ -150,62 +163,6 @@ func (u *userService) VerifyEmail(ctx context.Context, actionToken string) error return nil } -func (u *userService) SendEmailForgotPassword(ctx context.Context, email string) error { - // user, err := u.getUserById(ctx, userId) - user, err := u.deps.UserRepo.GetUserByEmail(ctx, email) - if err != nil { - return err - } - - actionToken, err := u.deps.ActionTokenRepo.CreateActionToken( - ctx, - models.ActionTokenDTO{ - UserId: user.Id, - Value: uuid.New().String(), - Target: models.ActionTokenTargetRestorePassword, - Expiration: time.Now().Add(15 * time.Minute), - }, - ) - if err != nil { - return err - } - - return u.deps.EventRepo.SendEmailForgotPassword(ctx, user.Email, actionToken.Value) -} - -func (u *userService) sendEmailVerifyUser(ctx context.Context, userId, email string) error { - actionToken, err := u.deps.ActionTokenRepo.CreateActionToken( - ctx, - models.ActionTokenDTO{ - UserId: userId, - Value: uuid.New().String(), - Target: models.ActionTokenTargetVerifyEmail, - Expiration: time.Now().Add(1 * time.Hour), - }, - ) - if err != nil { - return err - } - - return u.deps.EventRepo.SendEmailVerifyUser(ctx, email, actionToken.Value) -} - -func (u *userService) SendEmailVerifyUser(ctx context.Context, email string) error { - //user, err := u.getUserById(ctx, userId) - user, err := u.deps.UserRepo.GetUserByEmail(ctx, email) - if err != nil { - return err - } - if user == nil { - return fmt.Errorf("no such user") - } - if user.EmailVerified { - return fmt.Errorf("user already verified") - } - - return u.sendEmailVerifyUser(ctx, user.Id, user.Email) -} - func (u *userService) ChangePasswordWithToken(ctx context.Context, actionToken, newPassword string) error { token, err := u.deps.ActionTokenRepo.GetActionToken(ctx, actionToken, models.ActionTokenTargetRestorePassword) if err != nil { @@ -308,3 +265,57 @@ func (u *userService) ValidateAuthToken(ctx context.Context, tokenStr string) (* return user, nil } + +func (u *userService) RequestRestorePassword(ctx context.Context, email string) error { + user, err := u.deps.UserRepo.GetUserByEmail(ctx, email) + if err != nil { + return err + } + + actionToken, err := u.deps.ActionTokenRepo.CreateActionToken( + ctx, + models.ActionTokenDTO{ + UserId: user.Id, + Value: uuid.New().String(), + Target: models.ActionTokenTargetRestorePassword, + Expiration: time.Now().Add(15 * time.Minute), + }, + ) + if err != nil { + return err + } + + return u.deps.EventRepo.SendEmailForgotPassword(ctx, user.Email, actionToken.Value) +} + +func (u *userService) RequestVerifyUser(ctx context.Context, email string) error { + user, err := u.deps.UserRepo.GetUserByEmail(ctx, email) + if err != nil { + return err + } + if user == nil { + return fmt.Errorf("no such user") + } + if user.EmailVerified { + return fmt.Errorf("user already verified") + } + + return u.sendEmailVerifyUser(ctx, user.Id, user.Email) +} + +func (u *userService) sendEmailVerifyUser(ctx context.Context, userId, email string) error { + actionToken, err := u.deps.ActionTokenRepo.CreateActionToken( + ctx, + models.ActionTokenDTO{ + UserId: userId, + Value: uuid.New().String(), + Target: models.ActionTokenTargetVerifyEmail, + Expiration: time.Now().Add(1 * time.Hour), + }, + ) + if err != nil { + return err + } + + return u.deps.EventRepo.SendEmailVerifyUser(ctx, email, actionToken.Value) +} From 00b8636b5a4e8ae718560a5b6da4df1c2f1b7e68 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Sat, 22 Feb 2025 12:37:36 +0300 Subject: [PATCH 5/5] add ban by login attempts and fix sql --- cmd/backend/app.go | 25 ++++++++++--------- internal/core/repos/action_token.go | 4 ++-- internal/core/repos/user_repo.go | 4 ++-- internal/core/services/user_service.go | 33 +++++++++++++++++++------- sql/01_user.sql | 4 +++- sql/02_shortlinks.sql | 5 ++-- sql/03_action_token.sql | 13 +++++----- 7 files changed, 55 insertions(+), 33 deletions(-) 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