diff --git a/README.md b/README.md new file mode 100644 index 0000000..d8c24f3 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +```bash +go run ./cmd/server . + +# "google/wire" module +go install github.com/google/wire/cmd/wire@latest +wire ./cmd/server + +# "swaggo/swag" module +go install github.com/swaggo/swag/cmd/swag@latest +swag init -pd -g ./cmd/server/main.go -o ./third_party/docs/ + +# "golang.org/x/tools" module +go install golang.org/x/tools/cmd/goimports@latest +goimports -w . +gofmt -s -w . +``` diff --git a/cmd/server/wire_gen.go b/cmd/server/wire_gen.go index 25cab25..218ed90 100644 --- a/cmd/server/wire_gen.go +++ b/cmd/server/wire_gen.go @@ -13,6 +13,7 @@ import ( "github.com/team-nerd-planet/api-server/infra/router" "github.com/team-nerd-planet/api-server/internal/controller/rest" "github.com/team-nerd-planet/api-server/internal/usecase/item" + "github.com/team-nerd-planet/api-server/internal/usecase/subscription" "github.com/team-nerd-planet/api-server/internal/usecase/tag" ) @@ -35,6 +36,9 @@ func InitServer() (router.Router, error) { skillTagRepo := repository.NewSkillTagRepo(databaseDatabase) skillTagUsecase := tag.NewSkillTagUsecase(skillTagRepo) tagController := rest.NewTagController(jobTagUsecase, skillTagUsecase) - routerRouter := router.NewRouter(configConfig, itemController, tagController) + subscriptionRepo := repository.NewSubscriptionRepo(databaseDatabase) + subscriptionUsecase := subscription.NewSubscriptionUsecase(subscriptionRepo, configConfig) + subscriptionController := rest.NewSubscriptionController(subscriptionUsecase) + routerRouter := router.NewRouter(configConfig, itemController, tagController, subscriptionController) return routerRouter, nil } diff --git a/go.mod b/go.mod index 5d9fa98..107472d 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/gin-contrib/cache v1.3.0 github.com/gin-contrib/cors v1.7.2 github.com/gin-gonic/gin v1.10.0 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/wire v0.6.0 github.com/lib/pq v1.10.9 github.com/spf13/viper v1.18.2 diff --git a/go.sum b/go.sum index 0d20018..cfc0f5c 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,8 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +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/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= diff --git a/infra/config/config.go b/infra/config/config.go index 7eaa782..c571847 100644 --- a/infra/config/config.go +++ b/infra/config/config.go @@ -13,6 +13,8 @@ type Config struct { Rest Rest `mapstructure:"REST"` Database Database `mapstructure:"DATABASE"` Swagger Swagger `mapstructure:"SWAGGER"` + Jwt Jwt `mapstructure:"JWT"` + Smtp Smtp `mapstructure:"SMTP"` } type App struct { @@ -38,6 +40,17 @@ type Swagger struct { BasePath string `mapstructure:"BASE_PATH"` } +type Jwt struct { + SecretKey string `mapstructure:"SECRET_KEY"` +} + +type Smtp struct { + Host string `mapstructure:"HOST"` + Port int `mapstructure:"PORT"` + UserName string `mapstructure:"USER_NAME"` + Password string `mapstructure:"PASSWORD"` +} + func NewConfig() (*Config, error) { _, b, _, _ := runtime.Caller(0) configDirPath := path.Join(path.Dir(b)) diff --git a/infra/config/config.json b/infra/config/config.json index 481edd6..c598b84 100644 --- a/infra/config/config.json +++ b/infra/config/config.json @@ -15,7 +15,7 @@ "LOG_LEVEL": 4 }, "SWAGGER": { - "HOST": "localhost", + "HOST": "localhost:5000", "BASE_PATH": "/" } } diff --git a/infra/database/repository/subscription_repo.go b/infra/database/repository/subscription_repo.go new file mode 100644 index 0000000..e869370 --- /dev/null +++ b/infra/database/repository/subscription_repo.go @@ -0,0 +1,72 @@ +package repository + +import ( + "errors" + + "github.com/team-nerd-planet/api-server/infra/database" + "github.com/team-nerd-planet/api-server/internal/entity" + "gorm.io/gorm" +) + +type SubscriptionRepo struct { + db *database.Database +} + +func NewSubscriptionRepo(db *database.Database) entity.SubscriptionRepo { + db.AutoMigrate(&entity.Subscription{}) + + return &SubscriptionRepo{ + db: db, + } +} + +// Create implements entity.SubscriptionRepo. +func (sr *SubscriptionRepo) Create(newSubscription entity.Subscription) (*entity.Subscription, error) { + err := sr.db.Create(&newSubscription).Error + if err != nil { + return nil, err + } + + return &newSubscription, nil +} + +// Delete implements entity.SubscriptionRepo. +func (sr *SubscriptionRepo) Delete(id int64) (*entity.Subscription, error) { + panic("unimplemented") +} + +// ExistEmail implements entity.SubscriptionRepo. +func (sr *SubscriptionRepo) ExistEmail(email string) (*int64, error) { + data := entity.Subscription{} + + err := sr.db.Select("id").Take(&data, "email = ?", email).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } else if err != nil { + return nil, err + } + + id := int64(data.ID) + return &id, nil +} + +// Update implements entity.SubscriptionRepo. +func (sr *SubscriptionRepo) Update(id int64, newSubscription entity.Subscription) (*entity.Subscription, error) { + var ( + subscription entity.Subscription + ) + + err := sr.db.First(&subscription, id).Error + if err != nil { + return nil, err + } + + subscription = newSubscription + subscription.ID = uint(id) + err = sr.db.Save(&subscription).Error + if err != nil { + return nil, err + } + + return &subscription, nil +} diff --git a/infra/infra.go b/infra/infra.go index 56774c3..0254b2d 100644 --- a/infra/infra.go +++ b/infra/infra.go @@ -15,4 +15,5 @@ var InfraSet = wire.NewSet( repository.NewItemRepo, repository.NewJobTagRepo, repository.NewSkillTagRepo, + repository.NewSubscriptionRepo, ) diff --git a/infra/router/handler/item_handler.go b/infra/router/handler/item_handler.go index 8f483ed..95d8b91 100644 --- a/infra/router/handler/item_handler.go +++ b/infra/router/handler/item_handler.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" "github.com/team-nerd-planet/api-server/infra/router/util" "github.com/team-nerd-planet/api-server/internal/controller/rest" + _ "github.com/team-nerd-planet/api-server/internal/controller/rest/dto" "github.com/team-nerd-planet/api-server/internal/controller/rest/dto/item_dto" _ "github.com/team-nerd-planet/api-server/internal/entity" ) @@ -23,7 +24,7 @@ import ( // @Param job_tags query []int64 false "관련 직무 DB ID 배열" collectionFormat(multi) // @Param skill_tags query []int64 false "관련 스킬 DB ID 배열" collectionFormat(multi) // @Param page query int true "페이지" -// @Success 200 {object} item_dto.FindAllItemRes +// @Success 200 {object} dto.Paginated[[]item_dto.FindAllItemRes] // @Failure 400 {object} util.HTTPError // @Failure 404 {object} util.HTTPError // @Failure 500 {object} util.HTTPError @@ -31,7 +32,6 @@ import ( func ListItems(c *gin.Context, ctrl rest.ItemController) { req, err := util.ValidateQuery[item_dto.FindAllItemReq](c) if err != nil { - c.Status(http.StatusBadRequest) util.NewError(c, http.StatusBadRequest, err) return } diff --git a/infra/router/handler/subscription_handler.go b/infra/router/handler/subscription_handler.go new file mode 100644 index 0000000..84d8c0b --- /dev/null +++ b/infra/router/handler/subscription_handler.go @@ -0,0 +1,59 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/team-nerd-planet/api-server/infra/router/util" + "github.com/team-nerd-planet/api-server/internal/controller/rest" + "github.com/team-nerd-planet/api-server/internal/controller/rest/dto/subscription_dto" +) + +// Apply +// +// @Summary Apply subscription +// @Description apply for subscription +// @Tags subscription +// @Schemes http +// @Accept json +// @Produce json +// @Param request body subscription_dto.ApplyReq true "contents for applying for subscription." +// @Success 200 {object} subscription_dto.ApplyRes +// @Failure 400 {object} util.HTTPError +// @Failure 500 {object} util.HTTPError +// @Router /v1/subscription/apply [post] +func Apply(c *gin.Context, ctrl rest.SubscriptionController) { + req, err := util.ValidateBody[subscription_dto.ApplyReq](c) + if err != nil { + util.NewError(c, http.StatusBadRequest, err) + return + } + + res, ok := ctrl.Apply(*req) + if !ok { + util.NewError(c, http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, res) +} + +func Approve(c *gin.Context, ctrl rest.SubscriptionController) { + req, err := util.ValidateQuery[subscription_dto.ApproveReq](c) + if err != nil { + c.Redirect(http.StatusBadRequest, "https://www.nerdplanet.app") + return + } + + res, ok := ctrl.Approve(*req) + if !ok { + c.Redirect(http.StatusInternalServerError, "https://www.nerdplanet.app") + return + } + + if res.Ok { + c.Redirect(http.StatusFound, "https://www.nerdplanet.app") + } else { + c.Redirect(http.StatusBadRequest, "https://www.nerdplanet.app") + } +} diff --git a/infra/router/router.go b/infra/router/router.go index 8dd0b8f..0445cba 100644 --- a/infra/router/router.go +++ b/infra/router/router.go @@ -9,11 +9,11 @@ import ( "github.com/gin-gonic/gin" swaggerfiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" - docs "github.com/team-nerd-planet/api-server/docs" "github.com/team-nerd-planet/api-server/infra/config" "github.com/team-nerd-planet/api-server/infra/router/handler" "github.com/team-nerd-planet/api-server/infra/router/middleware" "github.com/team-nerd-planet/api-server/internal/controller/rest" + docs "github.com/team-nerd-planet/api-server/third_party/docs" ) type Router struct { @@ -22,7 +22,7 @@ type Router struct { conf *config.Config } -func NewRouter(conf *config.Config, itemCtrl rest.ItemController, tabCtrl rest.TagController) Router { +func NewRouter(conf *config.Config, itemCtrl rest.ItemController, tagCtrl rest.TagController, subscriptionCtrl rest.SubscriptionController) Router { if conf.Rest.Mode == "release" { gin.SetMode(gin.ReleaseMode) } @@ -44,8 +44,14 @@ func NewRouter(conf *config.Config, itemCtrl rest.ItemController, tabCtrl rest.T tag := v1.Group("/tag") { - tag.GET("/job", cache.CachePage(s, time.Hour, func(c *gin.Context) { handler.ListJobTags(c, tabCtrl) })) - tag.GET("/skill", cache.CachePage(s, time.Hour, func(c *gin.Context) { handler.ListSkillTags(c, tabCtrl) })) + tag.GET("/job", cache.CachePage(s, time.Hour, func(c *gin.Context) { handler.ListJobTags(c, tagCtrl) })) + tag.GET("/skill", cache.CachePage(s, time.Hour, func(c *gin.Context) { handler.ListSkillTags(c, tagCtrl) })) + } + + subscription := v1.Group("/subscription") + { + subscription.POST("/apply", func(c *gin.Context) { handler.Apply(c, subscriptionCtrl) }) + subscription.GET("/approve", func(c *gin.Context) { handler.Approve(c, subscriptionCtrl) }) } } diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 922f967..b6dc164 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -5,4 +5,4 @@ import ( "github.com/team-nerd-planet/api-server/internal/controller/rest" ) -var ControllerSet = wire.NewSet(rest.NewItemController, rest.NewTagController) +var ControllerSet = wire.NewSet(rest.NewItemController, rest.NewTagController, rest.NewSubscriptionController) diff --git a/internal/controller/rest/dto/item_dto/item_find_all_dto.go b/internal/controller/rest/dto/item_dto/find_all_dto.go similarity index 100% rename from internal/controller/rest/dto/item_dto/item_find_all_dto.go rename to internal/controller/rest/dto/item_dto/find_all_dto.go diff --git a/internal/controller/rest/dto/subscription_dto/apply_dto.go b/internal/controller/rest/dto/subscription_dto/apply_dto.go new file mode 100644 index 0000000..49e01e0 --- /dev/null +++ b/internal/controller/rest/dto/subscription_dto/apply_dto.go @@ -0,0 +1,34 @@ +package subscription_dto + +import "github.com/team-nerd-planet/api-server/internal/entity" + +type ApplyReq struct { + Email string `json:"email" binding:"required,email"` // 이메일 + Name *string `json:"name" binding:"omitempty"` // 이름 + Division *string `json:"division" binding:"omitempty"` // 소속 + PreferredCompanyArr []int64 `json:"preferred_company_arr" binding:"required"` // 회사 DB ID 배열 + PreferredCompanySizeArr []entity.CompanySizeType `json:"preferred_companySize_arr" binding:"required"` // 회사 규모 배열 (0:스타트업, 1:중소기업, 2:중견기업, 3:대기업, 4:외국계) + PreferredJobArr []int64 `json:"preferred_job_arr" binding:"required"` // 직무 DB ID 배열 + PreferredSkillArr []int64 `json:"preferred_skill_arr" binding:"required"` // 스킬 DB ID 배열 +} + +type ApplyRes struct { + Ok bool `json:"ok"` // 구독 신청 메일 전송 결과 +} + +func (asr ApplyReq) NewSubscription() entity.Subscription { + companySizeArr := make([]int64, len(asr.PreferredCompanySizeArr)) + for i, companySize := range asr.PreferredCompanySizeArr { + companySizeArr[i] = int64(companySize) + } + + return entity.Subscription{ + Email: asr.Email, + Name: asr.Name, + Division: asr.Division, + PreferredCompanyArr: asr.PreferredCompanyArr, + PreferredCompanySizeArr: companySizeArr, + PreferredJobArr: asr.PreferredJobArr, + PreferredSkillArr: asr.PreferredSkillArr, + } +} diff --git a/internal/controller/rest/dto/subscription_dto/approve_dto.go b/internal/controller/rest/dto/subscription_dto/approve_dto.go new file mode 100644 index 0000000..ac04c77 --- /dev/null +++ b/internal/controller/rest/dto/subscription_dto/approve_dto.go @@ -0,0 +1,9 @@ +package subscription_dto + +type ApproveReq struct { + Token string `form:"token" binding:"required"` +} + +type ApproveRes struct { + Ok bool `json:"ok"` // 구독 인증 결과 +} \ No newline at end of file diff --git a/internal/controller/rest/subscription_controller.go b/internal/controller/rest/subscription_controller.go new file mode 100644 index 0000000..7e8bce1 --- /dev/null +++ b/internal/controller/rest/subscription_controller.go @@ -0,0 +1,48 @@ +package rest + +import ( + "github.com/team-nerd-planet/api-server/internal/controller/rest/dto/subscription_dto" + "github.com/team-nerd-planet/api-server/internal/usecase/subscription" +) + +type SubscriptionController struct { + subscriptionUcase subscription.SubscriptionUsecase +} + +func NewSubscriptionController(subscriptionUcase subscription.SubscriptionUsecase) SubscriptionController { + return SubscriptionController{ + subscriptionUcase: subscriptionUcase, + } +} + +func (sc SubscriptionController) Apply(req subscription_dto.ApplyReq) (*subscription_dto.ApplyRes, bool) { + subscription, ok := sc.subscriptionUcase.ApplySubscription(req.NewSubscription()) + if !ok { + return nil, false + } + + result := false + if subscription != nil { + result = true + } + + return &subscription_dto.ApplyRes{ + Ok: result, + }, true +} + +func (sc SubscriptionController) Approve(req subscription_dto.ApproveReq) (*subscription_dto.ApproveRes, bool) { + subscription, ok := sc.subscriptionUcase.Subscribe(req.Token) + if !ok { + return nil, false + } + + result := false + if subscription != nil { + result = true + } + + return &subscription_dto.ApproveRes{ + Ok: result, + }, true +} diff --git a/internal/entity/feed.go b/internal/entity/feed.go index 4b66ea7..0a798a2 100644 --- a/internal/entity/feed.go +++ b/internal/entity/feed.go @@ -5,11 +5,11 @@ import "time" type CompanySizeType int const ( - STARTUP CompanySizeType = iota //스타트업 - SMALL //중소기업 - MEDIUM //중견기업 - LARGE //대기업 - FOREIGN //외국계 + STARTUP CompanySizeType = iota // 스타트업 + SMALL // 중소기업 + MEDIUM // 중견기업 + LARGE // 대기업 + FOREIGN // 외국계 ) type Feed struct { diff --git a/internal/entity/subscription.go b/internal/entity/subscription.go new file mode 100644 index 0000000..91772c4 --- /dev/null +++ b/internal/entity/subscription.go @@ -0,0 +1,21 @@ +package entity + +import "github.com/lib/pq" + +type Subscription struct { + ID uint `gorm:"column:id;primarykey"` + Email string `gorm:"column:email;type:varchar;not null;unique"` + Name *string `gorm:"column:name;type:varchar"` + Division *string `gorm:"column:division;type:varchar"` + PreferredCompanyArr pq.Int64Array `gorm:"column:preferred_company_arr;type:int8[];not null"` + PreferredCompanySizeArr pq.Int64Array `gorm:"column:preferred_company_size_arr;type:int8[];not null"` + PreferredJobArr pq.Int64Array `gorm:"column:preferred_job_arr;type:int8[];not null"` + PreferredSkillArr pq.Int64Array `gorm:"column:preferred_skill_arr;type:int8[];not null"` +} + +type SubscriptionRepo interface { + ExistEmail(email string) (*int64, error) + Create(subscription Subscription) (*Subscription, error) + Update(id int64, subscription Subscription) (*Subscription, error) + Delete(id int64) (*Subscription, error) +} diff --git a/internal/usecase/subscription/subscriber_ucase.go b/internal/usecase/subscription/subscriber_ucase.go new file mode 100644 index 0000000..e1c07db --- /dev/null +++ b/internal/usecase/subscription/subscriber_ucase.go @@ -0,0 +1,199 @@ +package subscription + +import ( + "bytes" + "errors" + "fmt" + "html/template" + "log/slog" + "net/smtp" + "path" + "runtime" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/team-nerd-planet/api-server/infra/config" + "github.com/team-nerd-planet/api-server/internal/entity" +) + +type SubscriptionUsecase struct { + subscriptionRepo entity.SubscriptionRepo + conf *config.Config +} + +func NewSubscriptionUsecase( + subscriptionRepo entity.SubscriptionRepo, + config *config.Config, +) SubscriptionUsecase { + return SubscriptionUsecase{ + subscriptionRepo: subscriptionRepo, + conf: config, + } +} + +func (su SubscriptionUsecase) ApplySubscription(subscription entity.Subscription) (*entity.Subscription, bool) { + id, err := su.subscriptionRepo.ExistEmail(subscription.Email) + if err != nil { + slog.Error(err.Error()) + return nil, false + } + + token := emailToken{Subscription: subscription} + token.RegisteredClaims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(1 * time.Hour)) + + if id == nil { + token.Type = SUBSCRIBE + } else { + token.Type = RESUBSCRIBE + token.Subscription.ID = uint(*id) + } + + tokenStr, err := generateEmailToken(token, su.conf.Jwt.SecretKey) + if err != nil { + slog.Error(err.Error()) + return nil, false + } + + if err := sendSubscribeMail(su.conf.Smtp.Host, su.conf.Smtp.Port, su.conf.Smtp.UserName, su.conf.Smtp.Password, subscription.Email, tokenStr); err != nil { + return nil, false + } + + return &subscription, true +} + +func (su SubscriptionUsecase) CancelSubscription(email string) (*entity.Subscription, bool) { + //이메일 조회 + //JWT 토큰 생성 + //이메일이 없으면 실패 반환 + //이메일이 있으면 DELETE 이메일 전송 + return nil, true +} + +func (su SubscriptionUsecase) Subscribe(token string) (*entity.Subscription, bool) { + var ( + emailToken *emailToken + err error + subscription *entity.Subscription + ) + + emailToken, err = verifyEmailToken(token, su.conf.Jwt.SecretKey) + if err != nil { + slog.Error(err.Error()) + return nil, false + } + + switch emailToken.Type { + case SUBSCRIBE: + subscription, err = su.subscriptionRepo.Create(emailToken.Subscription) + case RESUBSCRIBE: + subscription, err = su.subscriptionRepo.Update(int64(emailToken.Subscription.ID), emailToken.Subscription) + case UNSUBSCRIBE: + subscription, err = su.subscriptionRepo.Delete(int64(emailToken.Subscription.ID)) + default: + return nil, false + } + + if err != nil { + slog.Error(err.Error()) + return nil, false + } + + return subscription, true +} + +func (su SubscriptionUsecase) Resubscribe(subscription entity.Subscription) (*entity.Subscription, bool) { + return nil, false +} + +func (su SubscriptionUsecase) Unsubscribe(email string) (*entity.Subscription, bool) { + return nil, false +} + +func sendSubscribeMail(host string, port int, userName, password, email, token string) error { + data := struct { + Token string + }{ + Token: token, + } + + _, b, _, _ := runtime.Caller(0) + configDirPath := path.Join(path.Dir(b)) + t, err := template.ParseFiles(fmt.Sprintf("%s/template/subscription.html", configDirPath)) + if err != nil { + slog.Error(err.Error()) + return err + } + + var body bytes.Buffer + if err := t.Execute(&body, data); err != nil { + slog.Error(err.Error()) + return err + } + + auth := smtp.PlainAuth("", userName, password, host) + from := userName + to := []string{email} + subject := "Subject: Nerd Planet 메일 인증\n" + mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" + msg := []byte(subject + mime + body.String()) + err = smtp.SendMail(fmt.Sprintf("%s:%d", host, port), auth, from, to, msg) + if err != nil { + return err + } + + return nil +} + +var ( + errTokenExpired = errors.New("token has invalid claims: token is expired") + errUnexpectedSigningMethod = errors.New("unexpected signing method: HMAC-SHA") + errSignatureInvalid = errors.New("token signature is invalid: signature is invalid") +) + +type tokenType int + +const ( + SUBSCRIBE tokenType = iota + RESUBSCRIBE + UNSUBSCRIBE +) + +type emailToken struct { + Type tokenType `json:"type"` + Subscription entity.Subscription `json:"subscription"` + jwt.RegisteredClaims +} + +func generateEmailToken(token emailToken, secretKey string) (string, error) { + newToken := jwt.NewWithClaims(jwt.SigningMethodHS256, token) + return newToken.SignedString([]byte(secretKey)) +} + +func verifyEmailToken(tokenString string, secretKey string) (*emailToken, error) { + token, err := jwt.ParseWithClaims(tokenString, &emailToken{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errUnexpectedSigningMethod + } + + expiration, err := token.Claims.GetExpirationTime() + if err != nil { + return nil, err + } + + if expiration.Time.Unix() < time.Now().Unix() { + return nil, errTokenExpired + } + + return []byte(secretKey), nil + }) + if err != nil { + return nil, err + } + + claims, ok := token.Claims.(*emailToken) + if !ok { + return nil, errSignatureInvalid + } + + return claims, nil +} diff --git a/internal/usecase/subscription/template/subscription.html b/internal/usecase/subscription/template/subscription.html new file mode 100644 index 0000000..c91eedc --- /dev/null +++ b/internal/usecase/subscription/template/subscription.html @@ -0,0 +1,45 @@ + + + +
+ + +