diff --git a/api/nkode_api.go b/api/nkode_api.go index ddcb856..fd538fc 100644 --- a/api/nkode_api.go +++ b/api/nkode_api.go @@ -5,6 +5,7 @@ import ( "git.infra.nkode.tech/dkelly/nkode-core/config" "git.infra.nkode.tech/dkelly/nkode-core/email" "git.infra.nkode.tech/dkelly/nkode-core/entities" + "git.infra.nkode.tech/dkelly/nkode-core/memCache" "git.infra.nkode.tech/dkelly/nkode-core/repository" "git.infra.nkode.tech/dkelly/nkode-core/security" "github.com/google/uuid" @@ -23,6 +24,7 @@ type NKodeAPI struct { repo repository.CustomerUserRepository signupSessionCache *cache.Cache emailQueue *email.Queue + forgotNkodeCache memCache.ForgotNKodeCache } func NewNKodeAPI(repo repository.CustomerUserRepository, queue *email.Queue) NKodeAPI { @@ -30,6 +32,7 @@ func NewNKodeAPI(repo repository.CustomerUserRepository, queue *email.Queue) NKo repo: repo, emailQueue: queue, signupSessionCache: cache.New(sessionExpiration, sessionCleanupInterval), + forgotNkodeCache: memCache.NewForgotNKodeCache(), } } @@ -79,7 +82,6 @@ func (n *NKodeAPI) GenerateSignupResetInterface(userEmail entities.UserEmail, cu return nil, err } svgInterface, err := n.repo.GetSvgStringInterface(signupSession.LoginUserInterface.SvgId) - if err != nil { return nil, err } @@ -246,7 +248,7 @@ func (n *NKodeAPI) RefreshToken(userEmail entities.UserEmail, customerId entitie return security.EncodeAndSignClaims(newAccessClaims) } -func (n *NKodeAPI) ResetNKode(userEmail entities.UserEmail, customerId entities.CustomerId) error { +func (n *NKodeAPI) ForgotNKode(userEmail entities.UserEmail, customerId entities.CustomerId) error { user, err := n.repo.GetUser(userEmail, customerId) if err != nil { return fmt.Errorf("error getting user in rest nkode %v", err) @@ -272,6 +274,7 @@ func (n *NKodeAPI) ResetNKode(userEmail entities.UserEmail, customerId entities. Content: htmlBody, } n.emailQueue.AddEmail(email) + n.forgotNkodeCache.Set(userEmail, customerId) return nil } diff --git a/email/queue.go b/email/queue.go index 57ad73f..f4f7742 100644 --- a/email/queue.go +++ b/email/queue.go @@ -14,6 +14,13 @@ import ( "time" ) +const ( + EmailRetryExpiration = 5 * time.Minute + SesCleanupInterval = 10 * time.Minute + EmailQueueBufferSize = 100 + MaxEmailsPerSecond = 13 +) + type Client interface { SendEmail(Email) error } @@ -36,14 +43,9 @@ type SESClient struct { ResetCache *cache.Cache } -const ( - emailRetryExpiration = 5 * time.Minute - sesCleanupInterval = 10 * time.Minute -) - func NewSESClient() SESClient { return SESClient{ - ResetCache: cache.New(emailRetryExpiration, sesCleanupInterval), + ResetCache: cache.New(EmailRetryExpiration, SesCleanupInterval), } } @@ -79,7 +81,7 @@ func (s *SESClient) SendEmail(email Email) error { Source: aws.String(email.Sender), } - if err = s.ResetCache.Add(email.Recipient, nil, emailRetryExpiration); err != nil { + if err = s.ResetCache.Add(email.Recipient, nil, EmailRetryExpiration); err != nil { return err } diff --git a/entities/policy.go b/entities/policy.go index 773c788..3369a8c 100644 --- a/entities/policy.go +++ b/entities/policy.go @@ -3,12 +3,12 @@ package entities import "git.infra.nkode.tech/dkelly/nkode-core/config" type NKodePolicy struct { - MaxNkodeLen int `json:"max_nkode_len" form:"max_nkode_len" binding:"required"` - MinNkodeLen int `json:"min_nkode_len" form:"min_nkode_len" binding:"required"` - DistinctSets int `json:"distinct_sets" form:"distinct_sets" binding:"required"` - DistinctAttributes int `json:"distinct_attributes" form:"distinct_attributes" binding:"required"` - LockOut int `json:"lock_out" form:"lock_out" binding:"required"` - Expiration int `json:"expiration" form:"expiration" binding:"required"` // seconds, -1 no expiration + MaxNkodeLen int `form:"max_nkode_len"` + MinNkodeLen int `form:"min_nkode_len"` + DistinctSets int `form:"distinct_sets"` + DistinctAttributes int `form:"distinct_attributes"` + LockOut int `form:"lock_out"` + Expiration int `form:"expiration"` // seconds, -1 no expiration } func NewDefaultNKodePolicy() NKodePolicy { diff --git a/handler/handler.go b/handler/handler.go index 59ba674..3826fd0 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -29,7 +29,7 @@ func (h *NkodeHandler) RegisterRoutes(r *gin.Engine) { r.Group("/v1/nkode") { r.POST("/create-new-customer", h.CreateNewCustomerHandler) - r.POST("/generate-signup-reset-interface", h.SignupResetHandler) + r.POST("/signup", h.SignupHandler) r.POST("/set-nkode", h.SetNKodeHandler) r.POST("/confirm-nkode", h.ConfirmNKodeHandler) r.POST("/get-login-interface", h.GetLoginInterfaceHandler) @@ -37,8 +37,9 @@ func (h *NkodeHandler) RegisterRoutes(r *gin.Engine) { r.POST("/renew-attributes", h.RenewAttributesHandler) r.POST("/random-svg-interface", h.RandomSvgInterfaceHandler) r.POST("/refresh-token", h.RefreshTokenHandler) - r.POST("/reset-nkode", h.ResetNKodeHandler) + r.POST("/forgot-nkode", h.ForgotNKodeHandler) r.POST("/signout", h.SignoutHandler) + r.POST("/reset", h.ResetHandler) } } @@ -55,12 +56,12 @@ func (h *NkodeHandler) CreateNewCustomerHandler(c *gin.Context) { return } h.Logger.Println("create new customer") - c.JSON(200, gin.H{"customer_id": customerId}) + c.JSON(200, gin.H{"customer_id": entities.CustomerIdToString(*customerId)}) } -func (h *NkodeHandler) SignupResetHandler(c *gin.Context) { +func (h *NkodeHandler) SignupHandler(c *gin.Context) { h.Logger.Println("generate signup reset interface") - var postBody models.SignupRestPostBody + var postBody models.SignupPostBody if err := c.ShouldBind(&postBody); err != nil { handleError(c, err) return @@ -86,7 +87,8 @@ func (h *NkodeHandler) SignupResetHandler(c *gin.Context) { c.String(400, malformedUserEmail) return } - resp, err := h.API.GenerateSignupResetInterface(userEmail, entities.CustomerId(customerId), kp, postBody.Reset) + + resp, err := h.API.GenerateSignupResetInterface(userEmail, entities.CustomerId(customerId), kp, false) if err != nil { handleError(c, err) return @@ -98,7 +100,7 @@ func (h *NkodeHandler) SignupResetHandler(c *gin.Context) { func (h *NkodeHandler) SetNKodeHandler(c *gin.Context) { h.Logger.Println("set nkode") var postBody models.SetNKodePost - if err := c.ShouldBind(&postBody); err != nil { + if err := c.ShouldBindJSON(&postBody); err != nil { handleError(c, err) return } @@ -125,7 +127,7 @@ func (h *NkodeHandler) SetNKodeHandler(c *gin.Context) { func (h *NkodeHandler) ConfirmNKodeHandler(c *gin.Context) { h.Logger.Println("confirm nkode") var postBody models.ConfirmNKodePost - if err := c.ShouldBind(&postBody); err != nil { + if err := c.ShouldBindJSON(&postBody); err != nil { handleError(c, err) return } @@ -177,7 +179,7 @@ func (h *NkodeHandler) LoginHandler(c *gin.Context) { h.Logger.Println("login") var loginPost models.LoginPost - if err := c.ShouldBind(&loginPost); err != nil { + if err := c.ShouldBindJSON(&loginPost); err != nil { handleError(c, err) return } @@ -259,25 +261,25 @@ func (h *NkodeHandler) RefreshTokenHandler(c *gin.Context) { c.JSON(200, gin.H{"access_token": accessToken}) } -func (h *NkodeHandler) ResetNKodeHandler(c *gin.Context) { - h.Logger.Println("reset nkode") - var resetNKodePost models.ResetNKodePost - if err := c.ShouldBind(&resetNKodePost); err != nil { +func (h *NkodeHandler) ForgotNKodeHandler(c *gin.Context) { + h.Logger.Println("forgot nkode") + var forgotNKodePost models.ForgotNKodePost + if err := c.ShouldBind(&forgotNKodePost); err != nil { handleError(c, err) return } - customerId, err := uuid.Parse(resetNKodePost.CustomerId) + customerId, err := uuid.Parse(forgotNKodePost.CustomerId) if err != nil { c.String(400, malformedCustomerId) return } - userEmail, err := entities.ParseEmail(resetNKodePost.UserEmail) + userEmail, err := entities.ParseEmail(forgotNKodePost.UserEmail) if err != nil { c.String(400, malformedUserEmail) return } - if err := h.API.ResetNKode(userEmail, entities.CustomerId(customerId)); err != nil { + if err := h.API.ForgotNKode(userEmail, entities.CustomerId(customerId)); err != nil { handleError(c, err) return } @@ -313,6 +315,59 @@ func (h *NkodeHandler) SignoutHandler(c *gin.Context) { c.Status(200) } +func (h *NkodeHandler) ResetHandler(c *gin.Context) { + h.Logger.Println("reset") + + token, err := getBearerToken(c) + if err != nil { + c.String(403, "forbidden") + return + } + resetClaims, err := security.ParseRestNKodeToken(token) + if err != nil { + handleError(c, err) + return + } + var postBody models.SignupPostBody + if err = c.ShouldBind(&postBody); err != nil { + handleError(c, err) + return + } + customerId, err := uuid.Parse(postBody.CustomerId) + if err != nil { + c.String(400, malformedCustomerId) + return + } + userEmail, err := entities.ParseEmail(postBody.UserEmail) + if err != nil { + c.String(400, malformedUserEmail) + return + } + if postBody.UserEmail != resetClaims.Subject || + postBody.CustomerId != resetClaims.Issuer { + c.String(403, "forbidden") + return + } + + kp := entities.KeypadDimension{ + AttrsPerKey: postBody.AttrsPerKey, + NumbOfKeys: postBody.NumbOfKeys, + } + + if err := kp.IsValidKeypadDimension(); err != nil { + c.String(400, invalidKeypadDimensions) + return + } + resp, err := h.API.GenerateSignupResetInterface(userEmail, entities.CustomerId(customerId), kp, true) + if err != nil { + handleError(c, err) + return + } + + c.JSON(200, resp) + +} + func handleError(c *gin.Context, err error) { log.Print("handling error: ", err) statusCode, _ := config.HttpErrMap[err] diff --git a/handler/handler_test.go b/handler/handler_test.go new file mode 100644 index 0000000..60b20fe --- /dev/null +++ b/handler/handler_test.go @@ -0,0 +1,292 @@ +package handler + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "git.infra.nkode.tech/dkelly/nkode-core/api" + "git.infra.nkode.tech/dkelly/nkode-core/email" + "git.infra.nkode.tech/dkelly/nkode-core/entities" + "git.infra.nkode.tech/dkelly/nkode-core/models" + "git.infra.nkode.tech/dkelly/nkode-core/repository" + "git.infra.nkode.tech/dkelly/nkode-core/security" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "log" + "net/http" + "net/http/httptest" + "os" + "testing" +) + +func TestNkodeAPI(t *testing.T) { + tr := NewTestRouter() + tr.Start() + defer func(tr *TestRouter) { + err := tr.Stop() + assert.NoError(t, err) + }(tr) + // *** Create New Customer *** + customerID, status, err := tr.CreateNewCustomerDefaultPolicy() + assert.NoError(t, err) + assert.Equal(t, 200, status) + attrPerKey := 9 + numKeys := 6 + userEmail := "test_username" + security.GenerateRandomString(12) + "@example.com" + reset := false + + // *** Signup *** + resp, status, err := tr.Signup(customerID, attrPerKey, numKeys, userEmail, reset) + assert.NoError(t, err) + assert.Equal(t, 200, status) + + passcodeLen := 4 + userPasscode := resp.UserIdxInterface[:passcodeLen] + kpSet := entities.KeypadDimension{ + AttrsPerKey: numKeys, + NumbOfKeys: numKeys, + } + setKeySelection, err := entities.SelectKeyByAttrIdx(resp.UserIdxInterface, userPasscode, kpSet) + assert.NoError(t, err) + + // *** Set nKode *** + confirmInterface, status, err := tr.SetNKode(customerID, setKeySelection, resp.SessionId) + assert.NoError(t, err) + assert.Equal(t, 200, status) + + confirmKeySelection, err := entities.SelectKeyByAttrIdx(confirmInterface, userPasscode, kpSet) + assert.NoError(t, err) + + // *** Confirm nKode *** + status, err = tr.ConfirmNKode(customerID, confirmKeySelection, resp.SessionId) + assert.NoError(t, err) + assert.Equal(t, 200, status) + + // *** Get Login Interface *** + loginInterface, status, err := tr.GetLoginInterface(userEmail, customerID) + assert.NoError(t, err) + assert.Equal(t, 200, status) + kp := entities.KeypadDimension{ + AttrsPerKey: attrPerKey, + NumbOfKeys: numKeys, + } + loginKeySelection, err := entities.SelectKeyByAttrIdx(loginInterface.UserIdxInterface, userPasscode, kp) + assert.NoError(t, err) + + // *** Login *** + tokens, status, err := tr.Login(customerID, userEmail, loginKeySelection) + assert.NoError(t, err) + assert.Equal(t, 200, status) + assert.NotEmpty(t, tokens.AccessToken) + assert.NotEmpty(t, tokens.RefreshToken) + + // *** Renew Attributes *** + +} + +type TestRouter struct { + Router *gin.Engine + EmailQueue *email.Queue + Repo *repository.SqliteRepository + Handler *NkodeHandler +} + +func NewTestRouter() *TestRouter { + gin.SetMode(gin.TestMode) + router := gin.Default() + logger := log.Default() + ctx := context.Background() + dbPath := os.Getenv("TEST_DB") + repo, err := repository.NewSqliteRepository(ctx, dbPath) + if err != nil { + log.Fatal(err) + } + sesClient := email.NewSESClient() + emailQueue := email.NewEmailQueue(email.EmailQueueBufferSize, email.MaxEmailsPerSecond, &sesClient) + nkodeAPI := api.NewNKodeAPI(repo, emailQueue) + h := NkodeHandler{ + API: nkodeAPI, + Logger: logger, + } + h.RegisterRoutes(router) + return &TestRouter{ + Handler: &h, + Router: router, + EmailQueue: emailQueue, + Repo: repo, + } +} + +func (r *TestRouter) Start() { + r.Repo.Start() + r.EmailQueue.Start() +} + +func (r *TestRouter) Stop() error { + r.EmailQueue.Stop() + return r.Repo.Stop() +} + +func (r *TestRouter) CreateNewCustomerDefaultPolicy() (string, int, error) { + p := entities.NewDefaultNKodePolicy() + body := bytes.NewBufferString(fmt.Sprintf( + "max_nkode_len=%d&min_nkode_len=%d&distinct_sets=%d&distinct_attributes=%d&lock_out=%d&expiration=%d", + p.MaxNkodeLen, + p.MinNkodeLen, + p.DistinctSets, + p.DistinctAttributes, + p.LockOut, + p.Expiration, + )) + req := httptest.NewRequest(http.MethodPost, "/create-new-customer", body) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rec := httptest.NewRecorder() + r.Router.ServeHTTP(rec, req) + var resp struct { + CustomerID string `json:"customer_id"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + return "", rec.Code, err + } + return resp.CustomerID, rec.Code, nil +} + +func (r *TestRouter) Signup( + customerID string, + attrsPerKey int, + numberOfKeys int, + userEmail string, + reset bool, +) (*entities.SignupResetInterface, int, error) { + body := bytes.NewBufferString(fmt.Sprintf( + "customer_id=%s&attrs_per_key=%d&numb_of_keys=%d&email=%s&reset=%t", + customerID, + attrsPerKey, + numberOfKeys, + userEmail, + reset, + )) + req := httptest.NewRequest(http.MethodPost, "/signup", body) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rec := httptest.NewRecorder() + r.Router.ServeHTTP(rec, req) + var resp entities.SignupResetInterface + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + return nil, rec.Code, err + } + return &resp, rec.Code, nil +} + +func (r *TestRouter) SetNKode( + customerID string, + selection []int, + sessionID string, +) ([]int, int, error) { + data := models.SetNKodePost{ + CustomerId: customerID, + KeySelection: selection, + SessionId: sessionID, + } + + body, err := json.Marshal(data) + if err != nil { + return nil, 0, err + } + req := httptest.NewRequest(http.MethodPost, "/set-nkode", bytes.NewBuffer(body)) + + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + r.Router.ServeHTTP(rec, req) + var resp struct { + UserInterface []int `json:"user_interface"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + return nil, rec.Code, err + } + return resp.UserInterface, rec.Code, nil +} + +func (r *TestRouter) ConfirmNKode( + customerID string, + selection entities.KeySelection, + sessionID string, +) (int, error) { + data := models.ConfirmNKodePost{ + CustomerId: customerID, + KeySelection: selection, + SessionId: sessionID, + } + body, err := json.Marshal(data) + if err != nil { + return 0, err + } + req := httptest.NewRequest(http.MethodPost, "/confirm-nkode", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + r.Router.ServeHTTP(rec, req) + return rec.Code, nil +} + +func (r *TestRouter) GetLoginInterface( + userEmail string, + customerID string, +) (entities.LoginInterface, int, error) { + body := bytes.NewBufferString(fmt.Sprintf( + "email=%s&customer_id=%s", + userEmail, + customerID, + )) + + req := httptest.NewRequest(http.MethodPost, "/get-login-interface", body) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rec := httptest.NewRecorder() + r.Router.ServeHTTP(rec, req) + var resp entities.LoginInterface + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + return entities.LoginInterface{}, rec.Code, err + } + return resp, rec.Code, nil +} + +func (r *TestRouter) Login( + customerID string, + userEmail string, + selection []int, +) (security.AuthenticationTokens, int, error) { + data := models.LoginPost{ + CustomerId: customerID, + UserEmail: userEmail, + KeySelection: selection, + } + body, err := json.Marshal(data) + if err != nil { + return security.AuthenticationTokens{}, 0, err + } + req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rec := httptest.NewRecorder() + r.Router.ServeHTTP(rec, req) + var resp security.AuthenticationTokens + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + return security.AuthenticationTokens{}, rec.Code, err + } + return resp, rec.Code, nil +} + +func (r *TestRouter) RenewAttributes( + customerID string, +) (int, error) { + data := models.RenewAttributesPost{ + CustomerId: customerID, + } + body, err := json.Marshal(data) + if err != nil { + return 0, err + } + req := httptest.NewRequest(http.MethodPost, "/renew-attributes", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + r.Router.ServeHTTP(rec, req) + return rec.Code, nil +} diff --git a/memCache/forgot_nkode.go b/memCache/forgot_nkode.go new file mode 100644 index 0000000..8640ae8 --- /dev/null +++ b/memCache/forgot_nkode.go @@ -0,0 +1,38 @@ +package memCache + +import ( + "git.infra.nkode.tech/dkelly/nkode-core/entities" + "github.com/patrickmn/go-cache" + "time" +) + +const ( + forgotExpiration = 5 * time.Minute + forgotCleanupInterval = 10 * time.Minute +) + +type ForgotNKodeCache struct { + innerCache *cache.Cache +} + +func NewForgotNKodeCache() ForgotNKodeCache { + forgotCache := cache.New(forgotExpiration, forgotCleanupInterval) + return ForgotNKodeCache{forgotCache} +} + +func (f *ForgotNKodeCache) Set(userEmail entities.UserEmail, customerId entities.CustomerId) { + f.innerCache.Set(key(userEmail, customerId), true, forgotExpiration) +} + +func (f *ForgotNKodeCache) Get(userEmail entities.UserEmail, customerId entities.CustomerId) bool { + _, found := f.innerCache.Get(key(userEmail, customerId)) + return found +} + +func (f *ForgotNKodeCache) Delete(userEmail entities.UserEmail, customerId entities.CustomerId) { + f.innerCache.Delete(key(userEmail, customerId)) +} + +func key(email entities.UserEmail, id entities.CustomerId) string { + return string(email) + entities.CustomerIdToString(id) +} diff --git a/models/models.go b/models/models.go index e4213fc..63865c3 100644 --- a/models/models.go +++ b/models/models.go @@ -15,24 +15,23 @@ type RefreshTokenResp struct { AccessToken string `form:"access_token" binding:"required"` } -type SignupRestPostBody struct { - CustomerId string `form:"customer_id" binding:"required"` - AttrsPerKey int `form:"attrs_per_key" binding:"required"` - NumbOfKeys int `form:"numb_of_keys" binding:"required"` - UserEmail string `form:"email" binding:"required"` - Reset bool `form:"reset" binding:"required"` +type SignupPostBody struct { + CustomerId string `form:"customer_id"` + AttrsPerKey int `form:"attrs_per_key"` + NumbOfKeys int `form:"numb_of_keys"` + UserEmail string `form:"email"` } type SetNKodePost struct { - CustomerId string `form:"customer_id" binding:"required"` - KeySelection []int `form:"key_selection" binding:"required"` - SessionId string `form:"session_id" binding:"required"` + CustomerId string `json:"customer_id" binding:"required"` + KeySelection []int `json:"key_selection" binding:"required"` + SessionId string `json:"session_id" binding:"required"` } type ConfirmNKodePost struct { - CustomerId string `form:"customer_id" binding:"required"` - KeySelection entities.KeySelection `form:"key_selection" binding:"required"` - SessionId string `form:"session_id" binding:"required"` + CustomerId string `json:"customer_id" binding:"required"` + KeySelection []int `json:"key_selection" binding:"required"` + SessionId string `json:"session_id" binding:"required"` } type LoginInterfacePost struct { @@ -41,16 +40,16 @@ type LoginInterfacePost struct { } type LoginPost struct { - CustomerId string `form:"customer_id" binding:"required"` - UserEmail string `form:"email" binding:"required"` - KeySelection entities.KeySelection `form:"key_selection" binding:"required"` + CustomerId string `json:"customer_id" binding:"required"` + UserEmail string `json:"email" binding:"required"` + KeySelection entities.KeySelection `json:"key_selection" binding:"required"` } type RenewAttributesPost struct { CustomerId string `form:"customer_id" binding:"required"` } -type ResetNKodePost struct { +type ForgotNKodePost struct { UserEmail string `form:"email" binding:"required"` CustomerId string `form:"customer_id" binding:"required"` } diff --git a/security/jwt_claims.go b/security/jwt_claims.go index a832847..3c7f0c8 100644 --- a/security/jwt_claims.go +++ b/security/jwt_claims.go @@ -65,6 +65,7 @@ func NewAuthenticationTokens(username string, customerId uuid.UUID) (Authenticat } func NewAccessClaim(username string, customerId uuid.UUID) jwt.RegisteredClaims { + // TODO: CHANGE ISSUER TO BE URL return jwt.RegisteredClaims{ Subject: username, Issuer: customerId.String(), @@ -85,6 +86,10 @@ func ParseRestNKodeToken(resetNKodeToken string) (*ResetNKodeClaims, error) { return parseJwt[*ResetNKodeClaims](resetNKodeToken, &ResetNKodeClaims{}) } +func ParseResetNKodeClaim(token string) (*ResetNKodeClaims, error) { + return parseJwt[*ResetNKodeClaims](token, &ResetNKodeClaims{}) +} + func parseJwt[T *ResetNKodeClaims | *jwt.RegisteredClaims](tokenStr string, claim jwt.Claims) (T, error) { token, err := jwt.ParseWithClaims(tokenStr, claim, func(token *jwt.Token) (interface{}, error) { return secret, nil