Skip to content

Commit

Permalink
refactor: add Token Usecase
Browse files Browse the repository at this point in the history
  • Loading branch information
slowhigh committed Jun 4, 2024
1 parent 38f5e0b commit 2763e76
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 89 deletions.
4 changes: 3 additions & 1 deletion cmd/server/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 9 additions & 9 deletions internal/entity/subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import (
)

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"`
Published time.Time `gorm:"column:published;type:timestamp;not null"`
ID uint `gorm:"column:id;primarykey" json:"id"`
Email string `gorm:"column:email;type:varchar;not null;unique" json:"email"`
Name *string `gorm:"column:name;type:varchar" json:"name"`
Division *string `gorm:"column:division;type:varchar" json:"division"`
PreferredCompanyArr pq.Int64Array `gorm:"column:preferred_company_arr;type:int8[];not null" json:"preferred_company_arr"`
PreferredCompanySizeArr pq.Int64Array `gorm:"column:preferred_company_size_arr;type:int8[];not null" json:"preferred_company_size_arr"`
PreferredJobArr pq.Int64Array `gorm:"column:preferred_job_arr;type:int8[];not null" json:"preferred_job_arr"`
PreferredSkillArr pq.Int64Array `gorm:"column:preferred_skill_arr;type:int8[];not null" json:"preferred_skill_arr"`
Published time.Time `gorm:"column:published;type:timestamp;not null" json:"published"`
}

type SubscriptionRepo interface {
Expand Down
109 changes: 30 additions & 79 deletions internal/usecase/subscription/subscriber_ucase.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package subscription

import (
"bytes"
"errors"
"fmt"
"html/template"
"log/slog"
Expand All @@ -11,58 +10,65 @@ import (
"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"
"github.com/team-nerd-planet/api-server/internal/usecase/token"
"github.com/team-nerd-planet/api-server/internal/usecase/token/model"
)

type SubscriptionUsecase struct {
subscriptionRepo entity.SubscriptionRepo
tokenUcase token.TokenUsecase
conf *config.Config
}

func NewSubscriptionUsecase(
subscriptionRepo entity.SubscriptionRepo,
tokenUsecase token.TokenUsecase,
config *config.Config,
) SubscriptionUsecase {
return SubscriptionUsecase{
subscriptionRepo: subscriptionRepo,
tokenUcase: tokenUsecase,
conf: config,
}
}

func (su SubscriptionUsecase) Apply(subscription entity.Subscription) (*entity.Subscription, bool) {
var (
emailToken model.EmailToken
emailTokenStr string
name string
)

subscription.Published = time.Now()

if subscription.Name != nil {
name = *subscription.Name
} else {
name = subscription.Email
}

id, err := su.subscriptionRepo.ExistEmail(subscription.Email)
if err != nil {
slog.Error(err.Error(), "error", err)
return nil, false
}

subscription.Published = time.Now()
token := emailToken{Subscription: subscription}
token.RegisteredClaims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(1 * time.Hour))

if id == nil {
token.Type = SUBSCRIBE
emailToken = model.NewEmailToken(model.SUBSCRIBE, subscription)
} else {
token.Type = RESUBSCRIBE
token.Subscription.ID = uint(*id)
subscription.ID = uint(*id)
emailToken = model.NewEmailToken(model.RESUBSCRIBE, subscription)
}

tokenStr, err := generateEmailToken(token, su.conf.Jwt.SecretKey)
emailTokenStr, err = su.tokenUcase.GenerateToken(emailToken)
if err != nil {
slog.Error(err.Error(), "error", err)
return nil, false
}

var name string
if token.Subscription.Name != nil {
name = *token.Subscription.Name
} else {
name = token.Subscription.Email
}

if err := sendSubscribeMail(su.conf.Smtp, name, subscription.Email, tokenStr); err != nil {
if err := sendSubscribeMail(su.conf.Smtp, name, subscription.Email, emailTokenStr); err != nil {
slog.Error(err.Error(), "error", err)
return nil, false
}
Expand All @@ -72,23 +78,22 @@ func (su SubscriptionUsecase) Apply(subscription entity.Subscription) (*entity.S

func (su SubscriptionUsecase) Approve(token string) (*entity.Subscription, bool) {
var (
emailToken *emailToken
emailToken model.EmailToken
err error
subscription *entity.Subscription
)

emailToken, err = verifyEmailToken(token, su.conf.Jwt.SecretKey)
if err != nil {
if err := su.tokenUcase.VerifyToken(token, &emailToken); err != nil {
slog.Warn(err.Error())
return nil, true
}

switch emailToken.Type {
case SUBSCRIBE:
switch emailToken.TokenType {
case model.SUBSCRIBE:
subscription, err = su.subscriptionRepo.Create(emailToken.Subscription)
case RESUBSCRIBE:
case model.RESUBSCRIBE:
subscription, err = su.subscriptionRepo.Update(int64(emailToken.Subscription.ID), emailToken.Subscription)
case UNSUBSCRIBE:
case model.UNSUBSCRIBE:
subscription, err = su.subscriptionRepo.Delete(int64(emailToken.Subscription.ID))
default:
return nil, false
Expand Down Expand Up @@ -138,57 +143,3 @@ func sendSubscribeMail(smtpConf config.Smtp, name, email, token string) error {

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
}
32 changes: 32 additions & 0 deletions internal/usecase/token/model/email_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package model

import (
"time"

"github.com/golang-jwt/jwt/v5"
"github.com/team-nerd-planet/api-server/internal/entity"
)

type TokenType int

const (
SUBSCRIBE TokenType = iota
RESUBSCRIBE
UNSUBSCRIBE
)

type EmailToken struct {
TokenType TokenType `json:"token_type"`
Subscription entity.Subscription `json:"subscription"`
jwt.RegisteredClaims
}

func NewEmailToken(tokenType TokenType, subscription entity.Subscription) EmailToken {
return EmailToken{
TokenType: tokenType,
Subscription: subscription,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
},
}
}
57 changes: 57 additions & 0 deletions internal/usecase/token/token_ucase.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package token

import (
"errors"
"time"

"github.com/golang-jwt/jwt/v5"
"github.com/team-nerd-planet/api-server/infra/config"
)

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 TokenUsecase struct {
conf *config.Config
}

func NewTokenUsecase(conf *config.Config) TokenUsecase {
return TokenUsecase{
conf: conf,
}
}

func (tu TokenUsecase) GenerateToken(claims jwt.Claims) (string, error) {
newToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return newToken.SignedString([]byte(tu.conf.Jwt.SecretKey))
}

func (tu TokenUsecase) VerifyToken(tokenString string, claims jwt.Claims) (err error) {
token, err := jwt.ParseWithClaims(tokenString, claims, 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(tu.conf.Jwt.SecretKey), nil
})
if err != nil {
return err
}

if !token.Valid {
return errSignatureInvalid
}

return nil
}
62 changes: 62 additions & 0 deletions internal/usecase/token/token_ucase_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package token

import (
"testing"
"time"

"github.com/golang-jwt/jwt/v5"
"github.com/lib/pq"
"github.com/team-nerd-planet/api-server/infra/config"
"github.com/team-nerd-planet/api-server/internal/entity"
"github.com/team-nerd-planet/api-server/internal/usecase/token/model"
)

func Test_GenerateAndVerifyEmailToken(t *testing.T) {
tokenUsecase := NewTokenUsecase(&config.Config{
Jwt: config.Jwt{
SecretKey: "test_key",
},
})

name := "name"
division := "division"
token1 := model.EmailToken{
Subscription: entity.Subscription{
Email: "email",
Name: &name,
Division: &division,
Published: time.Now(),
PreferredCompanyArr: pq.Int64Array{},
PreferredCompanySizeArr: pq.Int64Array{},
PreferredJobArr: pq.Int64Array{},
PreferredSkillArr: pq.Int64Array{},
},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
},
}

str, err := tokenUsecase.GenerateToken(token1)
if err != nil {
t.Error(err.Error())
return

}

var token2 model.EmailToken
err = tokenUsecase.VerifyToken(str, &token2)
if err != nil {
t.Error(err.Error())
return
}

if token1.Subscription.Email != token2.Subscription.Email ||
*token1.Subscription.Name != *token2.Subscription.Name ||
*token1.Subscription.Division != *token2.Subscription.Division ||
token1.Subscription.Published.Compare(token2.Subscription.Published) != 0 {
t.Errorf("not same")
return
}

t.Log("PASS")
}
2 changes: 2 additions & 0 deletions internal/usecase/usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"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"
"github.com/team-nerd-planet/api-server/internal/usecase/token"
)

var UsecaseSet = wire.NewSet(
Expand All @@ -14,4 +15,5 @@ var UsecaseSet = wire.NewSet(
tag.NewSkillTagUsecase,
subscription.NewSubscriptionUsecase,
feed.NewFeedUsecase,
token.NewTokenUsecase,
)

0 comments on commit 2763e76

Please sign in to comment.