diff --git a/cmd/cgnolink/main.go b/cmd/cgnolink/main.go index 4a72941..45ce28a 100644 --- a/cmd/cgnolink/main.go +++ b/cmd/cgnolink/main.go @@ -1,9 +1,9 @@ package main import ( - "cgnolink" - "cgnolink/database" - appserver "cgnolink/server" + "cgnolink/pkg/cgnolink" + "cgnolink/pkg/cgnolink/database" + appserver "cgnolink/pkg/cgnolink/server" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "os" diff --git a/go.mod b/go.mod index ab59fcd..96d31a5 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,9 @@ require ( github.com/knadh/koanf v0.15.0 github.com/labstack/echo/v4 v4.2.1 github.com/mitchellh/copystructure v1.1.1 // indirect - github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible github.com/rs/zerolog v1.20.0 + golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 // indirect golang.org/x/text v0.3.5 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect diff --git a/link/service.go b/link/service.go deleted file mode 100644 index b35d7d5..0000000 --- a/link/service.go +++ /dev/null @@ -1,91 +0,0 @@ -package link - -import ( - apperrors "cgnolink/errors" - "github.com/patrickmn/go-cache" -) - -type Service interface { - Create(link *Link) error - GetById(id string) (*Link, error) - GetAll(limit int, offset int) (Links, error) - Update(data *Link) error - DeleteById(id string) error -} - -type PgService struct { - rep Repository - cache *cache.Cache -} - -func NewService(rep Repository) Service { - return &PgService{ - rep: rep, - cache: cache.New(cache.NoExpiration, 0), - } -} - -func (s *PgService) Create(link *Link) error { - existingLink, err := s.rep.FindById(link.Id) - if err != nil { - return apperrors.UnknownError{Err: err} - } - - if existingLink != nil { - return apperrors.AlreadyExistsError{Message: "Link with given ID already exists."} - } - - if err = s.rep.Save(link); err != nil { - return apperrors.UnknownError{Err: err} - } - - return nil -} - -func (s *PgService) GetById(id string) (*Link, error) { - if v, found := s.cache.Get(id); found { - if link, ok := v.(*Link); ok { - return link, nil - } - } - - link, err := s.rep.FindById(id) - if err != nil { - return nil, apperrors.UnknownError{Err: err} - } - - if link == nil { - return nil, apperrors.NotFoundError{Message: "Link with given ID was not found."} - } - - s.cache.Set(id, link, cache.DefaultExpiration) - - return link, nil -} - -func (s *PgService) GetAll(limit int, offset int) (Links, error) { - links, err := s.rep.GetAll(limit, offset) - if err != nil { - return nil, apperrors.UnknownError{Err: err} - } - - return links, nil -} - -func (s *PgService) Update(data *Link) error { - if err := s.rep.Update(data); err != nil { - return apperrors.UnknownError{Err: err} - } - - return nil -} - -func (s *PgService) DeleteById(id string) error { - s.cache.Delete(id) - - if err := s.rep.DeleteById(id); err != nil { - return apperrors.UnknownError{Err: err} - } - - return nil -} diff --git a/migrations/002_add_password.sql b/migrations/002_add_password.sql new file mode 100644 index 0000000..715fffb --- /dev/null +++ b/migrations/002_add_password.sql @@ -0,0 +1,13 @@ +ALTER TABLE links +ADD COLUMN password varchar; + +UPDATE links +SET password = ''; + +ALTER TABLE links +ALTER COLUMN password SET not null; + +---- create above / drop below ---- + +ALTER TABLE links +DROP COLUMN password; diff --git a/config.go b/pkg/cgnolink/config.go similarity index 100% rename from config.go rename to pkg/cgnolink/config.go diff --git a/database/database.go b/pkg/cgnolink/database/database.go similarity index 100% rename from database/database.go rename to pkg/cgnolink/database/database.go diff --git a/database/migrate.go b/pkg/cgnolink/database/migrate.go similarity index 100% rename from database/migrate.go rename to pkg/cgnolink/database/migrate.go diff --git a/database/util.go b/pkg/cgnolink/database/util.go similarity index 100% rename from database/util.go rename to pkg/cgnolink/database/util.go diff --git a/errors/errors.go b/pkg/cgnolink/errors/errors.go similarity index 100% rename from errors/errors.go rename to pkg/cgnolink/errors/errors.go diff --git a/link/dto.go b/pkg/cgnolink/link/dto.go similarity index 90% rename from link/dto.go rename to pkg/cgnolink/link/dto.go index d8b2e68..c811153 100644 --- a/link/dto.go +++ b/pkg/cgnolink/link/dto.go @@ -9,6 +9,7 @@ type CreationModel struct { Id string `json:"id"` Name string `json:"name"` RedirectURL string `json:"redirectUrl"` + Password string `json:"password"` } type ResourceModel struct { @@ -21,6 +22,7 @@ type ResourceModel struct { type UpdateModel struct { Name string `json:"name,omitempty"` RedirectURL string `json:"redirectUrl,omitempty"` + Password string `json:"password"` } func (m *CreationModel) MapModelToEntity() (*Link, error) { @@ -33,6 +35,7 @@ func (m *CreationModel) MapModelToEntity() (*Link, error) { Id: m.Id, Name: m.Name, RedirectURL: *u, + Password: m.Password, CreationTime: time.Now().UTC(), }, nil } diff --git a/link/entity.go b/pkg/cgnolink/link/entity.go similarity index 89% rename from link/entity.go rename to pkg/cgnolink/link/entity.go index 50f6521..343743e 100644 --- a/link/entity.go +++ b/pkg/cgnolink/link/entity.go @@ -9,6 +9,7 @@ type Link struct { Id string Name string RedirectURL url.URL + Password string CreationTime time.Time } diff --git a/link/handlers.go b/pkg/cgnolink/link/handlers.go similarity index 80% rename from link/handlers.go rename to pkg/cgnolink/link/handlers.go index aa7c178..10a25c6 100644 --- a/link/handlers.go +++ b/pkg/cgnolink/link/handlers.go @@ -11,13 +11,14 @@ import ( func redirectHandler(ctx echo.Context, serv Service) error { linkId := ctx.Param("id") + linkPassword := ctx.QueryParam("password") - link, err := serv.GetById(linkId) + redirectUrl, err := serv.AccessLink(linkId, linkPassword) if err != nil { return err } - return ctx.Redirect(http.StatusSeeOther, link.RedirectURL.String()) + return ctx.Redirect(http.StatusSeeOther, redirectUrl.String()) } func creationHandler(ctx echo.Context, serv Service) error { @@ -53,7 +54,7 @@ func allRetrievalHandler(ctx echo.Context, serv Service) error { limit := 20 if v := ctx.QueryParam("limit"); v != "" { num, err := strconv.Atoi(v) - if err != nil { + if err != nil || num < 0 { return echo.NewHTTPError(http.StatusBadRequest, "Invalid limit value.") } @@ -63,7 +64,7 @@ func allRetrievalHandler(ctx echo.Context, serv Service) error { offset := 0 if v := ctx.QueryParam("offset"); v != "" { num, err := strconv.Atoi(v) - if err != nil { + if err != nil || num < 0 { return echo.NewHTTPError(http.StatusBadRequest, "Invalid offset value.") } @@ -91,33 +92,20 @@ func updateHandler(ctx echo.Context, serv Service) error { return echo.NewHTTPError(http.StatusBadRequest, "Invalid data format.") } - updatingLink, err := serv.GetById(linkId) + var parsedUrl *url.URL + parsedUrl, err := url.Parse(model.RedirectURL) if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid URL value.") + } + + if err := serv.UpdateById(linkId, struct { + Name string + Password string + RedirectURL *url.URL + }{Name: model.Name, Password: model.Password, RedirectURL: parsedUrl}); err != nil { return err } - hasChanges := false - switch { - case model.Name != "" && model.Name != updatingLink.Name: - updatingLink.Name = model.Name - - hasChanges = true - case model.RedirectURL != "" && model.RedirectURL != updatingLink.RedirectURL.String(): - if parsedUrl, err := url.Parse(model.RedirectURL); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid URL value.") - } else { - updatingLink.RedirectURL = *parsedUrl - } - - hasChanges = true - } - - if hasChanges { - if err = serv.Update(updatingLink); err != nil { - return err - } - } - return ctx.NoContent(http.StatusOK) } diff --git a/link/repository.go b/pkg/cgnolink/link/repository.go similarity index 83% rename from link/repository.go rename to pkg/cgnolink/link/repository.go index 8751b3a..aa19b82 100644 --- a/link/repository.go +++ b/pkg/cgnolink/link/repository.go @@ -1,7 +1,7 @@ package link import ( - "cgnolink/database" + "cgnolink/pkg/cgnolink/database" "context" "errors" "github.com/jackc/pgtype" @@ -34,12 +34,12 @@ func (r *PgRepository) Save(link *Link) error { defer cancel() sql := ` - INSERT INTO links (id, name, redirect_url, creation_time) - VALUES ($1, $2, $3, $4::timestamp) + INSERT INTO links (id, name, redirect_url, password, creation_time) + VALUES ($1, $2, $3, $4, $5::timestamp) ` database.LogPoolState(r.pool, "Saving link") - _, err := r.pool.Exec(ctx, sql, link.Id, link.Name, link.RedirectURL.String(), link.CreationTime.Format("2006-01-02 15:04:05")) + _, err := r.pool.Exec(ctx, sql, link.Id, link.Name, link.RedirectURL.String(), link.Password, link.CreationTime.Format("2006-01-02 15:04:05")) if err != nil { return err } @@ -52,7 +52,7 @@ func (r *PgRepository) FindById(id string) (*Link, error) { defer cancel() sql := ` - SELECT id, name, redirect_url, creation_time + SELECT id, name, redirect_url, password, creation_time FROM links WHERE id = $1 ` @@ -82,7 +82,7 @@ func (r *PgRepository) GetAll(limit int, offset int) (Links, error) { defer cancel() sql := ` - SELECT id, name, redirect_url, creation_time + SELECT id, name, redirect_url, password, creation_time FROM links LIMIT $1 OFFSET $2 @@ -122,12 +122,12 @@ func (r *PgRepository) Update(link *Link) error { sql := ` UPDATE links - SET name = $1, redirect_url = $2 - WHERE id = $3 + SET name = $1, redirect_url = $2, password = $3 + WHERE id = $4 ` database.LogPoolState(r.pool, "Updating link") - _, err := r.pool.Exec(ctx, sql, link.Name, link.RedirectURL.String(), link.Id) + _, err := r.pool.Exec(ctx, sql, link.Name, link.RedirectURL.String(), link.Password, link.Id) if err != nil { return err } @@ -159,11 +159,11 @@ func mapRowToEntity(r interface{}) (*Link, error) { switch v := r.(type) { case pgx.Row: - if err := v.Scan(&entity.Id, &entity.Name, &urlStr, &t); err != nil { + if err := v.Scan(&entity.Id, &entity.Name, &urlStr, &entity.Password, &t); err != nil { return nil, err } case pgx.Rows: - if err := v.Scan(&entity.Id, &entity.Name, &urlStr, &t); err != nil { + if err := v.Scan(&entity.Id, &entity.Name, &urlStr, &entity.Password, &t); err != nil { return nil, err } default: diff --git a/pkg/cgnolink/link/service.go b/pkg/cgnolink/link/service.go new file mode 100644 index 0000000..b7f00f6 --- /dev/null +++ b/pkg/cgnolink/link/service.go @@ -0,0 +1,171 @@ +package link + +import ( + apperrors "cgnolink/pkg/cgnolink/errors" + "github.com/patrickmn/go-cache" + "golang.org/x/crypto/bcrypt" + "net/url" +) + +var linkNotFoundError = apperrors.NotFoundError{Message: "Link with given ID was not found."} + +type Service interface { + Create(link *Link) error + GetById(id string) (*Link, error) + AccessLink(id string, password string) (*url.URL, error) + GetAll(limit int, offset int) (Links, error) + UpdateById( + id string, + data struct { + Name string + Password string + RedirectURL *url.URL + }, + ) error + DeleteById(id string) error +} + +type PgService struct { + rep Repository + cache *cache.Cache +} + +func (service *PgService) AccessLink(id string, password string) (*url.URL, error) { + link, err := service.GetById(id) + if err != nil { + return nil, err + } + + if link.Password != "" { + if password == "" || bcrypt.CompareHashAndPassword([]byte(link.Password), []byte(password)) != nil { + return nil, linkNotFoundError + } + } + + return &link.RedirectURL, nil +} + +func NewService(rep Repository) Service { + return &PgService{ + rep: rep, + cache: cache.New(cache.NoExpiration, 0), + } +} + +func (service *PgService) Create(link *Link) error { + existingLink, err := service.rep.FindById(link.Id) + if err != nil { + return apperrors.UnknownError{Err: err} + } + + if existingLink != nil { + return apperrors.AlreadyExistsError{Message: "Link with given ID already exists."} + } + + if link.Password != "" { + hashedPassword, err := HashPassword(link.Password) + if err != nil { + return err + } + + link.Password = hashedPassword + } + + if err = service.rep.Save(link); err != nil { + return apperrors.UnknownError{Err: err} + } + + return nil +} + +func (service *PgService) GetById(id string) (*Link, error) { + if v, found := service.cache.Get(id); found { + if link, ok := v.(*Link); ok { + return link, nil + } + } + + link, err := service.rep.FindById(id) + if err != nil { + return nil, apperrors.UnknownError{Err: err} + } + + if link == nil { + return nil, linkNotFoundError + } + + service.cache.Set(id, link, cache.DefaultExpiration) + + return link, nil +} + +func (service *PgService) GetAll(limit int, offset int) (Links, error) { + links, err := service.rep.GetAll(limit, offset) + if err != nil { + return nil, apperrors.UnknownError{Err: err} + } + + return links, nil +} + +func (service *PgService) UpdateById( + id string, + data struct { + Name string + Password string + RedirectURL *url.URL + }, +) error { + link, err := service.GetById(id) + if err != nil { + return err + } + + hasChanges := false + switch { + case data.Name != "": + link.Name = data.Name + + hasChanges = true + case data.RedirectURL != nil: + link.RedirectURL = *data.RedirectURL + + hasChanges = true + case data.Password != "": + hashedPw, err := HashPassword(data.Password) + if err != nil { + return err + } + + link.Password = hashedPw + + hasChanges = true + } + + if hasChanges { + if err := service.rep.Update(link); err != nil { + return apperrors.UnknownError{Err: err} + } + service.cache.Delete(link.Id) + } + + return nil +} + +func (service *PgService) DeleteById(id string) error { + if err := service.rep.DeleteById(id); err != nil { + return apperrors.UnknownError{Err: err} + } + service.cache.Delete(id) + + return nil +} + +func HashPassword(password string) (string, error) { + hashedPw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", apperrors.UnknownError{Err: err} + } + + return string(hashedPw), nil +} diff --git a/server/middleware.go b/pkg/cgnolink/server/middleware.go similarity index 100% rename from server/middleware.go rename to pkg/cgnolink/server/middleware.go diff --git a/server/server.go b/pkg/cgnolink/server/server.go similarity index 95% rename from server/server.go rename to pkg/cgnolink/server/server.go index 11e23de..254116b 100644 --- a/server/server.go +++ b/pkg/cgnolink/server/server.go @@ -1,8 +1,8 @@ package server import ( - apperrors "cgnolink/errors" - "cgnolink/link" + apperrors "cgnolink/pkg/cgnolink/errors" + "cgnolink/pkg/cgnolink/link" "github.com/jackc/pgx/v4/pgxpool" "github.com/knadh/koanf" "github.com/labstack/echo/v4"