From 59e76a4ec18c9bc7f3f51eb00f543578ec560fe7 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Wed, 31 Jul 2024 08:02:10 +0300 Subject: [PATCH 1/4] add confirmation codes --- src/handlers/user_create_handler.go | 8 +-- src/models/action_token.go | 15 +++++ src/models/user.go | 7 ++- src/repo/action_token.go | 9 +++ src/repo/email_repo.go | 5 ++ src/repo/user_repo.go | 29 ++++++--- src/services/user_service.go | 93 ++++++++++++++++++++++++++--- 7 files changed, 143 insertions(+), 23 deletions(-) create mode 100644 src/models/action_token.go create mode 100644 src/repo/action_token.go create mode 100644 src/repo/email_repo.go diff --git a/src/handlers/user_create_handler.go b/src/handlers/user_create_handler.go index 3ab2269..b50f336 100644 --- a/src/handlers/user_create_handler.go +++ b/src/handlers/user_create_handler.go @@ -8,14 +8,14 @@ import ( ) type createUserInput struct { - Login string + Email string Password string Name string } type createUserOutput struct { Id string `json:"id"` - Login string `json:"login"` + Email string `json:"email"` Name string `json:"name"` } @@ -28,7 +28,7 @@ func NewUserCreateHandler(userService services.UserService) gin.HandlerFunc { } dto, err := userService.CreateUser(ctx, services.UserCreateParams{ - Login: params.Login, + Email: params.Email, Password: params.Password, Name: params.Name, }) @@ -43,7 +43,7 @@ func NewUserCreateHandler(userService services.UserService) gin.HandlerFunc { resultBody, err := json.Marshal(createUserOutput{ Id: dto.Id, - Login: dto.Login, + Email: dto.Email, Name: dto.Name, }) if err != nil { diff --git a/src/models/action_token.go b/src/models/action_token.go new file mode 100644 index 0000000..a8b01d5 --- /dev/null +++ b/src/models/action_token.go @@ -0,0 +1,15 @@ +package models + +type ActionTokenTarget int + +const ( + ActionTokenTargetForgotPassword ActionTokenTarget = iota + ActionTokenTargetLogin2FA +) + +type ActionTokenDTO struct { + Id string + UserId string + Value string + Target ActionTokenTarget +} diff --git a/src/models/user.go b/src/models/user.go index 1deb356..ced8300 100644 --- a/src/models/user.go +++ b/src/models/user.go @@ -2,7 +2,12 @@ package models type UserDTO struct { Id string - Login string + Email string + Secret string + Name string +} + +type UserUpdateDTO struct { Secret string Name string } diff --git a/src/repo/action_token.go b/src/repo/action_token.go new file mode 100644 index 0000000..8e4d659 --- /dev/null +++ b/src/repo/action_token.go @@ -0,0 +1,9 @@ +package repo + +import "backend/src/models" + +type ActionTokenRepo interface { + CreateActionToken(actionToken models.ActionTokenDTO) (*models.ActionTokenDTO, error) + FindActionToken(userId, val string, target models.ActionTokenTarget) (*models.ActionTokenDTO, error) + DeleteActionToken(id string) error +} diff --git a/src/repo/email_repo.go b/src/repo/email_repo.go new file mode 100644 index 0000000..3b8c256 --- /dev/null +++ b/src/repo/email_repo.go @@ -0,0 +1,5 @@ +package repo + +type EmailRepo interface { + SendEmailForgotPassword(email, token string) +} diff --git a/src/repo/user_repo.go b/src/repo/user_repo.go index a7d4e61..c2651bd 100644 --- a/src/repo/user_repo.go +++ b/src/repo/user_repo.go @@ -16,8 +16,9 @@ import ( type UserRepo interface { CreateUser(ctx context.Context, dto models.UserDTO) (*models.UserDTO, error) + UpdateUser(ctx context.Context, userId string, dto models.UserUpdateDTO) error GetUserById(ctx context.Context, id string) (*models.UserDTO, error) - GetUserByLogin(ctx context.Context, login string) (*models.UserDTO, error) + GetUserByEmail(ctx context.Context, login string) (*models.UserDTO, error) } func NewUserRepo(db *sql.DB) UserRepo { @@ -29,8 +30,8 @@ type userRepo struct { } func (u *userRepo) CreateUser(ctx context.Context, dto models.UserDTO) (*models.UserDTO, error) { - query := `insert into users (login, secret, name) values ($1, $2, $3) returning id;` - row := u.db.QueryRowContext(ctx, query, dto.Login, dto.Secret, dto.Name) + 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) id := "" if err := row.Scan(&id); err != nil { @@ -39,18 +40,28 @@ func (u *userRepo) CreateUser(ctx context.Context, dto models.UserDTO) (*models. return &models.UserDTO{ Id: id, - Login: dto.Login, + Email: dto.Email, Secret: dto.Secret, Name: dto.Name, }, nil } +func (u *userRepo) UpdateUser(ctx context.Context, userId string, dto models.UserUpdateDTO) error { + query := `update users set secret=$1, name=$2 where id = $3;` + _, err := u.db.ExecContext(ctx, query, dto.Secret, dto.Name, userId) + if err != nil { + return err + } + + return nil +} + func (u *userRepo) GetUserById(ctx context.Context, id string) (*models.UserDTO, error) { - query := `select id, login, secret, name from users where id = $1;` + query := `select id, email, secret, name from users where id = $1;` row := u.db.QueryRowContext(ctx, query, id) dto := &models.UserDTO{} - err := row.Scan(&dto.Id, &dto.Login, &dto.Secret, &dto.Name) + err := row.Scan(&dto.Id, &dto.Email, &dto.Secret, &dto.Name) if err == nil { return dto, nil } @@ -61,12 +72,12 @@ func (u *userRepo) GetUserById(ctx context.Context, id string) (*models.UserDTO, return nil, err } -func (u *userRepo) GetUserByLogin(ctx context.Context, login string) (*models.UserDTO, error) { - query := `select id, login, secret, name from users where login = $1;` +func (u *userRepo) GetUserByEmail(ctx context.Context, login string) (*models.UserDTO, error) { + query := `select id, email, secret, name from users where email = $1;` row := u.db.QueryRowContext(ctx, query, login) dto := &models.UserDTO{} - err := row.Scan(&dto.Id, &dto.Login, &dto.Secret, &dto.Name) + err := row.Scan(&dto.Id, &dto.Email, &dto.Secret, &dto.Name) if err == nil { return dto, nil } diff --git a/src/services/user_service.go b/src/services/user_service.go index 8c8a6ce..6e11d62 100644 --- a/src/services/user_service.go +++ b/src/services/user_service.go @@ -6,6 +6,8 @@ import ( "backend/src/utils" "context" "fmt" + + "github.com/google/uuid" ) var ( @@ -28,10 +30,12 @@ func NewUserService(deps UserServiceDeps) UserService { } type UserServiceDeps struct { - Jwt utils.JwtUtil - Password utils.PasswordUtil - UserRepo repo.UserRepo - UserCache repo.Cache[string, models.UserDTO] + Jwt utils.JwtUtil + Password utils.PasswordUtil + UserRepo repo.UserRepo + UserCache repo.Cache[string, models.UserDTO] + EmailRepo repo.EmailRepo + ActionTokenRepo repo.ActionTokenRepo } type userService struct { @@ -39,13 +43,13 @@ type userService struct { } type UserCreateParams struct { - Login string + Email string Password string Name string } func (u *userService) CreateUser(ctx context.Context, params UserCreateParams) (*models.UserDTO, error) { - exisitngUser, err := u.deps.UserRepo.GetUserByLogin(ctx, params.Login) + exisitngUser, err := u.deps.UserRepo.GetUserByEmail(ctx, params.Email) if err != nil { return nil, err } @@ -63,7 +67,7 @@ func (u *userService) CreateUser(ctx context.Context, params UserCreateParams) ( } user := models.UserDTO{ - Login: params.Login, + Email: params.Email, Secret: string(secret), Name: params.Name, } @@ -78,8 +82,8 @@ func (u *userService) CreateUser(ctx context.Context, params UserCreateParams) ( return result, nil } -func (u *userService) AuthenticateUser(ctx context.Context, login, password string) (string, error) { - user, err := u.deps.UserRepo.GetUserByLogin(ctx, login) +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 } @@ -102,6 +106,77 @@ func (u *userService) AuthenticateUser(ctx context.Context, login, password stri return jwt, nil } +func (u *userService) HelpPasswordForgot(ctx context.Context, userId string) error { + user, err := u.deps.UserRepo.GetUserById(ctx, userId) + if err != nil { + return err + } + + actionToken, err := u.deps.ActionTokenRepo.CreateActionToken(models.ActionTokenDTO{ + UserId: user.Id, + Value: uuid.New().String(), + Target: models.ActionTokenTargetForgotPassword, + }) + if err != nil { + return err + } + + u.deps.EmailRepo.SendEmailForgotPassword(user.Email, actionToken.Value) + return nil +} + +func (u *userService) ChangePasswordForgot(ctx context.Context, userId, newPassword, accessCode string) error { + user, err := u.deps.UserRepo.GetUserById(ctx, userId) + if err != nil { + return err + } + + code, err := u.deps.ActionTokenRepo.FindActionToken(userId, accessCode, models.ActionTokenTargetForgotPassword) + if err != nil { + return err + } + if code == nil { + return fmt.Errorf("wrong user access code") + } + + if err := u.deps.ActionTokenRepo.DeleteActionToken(code.Id); err != nil { + return fmt.Errorf("internal error occured: %w", err) + } + + return u.updatePassword(ctx, *user, newPassword) +} + +func (u *userService) ChangePassword(ctx context.Context, userId, oldPassword, newPassword string) error { + user, err := u.getUserById(ctx, userId) + if err != nil { + return err + } + + if !u.deps.Password.Compare(oldPassword, user.Secret) { + return ErrUserWrongPassword + } + + return u.updatePassword(ctx, *user, newPassword) +} + +func (u *userService) updatePassword(ctx context.Context, user models.UserDTO, newPassword string) error { + if err := u.deps.Password.Validate(newPassword); err != nil { + return ErrUserBadPassword + } + + u.deps.UserCache.Del(user.Id) + + newSecret, err := u.deps.Password.Hash(newPassword) + if err != nil { + return err + } + + return u.deps.UserRepo.UpdateUser(ctx, user.Id, models.UserUpdateDTO{ + Secret: newSecret, + Name: user.Name, + }) +} + func (u *userService) getUserById(ctx context.Context, userId string) (*models.UserDTO, error) { if user, ok := u.deps.UserCache.Get(userId); ok { return &user, nil From 7e3d9ec1559753ddcd914ca73e4f8c7f4345ef06 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Wed, 31 Jul 2024 08:45:52 +0300 Subject: [PATCH 2/4] implented action token db operations --- src/repo/action_token.go | 63 +++++++++++++++++++++++++++++++++--- src/services/user_service.go | 19 ++++++----- 2 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/repo/action_token.go b/src/repo/action_token.go index 8e4d659..453a4ab 100644 --- a/src/repo/action_token.go +++ b/src/repo/action_token.go @@ -1,9 +1,64 @@ package repo -import "backend/src/models" +import ( + "backend/src/models" + "context" + "database/sql" +) type ActionTokenRepo interface { - CreateActionToken(actionToken models.ActionTokenDTO) (*models.ActionTokenDTO, error) - FindActionToken(userId, val string, target models.ActionTokenTarget) (*models.ActionTokenDTO, error) - DeleteActionToken(id string) error + CreateActionToken(ctx context.Context, dto models.ActionTokenDTO) (*models.ActionTokenDTO, error) + PopActionToken(ctx context.Context, userId, value string, target models.ActionTokenTarget) (*models.ActionTokenDTO, error) +} + +func NewActionTokenRepo(db *sql.DB) ActionTokenRepo { + return &actionTokenRepo{ + db: db, + } +} + +type actionTokenRepo struct { + db *sql.DB +} + +func (a *actionTokenRepo) CreateActionToken(ctx context.Context, dto models.ActionTokenDTO) (*models.ActionTokenDTO, error) { + query := ` + insert into + action_tokens (user_id, value, target) + values ($1, $2, $3) + returning id;` + row := a.db.QueryRowContext(ctx, query, dto.UserId, dto.Value, dto.Target) + + id := "" + if err := row.Scan(&id); err != nil { + return nil, err + } + + return &models.ActionTokenDTO{ + Id: id, + UserId: dto.UserId, + Value: dto.Value, + Target: dto.Target, + }, nil +} + +func (a *actionTokenRepo) PopActionToken(ctx context.Context, userId, value string, target models.ActionTokenTarget) (*models.ActionTokenDTO, error) { + query := ` + delete + from action_tokens + where user_id=$1 and value=$2 and target=$3 + returning id;` + row := a.db.QueryRowContext(ctx, query, userId, value, target) + + id := "" + if err := row.Scan(&id); err != nil { + return nil, err + } + + return &models.ActionTokenDTO{ + Id: id, + UserId: userId, + Value: value, + Target: target, + }, nil } diff --git a/src/services/user_service.go b/src/services/user_service.go index 6e11d62..d733844 100644 --- a/src/services/user_service.go +++ b/src/services/user_service.go @@ -112,11 +112,14 @@ func (u *userService) HelpPasswordForgot(ctx context.Context, userId string) err return err } - actionToken, err := u.deps.ActionTokenRepo.CreateActionToken(models.ActionTokenDTO{ - UserId: user.Id, - Value: uuid.New().String(), - Target: models.ActionTokenTargetForgotPassword, - }) + actionToken, err := u.deps.ActionTokenRepo.CreateActionToken( + ctx, + models.ActionTokenDTO{ + UserId: user.Id, + Value: uuid.New().String(), + Target: models.ActionTokenTargetForgotPassword, + }, + ) if err != nil { return err } @@ -131,7 +134,7 @@ func (u *userService) ChangePasswordForgot(ctx context.Context, userId, newPassw return err } - code, err := u.deps.ActionTokenRepo.FindActionToken(userId, accessCode, models.ActionTokenTargetForgotPassword) + code, err := u.deps.ActionTokenRepo.PopActionToken(ctx, userId, accessCode, models.ActionTokenTargetForgotPassword) if err != nil { return err } @@ -139,10 +142,6 @@ func (u *userService) ChangePasswordForgot(ctx context.Context, userId, newPassw return fmt.Errorf("wrong user access code") } - if err := u.deps.ActionTokenRepo.DeleteActionToken(code.Id); err != nil { - return fmt.Errorf("internal error occured: %w", err) - } - return u.updatePassword(ctx, *user, newPassword) } From 233c5cb057123c0248efac070650cde063e6e36e Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Mon, 5 Aug 2024 06:10:34 +0300 Subject: [PATCH 3/4] fixes, add email msg send, login changed to email --- .vscode/launch.json | 3 ++- db_init.sql | 2 +- go.mod | 2 ++ go.sum | 4 ++++ main.go | 14 +++++++----- src/repo/email_repo.go | 43 ++++++++++++++++++++++++++++++++++++ src/services/user_service.go | 4 ++-- 7 files changed, 63 insertions(+), 9 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 0f8103e..797be4e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,8 @@ "type": "go", "request": "launch", "mode": "auto", - "program": "${workspaceFolder}" + "program": "${workspaceFolder}", + "args": ["-c", "./config_example/config.yaml", "-o", "./log.txt"] } ] } \ No newline at end of file diff --git a/db_init.sql b/db_init.sql index d17d1cd..6811da1 100644 --- a/db_init.sql +++ b/db_init.sql @@ -1,6 +1,6 @@ create table users ( id int generated always as identity, - login text unique not null, + email text unique not null, secret text not null, name text not null, diff --git a/go.mod b/go.mod index ef9cc86..39a6425 100644 --- a/go.mod +++ b/go.mod @@ -49,5 +49,7 @@ require ( golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect ) diff --git a/go.sum b/go.sum index 8826889..2dbc55c 100644 --- a/go.sum +++ b/go.sum @@ -122,11 +122,15 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index e7a4205..1ed5c95 100644 --- a/main.go +++ b/main.go @@ -67,7 +67,7 @@ func main() { logger.Fatal().Err(err).Msg("failed parsing postgres connection string") } - sqlDb := stdlib.OpenDB(connConf) + sqlDb = stdlib.OpenDB(connConf) if err := sqlDb.Ping(); err != nil { logger.Fatal().Err(err).Msg("failed pinging postgres db") } @@ -77,13 +77,17 @@ func main() { passwordUtil := utils.NewPasswordUtil() userRepo := repo.NewUserRepo(sqlDb) userCache := repo.NewCacheInmem[string, models.UserDTO](60 * 60) + emailRepo := repo.NewEmailRepo() + actionTokenRepo := repo.NewActionTokenRepo(sqlDb) userService := services.NewUserService( services.UserServiceDeps{ - Jwt: jwtUtil, - Password: passwordUtil, - UserRepo: userRepo, - UserCache: userCache, + Jwt: jwtUtil, + Password: passwordUtil, + UserRepo: userRepo, + UserCache: userCache, + EmailRepo: emailRepo, + ActionTokenRepo: actionTokenRepo, }, ) diff --git a/src/repo/email_repo.go b/src/repo/email_repo.go index 3b8c256..9dd6733 100644 --- a/src/repo/email_repo.go +++ b/src/repo/email_repo.go @@ -1,5 +1,48 @@ package repo +import ( + "strings" + + "gopkg.in/gomail.v2" +) + +const MSG_TEXT = ` + + + + +

This message was sent because you forgot a password

+

To change a password, use this link

+ + +` + type EmailRepo interface { SendEmailForgotPassword(email, token string) } + +func NewEmailRepo() EmailRepo { + return &emailRepo{} +} + +type emailRepo struct { + // mail *gomail.Dialer +} + +func (e *emailRepo) SendEmailForgotPassword(email, token string) { + link := "https://nucrea.ru?token=" + token + msgText := strings.ReplaceAll(MSG_TEXT, "{{Link}}", link) + + m := gomail.NewMessage() + m.SetHeader("From", "email") + m.SetHeader("To", email) + m.SetHeader("Subject", "Hello!") + m.SetBody("text/html", msgText) + + d := gomail.NewDialer("smtp.yandex.ru", 587, "login", "password") + + // Send the email to Bob, Cora and Dan. + if err := d.DialAndSend(m); err != nil { + panic(err) + } +} diff --git a/src/services/user_service.go b/src/services/user_service.go index d733844..f8e8433 100644 --- a/src/services/user_service.go +++ b/src/services/user_service.go @@ -107,7 +107,7 @@ func (u *userService) AuthenticateUser(ctx context.Context, email, password stri } func (u *userService) HelpPasswordForgot(ctx context.Context, userId string) error { - user, err := u.deps.UserRepo.GetUserById(ctx, userId) + user, err := u.getUserById(ctx, userId) if err != nil { return err } @@ -129,7 +129,7 @@ func (u *userService) HelpPasswordForgot(ctx context.Context, userId string) err } func (u *userService) ChangePasswordForgot(ctx context.Context, userId, newPassword, accessCode string) error { - user, err := u.deps.UserRepo.GetUserById(ctx, userId) + user, err := u.getUserById(ctx, userId) if err != nil { return err } From 10805ef2d5b9b924fe096db00eba4d46f601bd7c Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Mon, 5 Aug 2024 09:58:45 +0300 Subject: [PATCH 4/4] add shortlink generator and handlers --- main.go | 9 +++++ src/handlers/shortlink_handlers.go | 61 ++++++++++++++++++++++++++++++ src/services/shortlink_service.go | 60 +++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 src/handlers/shortlink_handlers.go create mode 100644 src/services/shortlink_service.go diff --git a/main.go b/main.go index 1ed5c95..1c7d398 100644 --- a/main.go +++ b/main.go @@ -90,6 +90,11 @@ func main() { ActionTokenRepo: actionTokenRepo, }, ) + linkService := services.NewShortlinkSevice( + services.NewShortlinkServiceParams{ + Cache: repo.NewCacheInmem[string, string](7 * 24 * 60 * 60), + }, + ) if !debugMode { gin.SetMode(gin.ReleaseMode) @@ -99,6 +104,10 @@ func main() { r.Use(middleware.NewRequestLogMiddleware(logger)) r.Use(gin.Recovery()) + linkGroup := r.Group("/s") + linkGroup.POST("/new", handlers.NewShortlinkCreateHandler(linkService)) + linkGroup.GET("/:linkId", handlers.NewShortlinkResolveHandler(linkService)) + userGroup := r.Group("/user") userGroup.POST("/create", handlers.NewUserCreateHandler(userService)) userGroup.POST("/login", handlers.NewUserLoginHandler(userService)) diff --git a/src/handlers/shortlink_handlers.go b/src/handlers/shortlink_handlers.go new file mode 100644 index 0000000..f228e8e --- /dev/null +++ b/src/handlers/shortlink_handlers.go @@ -0,0 +1,61 @@ +package handlers + +import ( + "backend/src/services" + "encoding/json" + "fmt" + "net/url" + + "github.com/gin-gonic/gin" +) + +type shortlinkCreateOutput struct { + Link string `json:"link"` +} + +func NewShortlinkCreateHandler(shortlinkService services.ShortlinkService) gin.HandlerFunc { + return func(ctx *gin.Context) { + rawUrl := ctx.Query("url") + if rawUrl == "" { + ctx.AbortWithError(400, fmt.Errorf("no url param")) + return + } + + u, err := url.Parse(rawUrl) + if err != nil { + ctx.Data(500, "plain/text", []byte(err.Error())) + return + } + u.Scheme = "https" + + linkId, err := shortlinkService.CreateLink(u.String()) + if err != nil { + ctx.Data(500, "plain/text", []byte(err.Error())) + return + } + + resultBody, err := json.Marshal(shortlinkCreateOutput{ + Link: "https:/nucrea.ru/s/" + linkId, + }) + if err != nil { + ctx.AbortWithError(500, err) + return + } + + ctx.Data(200, "application/json", resultBody) + } +} + +func NewShortlinkResolveHandler(shortlinkService services.ShortlinkService) gin.HandlerFunc { + return func(ctx *gin.Context) { + linkId := ctx.Param("linkId") + + linkUrl, err := shortlinkService.GetLink(linkId) + if err != nil { + ctx.AbortWithError(500, err) + return + } + + ctx.Redirect(301, linkUrl) + } +} diff --git a/src/services/shortlink_service.go b/src/services/shortlink_service.go new file mode 100644 index 0000000..d3f2469 --- /dev/null +++ b/src/services/shortlink_service.go @@ -0,0 +1,60 @@ +package services + +import ( + "backend/src/repo" + "fmt" + "math/rand" + "strings" + "time" +) + +type ShortlinkService interface { + CreateLink(in string) (string, error) + GetLink(id string) (string, error) +} + +type NewShortlinkServiceParams struct { + Endpoint string + Cache repo.Cache[string, string] +} + +func NewShortlinkSevice(params NewShortlinkServiceParams) ShortlinkService { + return &shortlinkService{ + cache: params.Cache, + } +} + +type shortlinkService struct { + cache repo.Cache[string, string] +} + +func (s *shortlinkService) randomStr() string { + src := rand.NewSource(time.Now().UnixMicro()) + randGen := rand.New(src) + + builder := strings.Builder{} + for i := 0; i < 9; i++ { + offset := 0x41 + if randGen.Int()%2 == 1 { + offset = 0x61 + } + + byte := offset + (randGen.Int() % 26) + builder.WriteRune(rune(byte)) + } + return builder.String() +} + +func (s *shortlinkService) CreateLink(in string) (string, error) { + str := s.randomStr() + s.cache.Set(str, in, 7*24*60*60) + return str, nil +} + +func (s *shortlinkService) GetLink(id string) (string, error) { + val, ok := s.cache.Get(id) + if !ok { + return "", fmt.Errorf("link does not exist or expired") + } + return val, nil +}