diff --git a/cmd/cgnolink/main.go b/cmd/cgnolink/main.go index 45ce28a..70635f9 100644 --- a/cmd/cgnolink/main.go +++ b/cmd/cgnolink/main.go @@ -1,12 +1,12 @@ package main import ( - "cgnolink/pkg/cgnolink" - "cgnolink/pkg/cgnolink/database" - appserver "cgnolink/pkg/cgnolink/server" + "cgnolink" + "cgnolink/database" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "os" + "time" ) func main() { @@ -14,13 +14,15 @@ func main() { conf := cgnolink.NewConfig() - pool := database.InitPool(conf) + pool := database.Pool(conf) + database.Migrate(pool) - server := appserver.NewServer(conf, pool) + server := cgnolink.NewServer(conf, pool) server.Logger.Fatal(server.Start(":8080")) } func configureLogger() { - log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "2006-01-02 15:04:05"}) + zerolog.TimeFieldFormat = time.RFC3339Nano + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) } diff --git a/pkg/cgnolink/config.go b/config.go similarity index 100% rename from pkg/cgnolink/config.go rename to config.go diff --git a/pkg/cgnolink/database/database.go b/database/database.go similarity index 60% rename from pkg/cgnolink/database/database.go rename to database/database.go index 5796452..a6314fb 100644 --- a/pkg/cgnolink/database/database.go +++ b/database/database.go @@ -6,34 +6,16 @@ import ( "fmt" "github.com/jackc/pgx/v4/pgxpool" "github.com/knadh/koanf" - "github.com/rs/zerolog/log" - "time" ) -func InitPool(config *koanf.Koanf) *pgxpool.Pool { - pool := NewPool(config) - - if err := runMigrations(pool); err != nil { - log.Fatal().Err(err).Msg("Couldn't apply migrations") - panic(err) - } - - return pool -} - -func NewPool(config *koanf.Koanf) *pgxpool.Pool { +func Pool(config *koanf.Koanf) *pgxpool.Pool { connStr, err := getConnectionString(config) if err != nil { - log.Fatal().Err(err).Msg("Couldn't construct DB connection string") panic(err) } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - pool, err := pgxpool.Connect(ctx, connStr) + pool, err := pgxpool.Connect(context.Background(), connStr) if err != nil { - log.Fatal().Err(err).Msg("Couldn't connect to DB") panic(err) } diff --git a/database/migrate.go b/database/migrate.go new file mode 100644 index 0000000..4223e4a --- /dev/null +++ b/database/migrate.go @@ -0,0 +1,27 @@ +package database + +import ( + "context" + "github.com/jackc/pgx/v4/pgxpool" + "github.com/jackc/tern/migrate" +) + +func Migrate(pool *pgxpool.Pool) { + conn, err := pool.Acquire(context.Background()) + if err != nil { + panic(err) + } + + migrator, err := migrate.NewMigrator(context.Background(), conn.Conn(), "schema_version") + if err != nil { + panic(err) + } + + if err = migrator.LoadMigrations("./migrations"); err != nil { + panic(err) + } + + if err = migrator.Migrate(context.Background()); err != nil { + panic(err) + } +} diff --git a/pkg/cgnolink/errors/errors.go b/errors/errors.go similarity index 100% rename from pkg/cgnolink/errors/errors.go rename to errors/errors.go diff --git a/go.mod b/go.mod index 96d31a5..ab59fcd 100644 --- a/go.mod +++ b/go.mod @@ -13,9 +13,8 @@ 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 + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 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/pkg/cgnolink/link/dto.go b/link/dto.go similarity index 90% rename from pkg/cgnolink/link/dto.go rename to link/dto.go index c811153..d8b2e68 100644 --- a/pkg/cgnolink/link/dto.go +++ b/link/dto.go @@ -9,7 +9,6 @@ type CreationModel struct { Id string `json:"id"` Name string `json:"name"` RedirectURL string `json:"redirectUrl"` - Password string `json:"password"` } type ResourceModel struct { @@ -22,7 +21,6 @@ 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) { @@ -35,7 +33,6 @@ 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/pkg/cgnolink/link/entity.go b/link/entity.go similarity index 89% rename from pkg/cgnolink/link/entity.go rename to link/entity.go index 343743e..50f6521 100644 --- a/pkg/cgnolink/link/entity.go +++ b/link/entity.go @@ -9,7 +9,6 @@ type Link struct { Id string Name string RedirectURL url.URL - Password string CreationTime time.Time } diff --git a/link/handlers.go b/link/handlers.go new file mode 100644 index 0000000..6892a35 --- /dev/null +++ b/link/handlers.go @@ -0,0 +1,163 @@ +package link + +import ( + "encoding/json" + "github.com/jackc/pgx/v4/pgxpool" + "github.com/labstack/echo/v4" + "net/http" + "net/url" + "strconv" +) + +func redirectHandler(c echo.Context, serv *PgService) error { + linkId := c.Param("id") + + link, err := serv.GetById(linkId) + if err != nil { + return err + } + + return c.Redirect(http.StatusSeeOther, link.RedirectURL.String()) +} + +func creationHandler(c echo.Context, serv *PgService) error { + var model CreationModel + if err := json.NewDecoder(c.Request().Body).Decode(&model); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid data format.") + } + + entity, err := model.MapModelToEntity() + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid URL.") + } + + if err = serv.Create(entity); err != nil { + return err + } + + return c.NoContent(http.StatusCreated) +} + +func retrievalByIdHandler(c echo.Context, serv *PgService) error { + linkId := c.Param("id") + + l, err := serv.GetById(linkId) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, MapEntityToModel(l)) +} + +func allRetrievalHandler(c echo.Context, serv *PgService) error { + limit := 20 + if v := c.QueryParam("limit"); v != "" { + num, err := strconv.Atoi(v) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid limit value.") + } + + limit = num + } + + offset := 0 + if v := c.QueryParam("offset"); v != "" { + num, err := strconv.Atoi(v) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid offset value.") + } + + offset = num + } + + links, err := serv.GetAll(limit, offset) + if err != nil { + return err + } + + models := make([]ResourceModel, len(links)) + for i, v := range links { + models[i] = MapEntityToModel(v) + } + + return c.JSON(http.StatusOK, models) +} + +func updateHandler(c echo.Context, serv *PgService) error { + linkId := c.Param("id") + + var model UpdateModel + if err := json.NewDecoder(c.Request().Body).Decode(&model); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid data format.") + } + + updatingLink, err := serv.GetById(linkId) + if 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 c.NoContent(http.StatusOK) +} + +func removalHandler(c echo.Context, serv *PgService) error { + linkId := c.Param("id") + + if err := serv.DeleteById(linkId); err != nil { + return err + } + + return c.NoContent(http.StatusNoContent) +} + +func AddHandlers(s *echo.Echo, pool *pgxpool.Pool) { + serv := NewPgService(PgRepository{pool: pool}) + + linksGroup := s.Group("/links") + exactLinkGroup := linksGroup.Group("/:id") + + linksGroup.POST("", func(ctx echo.Context) error { + return creationHandler(ctx, &serv) + }) + + linksGroup.GET("", func(ctx echo.Context) error { + return allRetrievalHandler(ctx, &serv) + }) + + exactLinkGroup.GET("", func(ctx echo.Context) error { + return retrievalByIdHandler(ctx, &serv) + }) + + exactLinkGroup.PATCH("", func(ctx echo.Context) error { + return updateHandler(ctx, &serv) + }) + + exactLinkGroup.DELETE("", func(ctx echo.Context) error { + return removalHandler(ctx, &serv) + }) + + s.GET("/:id", func(ctx echo.Context) error { + return redirectHandler(ctx, &serv) + }) +} diff --git a/pkg/cgnolink/link/repository.go b/link/repository.go similarity index 59% rename from pkg/cgnolink/link/repository.go rename to link/repository.go index aa19b82..6ab2fb2 100644 --- a/pkg/cgnolink/link/repository.go +++ b/link/repository.go @@ -1,7 +1,6 @@ package link import ( - "cgnolink/pkg/cgnolink/database" "context" "errors" "github.com/jackc/pgtype" @@ -11,12 +10,12 @@ import ( "time" ) -const defaultContextTimeout = 10 * time.Second +const defaultContextTimeout = 5 * time.Second type Repository interface { Save(link *Link) error FindById(id string) (*Link, error) - GetAll(limit int, offset int) (Links, error) + FindAll(limit int, offset int) (Links, error) Update(link *Link) error DeleteById(id string) error } @@ -25,22 +24,27 @@ type PgRepository struct { pool *pgxpool.Pool } -func NewRepository(pool *pgxpool.Pool) Repository { - return &PgRepository{pool: pool} -} - func (r *PgRepository) Save(link *Link) error { ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) defer cancel() + tx, err := r.pool.Begin(ctx) + if err != nil { + return err + } + sql := ` - INSERT INTO links (id, name, redirect_url, password, creation_time) - VALUES ($1, $2, $3, $4, $5::timestamp) + INSERT INTO links (id, name, redirect_url, creation_time) + VALUES ($1, $2, $3, $4::timestamp) ` - database.LogPoolState(r.pool, "Saving link") - _, err := r.pool.Exec(ctx, sql, link.Id, link.Name, link.RedirectURL.String(), link.Password, link.CreationTime.Format("2006-01-02 15:04:05")) + _, err = tx.Exec(ctx, sql, link.Id, link.Name, link.RedirectURL.String(), link.CreationTime.Format("2006-01-02 15:04:05")) if err != nil { + _ = tx.Rollback(ctx) + return err + } + + if err = tx.Commit(ctx); err != nil { return err } @@ -51,26 +55,35 @@ func (r *PgRepository) FindById(id string) (*Link, error) { ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) defer cancel() + tx, err := r.pool.Begin(ctx) + if err != nil { + return nil, err + } + sql := ` - SELECT id, name, redirect_url, password, creation_time + SELECT id, name, redirect_url, creation_time FROM links WHERE id = $1 ` - database.LogPoolState(r.pool, "Finding link by ID") - entity, err := mapRowToEntity(r.pool.QueryRow(ctx, sql, id)) + entity, err := mapRowToEntity(tx.QueryRow(ctx, sql, id)) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, nil } else { + _ = tx.Rollback(ctx) return nil, err } } + if err = tx.Commit(ctx); err != nil { + return nil, err + } + return entity, nil } -func (r *PgRepository) GetAll(limit int, offset int) (Links, error) { +func (r *PgRepository) FindAll(limit int, offset int) (Links, error) { if limit < 0 { return nil, errors.New("limit can't be negative") } @@ -81,16 +94,21 @@ func (r *PgRepository) GetAll(limit int, offset int) (Links, error) { ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) defer cancel() + tx, err := r.pool.Begin(ctx) + if err != nil { + return nil, err + } + sql := ` - SELECT id, name, redirect_url, password, creation_time + SELECT id, name, redirect_url, creation_time FROM links LIMIT $1 OFFSET $2 ` - database.LogPoolState(r.pool, "Getting all links") - rows, err := r.pool.Query(ctx, sql, limit, offset) + rows, err := tx.Query(ctx, sql, limit, offset) if err != nil { + _ = tx.Rollback(ctx) return nil, err } defer rows.Close() @@ -99,6 +117,7 @@ func (r *PgRepository) GetAll(limit int, offset int) (Links, error) { for rows.Next() { link, err := mapRowToEntity(rows) if err != nil { + _ = tx.Rollback(ctx) return nil, err } @@ -106,6 +125,11 @@ func (r *PgRepository) GetAll(limit int, offset int) (Links, error) { } if err = rows.Err(); err != nil { + _ = tx.Rollback(ctx) + return nil, err + } + + if err = tx.Commit(ctx); err != nil { return nil, err } @@ -120,15 +144,24 @@ func (r *PgRepository) Update(link *Link) error { ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) defer cancel() + tx, err := r.pool.Begin(ctx) + if err != nil { + return err + } + sql := ` UPDATE links - SET name = $1, redirect_url = $2, password = $3 - WHERE id = $4 + SET name = $1, redirect_url = $2 + WHERE id = $3 ` - database.LogPoolState(r.pool, "Updating link") - _, err := r.pool.Exec(ctx, sql, link.Name, link.RedirectURL.String(), link.Password, link.Id) + _, err = tx.Exec(ctx, sql, link.Name, link.RedirectURL.String(), link.Id) if err != nil { + _ = tx.Rollback(ctx) + return err + } + + if err = tx.Commit(ctx); err != nil { return err } @@ -139,13 +172,22 @@ func (r *PgRepository) DeleteById(id string) error { ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) defer cancel() + tx, err := r.pool.Begin(ctx) + if err != nil { + return err + } + sql := ` DELETE FROM links WHERE id = $1 ` - database.LogPoolState(r.pool, "Deleting link") - _, err := r.pool.Exec(ctx, sql, id) + _, err = tx.Exec(ctx, sql, id) if err != nil { + _ = tx.Rollback(ctx) + return err + } + + if err = tx.Commit(ctx); err != nil { return err } @@ -159,11 +201,11 @@ func mapRowToEntity(r interface{}) (*Link, error) { switch v := r.(type) { case pgx.Row: - if err := v.Scan(&entity.Id, &entity.Name, &urlStr, &entity.Password, &t); err != nil { + if err := v.Scan(&entity.Id, &entity.Name, &urlStr, &t); err != nil { return nil, err } case pgx.Rows: - if err := v.Scan(&entity.Id, &entity.Name, &urlStr, &entity.Password, &t); err != nil { + if err := v.Scan(&entity.Id, &entity.Name, &urlStr, &t); err != nil { return nil, err } default: diff --git a/link/service.go b/link/service.go new file mode 100644 index 0000000..f6acee1 --- /dev/null +++ b/link/service.go @@ -0,0 +1,92 @@ +package link + +import ( + apperrors "cgnolink/errors" + "github.com/patrickmn/go-cache" + "time" +) + +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 PgRepository + cache *cache.Cache +} + +func NewPgService(rep PgRepository) PgService { + return PgService{ + rep: rep, + cache: cache.New(1*time.Hour, 90*time.Minute), + } +} + +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, 0) + + return link, nil +} + +func (s *PgService) GetAll(limit int, offset int) (Links, error) { + links, err := s.rep.FindAll(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/pkg/cgnolink/server/middleware.go b/middleware/logger.go similarity index 95% rename from pkg/cgnolink/server/middleware.go rename to middleware/logger.go index 82c2901..b34af3c 100644 --- a/pkg/cgnolink/server/middleware.go +++ b/middleware/logger.go @@ -1,4 +1,4 @@ -package server +package middleware import ( "github.com/labstack/echo/v4" @@ -26,7 +26,7 @@ func Logger() echo.MiddlewareFunc { Str("user_agent", req.UserAgent()). Int("status", res.Status). Str("latency", stop.Sub(start).String()). - Msg("Request") + Send() return } diff --git a/migrations/002_add_password.sql b/migrations/002_add_password.sql deleted file mode 100644 index 715fffb..0000000 --- a/migrations/002_add_password.sql +++ /dev/null @@ -1,13 +0,0 @@ -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/pkg/cgnolink/database/migrate.go b/pkg/cgnolink/database/migrate.go deleted file mode 100644 index 44f96be..0000000 --- a/pkg/cgnolink/database/migrate.go +++ /dev/null @@ -1,33 +0,0 @@ -package database - -import ( - "context" - "github.com/jackc/pgx/v4/pgxpool" - "github.com/jackc/tern/migrate" - "time" -) - -func runMigrations(pool *pgxpool.Pool) (err error) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - conn, err := pool.Acquire(ctx) - if err != nil { - return - } - - migrator, err := migrate.NewMigrator(ctx, conn.Conn(), "schema_version") - if err != nil { - return - } - - if err = migrator.LoadMigrations("./migrations"); err != nil { - return - } - - if err = migrator.Migrate(ctx); err != nil { - return - } - - return -} diff --git a/pkg/cgnolink/database/util.go b/pkg/cgnolink/database/util.go deleted file mode 100644 index 9d31c75..0000000 --- a/pkg/cgnolink/database/util.go +++ /dev/null @@ -1,15 +0,0 @@ -package database - -import ( - "github.com/jackc/pgx/v4/pgxpool" - "github.com/rs/zerolog/log" -) - -func LogPoolState(pool *pgxpool.Pool, message string) { - stat := pool.Stat() - log.Info(). - Int32("acquired_conns", stat.AcquiredConns()). - Int32("idle_conns", stat.IdleConns()). - Int32("total_conns", stat.TotalConns()). - Msg(message) -} diff --git a/pkg/cgnolink/link/handlers.go b/pkg/cgnolink/link/handlers.go deleted file mode 100644 index 10a25c6..0000000 --- a/pkg/cgnolink/link/handlers.go +++ /dev/null @@ -1,151 +0,0 @@ -package link - -import ( - "encoding/json" - "github.com/jackc/pgx/v4/pgxpool" - "github.com/labstack/echo/v4" - "net/http" - "net/url" - "strconv" -) - -func redirectHandler(ctx echo.Context, serv Service) error { - linkId := ctx.Param("id") - linkPassword := ctx.QueryParam("password") - - redirectUrl, err := serv.AccessLink(linkId, linkPassword) - if err != nil { - return err - } - - return ctx.Redirect(http.StatusSeeOther, redirectUrl.String()) -} - -func creationHandler(ctx echo.Context, serv Service) error { - var model CreationModel - if err := json.NewDecoder(ctx.Request().Body).Decode(&model); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid data format.") - } - - entity, err := model.MapModelToEntity() - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid URL.") - } - - if err = serv.Create(entity); err != nil { - return err - } - - return ctx.NoContent(http.StatusCreated) -} - -func retrievalByIdHandler(ctx echo.Context, serv Service) error { - linkId := ctx.Param("id") - - l, err := serv.GetById(linkId) - if err != nil { - return err - } - - return ctx.JSON(http.StatusOK, MapEntityToModel(l)) -} - -func allRetrievalHandler(ctx echo.Context, serv Service) error { - limit := 20 - if v := ctx.QueryParam("limit"); v != "" { - num, err := strconv.Atoi(v) - if err != nil || num < 0 { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid limit value.") - } - - limit = num - } - - offset := 0 - if v := ctx.QueryParam("offset"); v != "" { - num, err := strconv.Atoi(v) - if err != nil || num < 0 { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid offset value.") - } - - offset = num - } - - links, err := serv.GetAll(limit, offset) - if err != nil { - return err - } - - models := make([]ResourceModel, len(links)) - for i, v := range links { - models[i] = MapEntityToModel(v) - } - - return ctx.JSON(http.StatusOK, models) -} - -func updateHandler(ctx echo.Context, serv Service) error { - linkId := ctx.Param("id") - - var model UpdateModel - if err := json.NewDecoder(ctx.Request().Body).Decode(&model); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid data format.") - } - - 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 - } - - return ctx.NoContent(http.StatusOK) -} - -func removalHandler(ctx echo.Context, serv Service) error { - linkId := ctx.Param("id") - - if err := serv.DeleteById(linkId); err != nil { - return err - } - - return ctx.NoContent(http.StatusNoContent) -} - -func AddHandlers(server *echo.Echo, pool *pgxpool.Pool) { - serv := NewService(NewRepository(pool)) - - linksGroup := server.Group("/links") - exactLinkGroup := linksGroup.Group("/:id") - - linksGroup.POST("", func(ctx echo.Context) error { - return creationHandler(ctx, serv) - }) - - linksGroup.GET("", func(ctx echo.Context) error { - return allRetrievalHandler(ctx, serv) - }) - - exactLinkGroup.GET("", func(ctx echo.Context) error { - return retrievalByIdHandler(ctx, serv) - }) - - exactLinkGroup.PATCH("", func(ctx echo.Context) error { - return updateHandler(ctx, serv) - }) - - exactLinkGroup.DELETE("", func(ctx echo.Context) error { - return removalHandler(ctx, serv) - }) - - server.GET("/:id", func(ctx echo.Context) error { - return redirectHandler(ctx, serv) - }) -} diff --git a/pkg/cgnolink/link/service.go b/pkg/cgnolink/link/service.go deleted file mode 100644 index b7f00f6..0000000 --- a/pkg/cgnolink/link/service.go +++ /dev/null @@ -1,171 +0,0 @@ -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/pkg/cgnolink/server/server.go b/server.go similarity index 92% rename from pkg/cgnolink/server/server.go rename to server.go index 254116b..08e7256 100644 --- a/pkg/cgnolink/server/server.go +++ b/server.go @@ -1,8 +1,9 @@ -package server +package cgnolink import ( - apperrors "cgnolink/pkg/cgnolink/errors" - "cgnolink/pkg/cgnolink/link" + apperrors "cgnolink/errors" + "cgnolink/link" + appmiddleware "cgnolink/middleware" "github.com/jackc/pgx/v4/pgxpool" "github.com/knadh/koanf" "github.com/labstack/echo/v4" @@ -26,7 +27,7 @@ func NewServer(conf *koanf.Koanf, pool *pgxpool.Pool) *echo.Echo { func addMiddleware(s *echo.Echo, conf *koanf.Koanf) { s.Use(middleware.CORS()) - s.Use(Logger()) + s.Use(appmiddleware.Logger()) s.Use(middleware.JWTWithConfig(middleware.JWTConfig{ Skipper: func(ctx echo.Context) bool {