diff --git a/go.mod b/go.mod index 6a43f77..8c2ac55 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,8 @@ require ( require ( github.com/andybalholm/brotli v1.0.5 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/snappy v0.0.1 // indirect github.com/google/uuid v1.5.0 // indirect github.com/klauspost/compress v1.17.0 // indirect diff --git a/go.sum b/go.sum index 3836e8f..4376a79 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM= github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= diff --git a/internal/app/app.go b/internal/app/app.go index 7f7c269..364bd71 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -5,6 +5,7 @@ import ( "errors" "go.uber.org/zap" "hub_admin_backend_service/internal/initialize" + "hub_admin_backend_service/internal/models" "hub_admin_backend_service/internal/server/http" "hub_admin_backend_service/pkg/closer" "time" @@ -42,11 +43,23 @@ func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error { internalSrv := http.NewServer(http.ServerConfig{ Logger: logger, Controllers: []http.Controller{controllers.PrivilegeInternal, controllers.TariffInternal}, + JWTConfig: &models.JWTConfiguration{ + PrivateKey: cfg.PrivateKey, + PublicKey: cfg.PublicKey, + Issuer: cfg.Issuer, + Audience: cfg.Audience, + }, }) externalSrv := http.NewServer(http.ServerConfig{ Logger: logger, Controllers: []http.Controller{controllers.PrivilegeExternal, controllers.TariffExternal}, + JWTConfig: &models.JWTConfiguration{ + PrivateKey: cfg.PrivateKey, + PublicKey: cfg.PublicKey, + Issuer: cfg.Issuer, + Audience: cfg.Audience, + }, }) go func() { diff --git a/internal/controller/tariff_external/controller.go b/internal/controller/tariff_external/controller.go index 5d90261..33665cc 100644 --- a/internal/controller/tariff_external/controller.go +++ b/internal/controller/tariff_external/controller.go @@ -3,15 +3,16 @@ package tariff_external import ( "github.com/gofiber/fiber/v2" "go.uber.org/zap" + "hub_admin_backend_service/internal/repository/tariff" ) type Deps struct { - Repo string + Repo *tariff.Tariff Logger *zap.Logger } type TariffExternal struct { - repo string + repo *tariff.Tariff logger *zap.Logger } diff --git a/internal/controller/tariff_internal/controller.go b/internal/controller/tariff_internal/controller.go index 79b4357..8812505 100644 --- a/internal/controller/tariff_internal/controller.go +++ b/internal/controller/tariff_internal/controller.go @@ -3,15 +3,19 @@ package tariff_internal import ( "github.com/gofiber/fiber/v2" "go.uber.org/zap" + "hub_admin_backend_service/internal/models" + "hub_admin_backend_service/internal/repository/tariff" ) +// todo middleware jwt + type Deps struct { - Repo string + Repo *tariff.Tariff Logger *zap.Logger } type TariffInternal struct { - repo string + repo *tariff.Tariff logger *zap.Logger } @@ -23,6 +27,16 @@ func NewTariffInternal(deps Deps) *TariffInternal { } func (t *TariffInternal) Get(ctx *fiber.Ctx) error { + tariffID := ctx.Params("id") + if tariffID == "" { + return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "url field id don't be empty"}) + } + + var req models.CreateUpdateTariff + if err := ctx.BodyParser(&req); err != nil { + return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"}) + } + return nil } diff --git a/internal/initialize/config.go b/internal/initialize/config.go index 2540c6f..c3ca236 100644 --- a/internal/initialize/config.go +++ b/internal/initialize/config.go @@ -16,6 +16,10 @@ type Config struct { MongoPassword string `env:"MONGO_PASSWORD" envDefault:"test"` MongoDatabase string `env:"MONGO_DB" envDefault:"admin"` MongoAuth string `env:"MONGO_AUTH" envDefault:"admin"` + PrivateKey string `env:"JWT_PRIVATE_KEY"` + PublicKey string `env:"JWT_PUBLIC_KEY"` + Issuer string `env:"JWT_ISSUER"` + Audience string `env:"JWT_AUDIENCE"` } func LoadConfig() (*Config, error) { diff --git a/internal/initialize/controller.go b/internal/initialize/controller.go index 626627d..4aa0776 100644 --- a/internal/initialize/controller.go +++ b/internal/initialize/controller.go @@ -32,11 +32,11 @@ func NewControllers(deps ControllerDeps) *Controller { }), TariffInternal: tariff_internal.NewTariffInternal(tariff_internal.Deps{ Logger: deps.Logger, - Repo: "", + Repo: deps.Repos.TariffRepo, }), TariffExternal: tariff_external.NewTariffExternal(tariff_external.Deps{ Logger: deps.Logger, - Repo: "", + Repo: deps.Repos.TariffRepo, }), } } diff --git a/internal/initialize/repository.go b/internal/initialize/repository.go index 6f7f078..63bfac3 100644 --- a/internal/initialize/repository.go +++ b/internal/initialize/repository.go @@ -4,6 +4,7 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.uber.org/zap" "hub_admin_backend_service/internal/repository/privilege" + "hub_admin_backend_service/internal/repository/tariff" ) type RepositoryDeps struct { @@ -13,6 +14,7 @@ type RepositoryDeps struct { type Repository struct { PrivilegeRepo *privilege.Privilege + TariffRepo *tariff.Tariff } func NewRepository(deps RepositoryDeps) *Repository { @@ -21,5 +23,9 @@ func NewRepository(deps RepositoryDeps) *Repository { Mdb: deps.Mdb.Collection("privileges"), Logger: deps.Logger, }), + TariffRepo: tariff.NewTariffRepo(tariff.Deps{ + Mdb: deps.Mdb.Collection("tariffs"), + Logger: deps.Logger, + }), } } diff --git a/internal/models/jwt.go b/internal/models/jwt.go new file mode 100644 index 0000000..ecf8291 --- /dev/null +++ b/internal/models/jwt.go @@ -0,0 +1,18 @@ +package models + +import ( + "github.com/golang-jwt/jwt/v5" + "time" +) + +type JWTConfiguration struct { + PrivateKey string + PublicKey string + Issuer string + Audience string + Algorithm jwt.SigningMethodRSA + ExpiresIn time.Duration +} + +const AuthJWTDecodedUserIDKey = "userID" +const AuthJWTDecodedAccessTokenKey = "access-token" diff --git a/internal/models/reqBodies.go b/internal/models/reqBodies.go index f9a430b..a98952b 100644 --- a/internal/models/reqBodies.go +++ b/internal/models/reqBodies.go @@ -13,3 +13,18 @@ type CreateUpdateReq struct { type ManyCreateUpdate struct { Privileges []CreateUpdateReq `json:"privileges"` } + +type TariffPagination struct { + TotalPages int `json:"totalPages"` + Tariffs []Tariff `json:"tariffs"` +} + +type CreateUpdateTariff struct { + Name string `json:"name"` + UserID string `json:"userId"` + Description string `json:"description"` + Price int `json:"price"` + Order int `json:"order"` + IsCustom bool `json:"isCustom"` + Privileges []Privilege `json:"privileges"` +} diff --git a/internal/models/tariff.go b/internal/models/tariff.go new file mode 100644 index 0000000..c395864 --- /dev/null +++ b/internal/models/tariff.go @@ -0,0 +1,19 @@ +package models + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +type Tariff struct { + ID primitive.ObjectID `json:"_id" bson:"_id"` + Name string `json:"name" bson:"name"` + UserID string `json:"userID" bson:"userID"` + Price int `json:"price" bson:"price"` + IsCustom bool `json:"isCustom" bson:"isCustom"` + Privileges []Privilege `json:"privileges" bson:"privileges"` + IsDeleted bool `json:"isDeleted" bson:"isDeleted"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` + DeletedAt time.Time `json:"deletedAt" bson:"deletedAt"` +} diff --git a/internal/repository/tariff/tariff.go b/internal/repository/tariff/tariff.go new file mode 100644 index 0000000..4c290aa --- /dev/null +++ b/internal/repository/tariff/tariff.go @@ -0,0 +1,23 @@ +package tariff + +import ( + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type Deps struct { + Mdb *mongo.Collection + Logger *zap.Logger +} + +type Tariff struct { + mdb *mongo.Collection + logger *zap.Logger +} + +func NewTariffRepo(deps Deps) *Tariff { + return &Tariff{ + mdb: deps.Mdb, + logger: deps.Logger, + } +} diff --git a/internal/server/http/http.go b/internal/server/http/http.go index 88633bd..dbffd50 100644 --- a/internal/server/http/http.go +++ b/internal/server/http/http.go @@ -5,11 +5,14 @@ import ( "fmt" "github.com/gofiber/fiber/v2" "go.uber.org/zap" + "hub_admin_backend_service/internal/models" + "hub_admin_backend_service/internal/utils" ) type ServerConfig struct { Logger *zap.Logger Controllers []Controller + JWTConfig *models.JWTConfiguration } type Server struct { @@ -20,6 +23,13 @@ type Server struct { func NewServer(config ServerConfig) *Server { app := fiber.New() + + jwtUtil := utils.NewJWT(config.JWTConfig) + app.Use("/tariff", utils.NewAuthenticator(jwtUtil)) + app.Use("/privilege", func(c *fiber.Ctx) error { + return c.Next() + }) + s := &Server{ Logger: config.Logger, Controllers: config.Controllers, diff --git a/internal/utils/authenticator.go b/internal/utils/authenticator.go new file mode 100644 index 0000000..f526ab2 --- /dev/null +++ b/internal/utils/authenticator.go @@ -0,0 +1,59 @@ +package utils + +import ( + "errors" + "github.com/gofiber/fiber/v2" + "hub_admin_backend_service/internal/models" + "strings" +) + +const ( + prefix = "Bearer " +) + +func NewAuthenticator(jwtUtil *JWT) fiber.Handler { + return func(c *fiber.Ctx) error { + if jwtUtil == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Invalid arguments") + } + + err := authenticate(jwtUtil, c) + if err != nil { + return fiber.NewError(fiber.StatusUnauthorized, err.Error()) + } + + return c.Next() + } +} + +func authenticate(jwtUtil *JWT, c *fiber.Ctx) error { + jws, err := parseJWSFromRequest(c) + if err != nil { + return err + } + + userID, validateErr := jwtUtil.Validate(jws) + if validateErr != nil { + return validateErr + } + + c.Locals(models.AuthJWTDecodedUserIDKey, userID) + c.Locals(models.AuthJWTDecodedAccessTokenKey, jws) + + return nil +} + +func parseJWSFromRequest(c *fiber.Ctx) (string, error) { + header := c.Get("Authorization") + + if header != "" && strings.HasPrefix(header, prefix) { + return strings.TrimPrefix(header, prefix), nil + } + + token := c.Query("Authorization") + if token == "" { + return "", errors.New("failed to parse jws from request: no valid token found") + } + + return token, nil +} diff --git a/internal/utils/jwt.go b/internal/utils/jwt.go new file mode 100644 index 0000000..03053a9 --- /dev/null +++ b/internal/utils/jwt.go @@ -0,0 +1,89 @@ +package utils + +import ( + "errors" + "fmt" + "github.com/golang-jwt/jwt/v5" + "hub_admin_backend_service/internal/models" + "time" +) + +type JWT struct { + privateKey []byte + publicKey []byte + algorithm *jwt.SigningMethodRSA + expiresIn time.Duration + issuer string + audience string +} + +func NewJWT(configuration *models.JWTConfiguration) *JWT { + return &JWT{ + privateKey: []byte(configuration.PrivateKey), + publicKey: []byte(configuration.PublicKey), + algorithm: jwt.SigningMethodRS256, + expiresIn: 15 * time.Minute, + issuer: configuration.Issuer, + audience: configuration.Audience, + } +} + +func (receiver *JWT) Create(id string) (string, error) { + privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(receiver.privateKey) + if err != nil { + return "", fmt.Errorf("failed to parse private key on of : %w", err) + } + + now := time.Now().UTC() + + claims := jwt.MapClaims{ + "id": id, // Our userID + "exp": now.Add(receiver.expiresIn).Unix(), // The expiration time after which the token must be disregarded. + "aud": receiver.audience, // Audience + "iss": receiver.issuer, // Issuer + } + + token, err := jwt.NewWithClaims(receiver.algorithm, claims).SignedString(privateKey) + if err != nil { + return "", fmt.Errorf("failed to sing on of : %w", err) + } + + return token, nil +} + +func (receiver *JWT) Validate(tokenString string) (string, error) { + key, err := jwt.ParseRSAPublicKeyFromPEM(receiver.publicKey) + if err != nil { + return "", fmt.Errorf("failed to parse rsa public key on of : %w", err) + } + + parseCallback := func(token *jwt.Token) (any, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method: %s", token.Header["alg"]) + } + + return key, nil + } + + token, err := jwt.Parse( + tokenString, + parseCallback, + jwt.WithAudience(receiver.audience), + jwt.WithIssuer(receiver.issuer), + ) + if err != nil { + return "", fmt.Errorf("failed to parse jwt token on of : %w", err) + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok || !token.Valid { + return "", errors.New("token is invalid on of ") + } + + data, ok := claims["id"].(string) + if !ok { + return "", errors.New("data is empty or not a string on of ") + } + + return data, nil +}