From e8f0e43836d6dc6e1e37bc5770acec8b1b73788c Mon Sep 17 00:00:00 2001 From: lafetz Date: Fri, 28 Mar 2025 01:30:55 +0300 Subject: [PATCH] impl service layer for auth --- db/migrations/000001_fortune.up.sql | 2 +- gen/db/models.go | 2 +- gen/db/user.sql.go | 4 +- go.mod | 10 +- go.sum | 11 ++ internal/domain/auth.go | 11 ++ internal/domain/user.go | 6 +- internal/repository/user.go | 8 +- internal/services/authentication/impl.go | 131 ++++++++++++++++++ internal/services/authentication/port.go | 16 +++ internal/services/authentication/service.go | 28 ++++ internal/services/user/port.go | 2 + internal/services/user/service.go | 11 +- internal/web_server/jwt/jwt.go | 53 +++++++ internal/web_server/jwt/jwt_test.go | 89 ++++++++++++ .../web_server/{app_routes.go => routes.go} | 0 16 files changed, 372 insertions(+), 12 deletions(-) create mode 100644 internal/services/authentication/impl.go create mode 100644 internal/services/authentication/port.go create mode 100644 internal/services/authentication/service.go create mode 100644 internal/web_server/jwt/jwt.go create mode 100644 internal/web_server/jwt/jwt_test.go rename internal/web_server/{app_routes.go => routes.go} (100%) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index bfd54da..b1df3e1 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -4,7 +4,7 @@ CREATE TABLE users ( last_name VARCHAR(255) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, phone_number VARCHAR(20) UNIQUE NOT NULL, - password TEXT NOT NULL, + password BYTEA NOT NULL, role VARCHAR(50) NOT NULL, verified BOOLEAN DEFAULT FALSE, created_at TIMESTAMP , diff --git a/gen/db/models.go b/gen/db/models.go index 307326d..82db472 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -14,7 +14,7 @@ type User struct { LastName string Email string PhoneNumber string - Password string + Password []byte Role string Verified pgtype.Bool CreatedAt pgtype.Timestamp diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 1608a32..c1b551e 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -22,7 +22,7 @@ type CreateUserParams struct { LastName string Email string PhoneNumber string - Password string + Password []byte Role string Verified pgtype.Bool } @@ -129,7 +129,7 @@ type UpdateUserParams struct { LastName string Email string PhoneNumber string - Password string + Password []byte Role string Verified pgtype.Bool } diff --git a/go.mod b/go.mod index 18620e4..73cdf76 100644 --- a/go.mod +++ b/go.mod @@ -5,32 +5,38 @@ go 1.24.1 require ( github.com/bytedance/sonic v1.13.2 github.com/gofiber/fiber/v2 v2.52.6 + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/jackc/pgx/v5 v5.7.4 github.com/joho/godotenv v1.5.1 + github.com/stretchr/testify v1.8.4 + golang.org/x/crypto v0.32.0 ) require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/stretchr/testify v1.8.4 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect - golang.org/x/crypto v0.32.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9d6be49..d53d292 100644 --- a/go.sum +++ b/go.sum @@ -8,11 +8,14 @@ github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFos github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -30,6 +33,10 @@ github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ib github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -41,6 +48,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -72,6 +81,8 @@ golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/domain/auth.go b/internal/domain/auth.go index 4188b5a..513ff8e 100644 --- a/internal/domain/auth.go +++ b/internal/domain/auth.go @@ -1 +1,12 @@ package domain + +import "time" + +type RefreshToken struct { + ID int64 + UserID int64 + Token string + ExpiresAt time.Time + CreatedAt time.Time + Revoked bool +} diff --git a/internal/domain/user.go b/internal/domain/user.go index 1e6b181..21d1a77 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -1,12 +1,16 @@ package domain +import "time" + type User struct { ID int64 FirstName string LastName string Email string PhoneNumber string - Password string + Password []byte Role string Verified bool + CreatedAt time.Time + UpdatedAt time.Time } diff --git a/internal/repository/user.go b/internal/repository/user.go index b19798f..d6f7a36 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -13,8 +13,8 @@ func (s *Store) CreateUser(ctx context.Context, firstName, lastName, email, phon LastName: lastName, Email: email, PhoneNumber: phoneNumber, - Password: password, - Role: role, + // Password: password, + Role: role, }) if err != nil { return domain.User{}, err @@ -71,8 +71,8 @@ func (s *Store) UpdateUser(ctx context.Context, id int64, firstName, lastName, e LastName: lastName, Email: email, PhoneNumber: phoneNumber, - Password: password, - Role: role, + // Password: password, + Role: role, }) return err } diff --git a/internal/services/authentication/impl.go b/internal/services/authentication/impl.go new file mode 100644 index 0000000..1bac295 --- /dev/null +++ b/internal/services/authentication/impl.go @@ -0,0 +1,131 @@ +package authentication + +import ( + "context" + "crypto/rand" + "encoding/base32" + "errors" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "golang.org/x/crypto/bcrypt" +) + +var ( + ErrInvalidPassword = errors.New("incorrect password") + ErrUserNotFound = errors.New("user not found") + ErrExpiredToken = errors.New("token expired") + ErrRefreshTokenNotFound = errors.New("refresh token not found") // i.e login again +) + +func (s *Service) Login(ctx context.Context, emailPhone EmailPhone, password string) (string, error) { + user, err := s.userStore.GetUserByEmailPhone(ctx, emailPhone) + if err != nil { + return "", err + } + err = matchPassword(password, user.Password) + if err != nil { + return "", err + } + // //create session + // accessToken, err := CreateJwt(strconv.Itoa(int(user.ID)), s.jwtConfig.JwtAccessKey, s.jwtConfig.JwtAccessExpiry) + // if err != nil { + // return Tokens{}, err + // } + refreshToken, err := generateRefreshToken() + if err != nil { + return "", err + } + err = s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{ + Token: refreshToken, + UserID: user.ID, + CreatedAt: time.Now(), + ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second), + }) + if err != nil { + return "", err + } + return refreshToken, nil +} + +func (s *Service) RefreshToken(ctx context.Context, refToken string) (string, error) { + // us, err := ParseJwt(tokens.RefreshToken, s.jwtConfig.JwtAccessKey) + // if err == nil { + // return Tokens{}, err + // } + // if !errors.Is(err, ErrExpiredToken) { + // return Tokens{}, err + // } + token, err := s.tokenStore.GetRefreshToken(ctx, refToken) + if err != nil { + return "", err + } + if token.Revoked { + return "", ErrRefreshTokenNotFound + } + if token.ExpiresAt.Before(time.Now()) { + return "", ErrExpiredToken + } + // + // naccessToken, err := CreateJwt(token., s.jwtConfig.JwtAccessKey, s.jwtConfig.JwtAccessExpiry) + // if err != nil { + // return Tokens{}, err + // } + newRefToken, err := generateRefreshToken() + if err != nil { + return "", err + } + + // ntokens := Tokens{ + // AccessToken: naccessToken, + // RefreshToken: nrefreshToken, + // } + + err = s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{ + Token: newRefToken, + UserID: token.UserID, + CreatedAt: time.Now(), + ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second), + }) + if err != nil { + return "", err + } + return newRefToken, nil +} +func (s *Service) Logout(ctx context.Context, refToken string) error { + token, err := s.tokenStore.GetRefreshToken(ctx, refToken) + if err != nil { + return err + } + if token.Revoked { + return ErrRefreshTokenNotFound + } + if token.ExpiresAt.Before(time.Now()) { + return ErrExpiredToken + } + + return s.tokenStore.RevokeRefreshToken(ctx, refToken) +} + +func matchPassword(plaintextPassword string, hash []byte) error { + err := bcrypt.CompareHashAndPassword(hash, []byte(plaintextPassword)) + if err != nil { + switch { + case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword): + return ErrInvalidPassword + default: + return err + } + } + + return err +} +func generateRefreshToken() (string, error) { + randomBytes := make([]byte, 32) + _, err := rand.Read(randomBytes) + if err != nil { + return "", err + } + plaintext := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randomBytes) + return plaintext, nil +} diff --git a/internal/services/authentication/port.go b/internal/services/authentication/port.go new file mode 100644 index 0000000..a9f3136 --- /dev/null +++ b/internal/services/authentication/port.go @@ -0,0 +1,16 @@ +package authentication + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type UserStore interface { + GetUserByEmailPhone(ctx context.Context, emailPhone EmailPhone) (domain.User, error) +} +type TokenStore interface { + CreateRefreshToken(ctx context.Context, rt domain.RefreshToken) error + GetRefreshToken(ctx context.Context, token string) (domain.RefreshToken, error) + RevokeRefreshToken(ctx context.Context, token string) error +} diff --git a/internal/services/authentication/service.go b/internal/services/authentication/service.go new file mode 100644 index 0000000..e40b75b --- /dev/null +++ b/internal/services/authentication/service.go @@ -0,0 +1,28 @@ +package authentication + +type EmailPhone struct { + Email ValidString + PhoneNumber ValidString + Password ValidString +} +type ValidString struct { + Value string + Valid bool +} +type Tokens struct { + AccessToken string + RefreshToken string +} +type Service struct { + userStore UserStore + tokenStore TokenStore + RefreshExpiry int +} + +func NewService(userStore UserStore, tokenStore TokenStore, RefreshExpiry int) *Service { + return &Service{ + userStore: userStore, + tokenStore: tokenStore, + RefreshExpiry: RefreshExpiry, + } +} diff --git a/internal/services/user/port.go b/internal/services/user/port.go index 34700ad..b6f20cf 100644 --- a/internal/services/user/port.go +++ b/internal/services/user/port.go @@ -12,4 +12,6 @@ type UserStore interface { GetAllUsers(ctx context.Context) ([]domain.User, error) UpdateUser(ctx context.Context, id int64, firstName, lastName, email, phoneNumber, password, role string, verified bool) error DeleteUser(ctx context.Context, id int64) error + // + //GetUserByEmailPhone(ctx context.Context, emailPhone EmailPhone) (domain.User, error) } diff --git a/internal/services/user/service.go b/internal/services/user/service.go index d427b66..8232bbb 100644 --- a/internal/services/user/service.go +++ b/internal/services/user/service.go @@ -4,13 +4,14 @@ import ( "context" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "golang.org/x/crypto/bcrypt" ) type Service struct { userStore UserStore } -func NewService(userStore UserStore) *Service { +func NewService(userStore UserStore, RefreshExpiry int) *Service { return &Service{ userStore: userStore, } @@ -31,3 +32,11 @@ func (s *Service) UpdateUser(ctx context.Context, id int64, firstName, lastName, func (s *Service) DeleteUser(ctx context.Context, id int64) error { return s.userStore.DeleteUser(ctx, id) } +func hashPassword(plaintextPassword string) ([]byte, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12) + if err != nil { + return []byte{}, err + } + + return hash, nil +} diff --git a/internal/web_server/jwt/jwt.go b/internal/web_server/jwt/jwt.go new file mode 100644 index 0000000..4dd89e1 --- /dev/null +++ b/internal/web_server/jwt/jwt.go @@ -0,0 +1,53 @@ +package user + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +// type UserToken struct { +// UserId string +// } +var ( + ErrExpiredToken = errors.New("token expired") + ErrMalformedToken = errors.New("token malformed") + ErrTokenNotExpired = errors.New("token not expired") + ErrRefreshTokenNotFound = errors.New("refresh token not found") // i.e login again +) + +type UserClaim struct { + jwt.RegisteredClaims + UserId string +} + +func CreateJwt(userId string, key string, expiry int) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaim{RegisteredClaims: jwt.RegisteredClaims{Issuer: "github.com/lafetz/snippitstash", + IssuedAt: jwt.NewNumericDate(time.Now()), + Audience: jwt.ClaimStrings{"fortune.com"}, + NotBefore: jwt.NewNumericDate(time.Now()), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expiry) * time.Second))}, + UserId: userId, + }) + jwtToken, err := token.SignedString([]byte(key)) // + return jwtToken, err +} +func ParseJwt(jwtToken string, key string) (*UserClaim, error) { + token, err := jwt.ParseWithClaims(jwtToken, &UserClaim{}, func(token *jwt.Token) (interface{}, error) { + return []byte(key), nil + }) + if err != nil { + if errors.Is(err, jwt.ErrTokenExpired) { + return nil, ErrExpiredToken + } + if errors.Is(err, jwt.ErrTokenMalformed) { + return nil, ErrMalformedToken + } + return nil, err + } + if claims, ok := token.Claims.(*UserClaim); ok && token.Valid { + return claims, nil + } + return nil, err +} diff --git a/internal/web_server/jwt/jwt_test.go b/internal/web_server/jwt/jwt_test.go new file mode 100644 index 0000000..45d1dcc --- /dev/null +++ b/internal/web_server/jwt/jwt_test.go @@ -0,0 +1,89 @@ +package user + +// func TestCreateJwt(t *testing.T) { +// // Define a user to test +// user := &domain.User{ +// ID: 123, +// } + +// // Secret key used for signing the JWT +// secretKey := "secret" + +// // Token expiry time (in seconds) +// expiry := 3600 // 1 hour + +// // Call CreateJwt function +// tokenString, err := CreateJwt(user, secretKey, expiry) + +// // Assertions +// assert.NoError(t, err, "Error should be nil when creating a JWT") +// assert.NotEmpty(t, tokenString, "Token string should not be empty") + +// // Parse the token back and verify its claims +// claims, err := ParseJwt(tokenString, secretKey) +// assert.NoError(t, err, "Error should be nil when parsing the JWT") +// assert.Equal(t, strconv.Itoa(int(user.ID)), claims.UserId, "User ID should match") +// assert.Equal(t, "github.com/lafetz/snippitstash", claims.Issuer, "Issuer should match") +// assert.True(t, claims.ExpiresAt.Time.After(time.Now()), "Token should not be expired yet") +// expectedExpiryTime := time.Now().Add(time.Duration(expiry) * time.Second) +// // Allow for a small margin of error due to the time delay in generating the token +// assert.True(t, claims.ExpiresAt.Time.Before(expectedExpiryTime.Add(1*time.Second)), +// "Token expiry time should be within the expected range") +// assert.True(t, claims.ExpiresAt.Time.After(expectedExpiryTime.Add(-1*time.Second)), +// "Token expiry time should be within the expected range") +// } +// func TestParseJwt(t *testing.T) { +// // Define a user to test +// user := &domain.User{ +// ID: 123, +// } + +// // Secret key used for signing the JWT +// secretKey := "secret" + +// // Token expiry time (in seconds) +// expiry := 3600 // 1 hour + +// // Generate a token using the CreateJwt function +// tokenString, err := CreateJwt(user, secretKey, expiry) +// assert.NoError(t, err, "Error should be nil when creating a JWT") +// assert.NotEmpty(t, tokenString, "Token string should not be empty") + +// // Now, we will parse the token +// claims, err := ParseJwt(tokenString, secretKey) +// assert.NoError(t, err, "Error should be nil when parsing the JWT") +// assert.NotNil(t, claims, "Claims should not be nil") + +// // Verify that the claims match the user and other values +// assert.Equal(t, strconv.Itoa(int(user.ID)), claims.UserId, "User ID should match") +// assert.Equal(t, "github.com/lafetz/snippitstash", claims.Issuer, "Issuer should match") +// assert.Equal(t, "fortune.com", claims.Audience[0], "Audience should match") +// assert.True(t, claims.ExpiresAt.Time.After(time.Now()), "Token should not be expired yet") + +// // Ensure the parsing fails when using an invalid token +// invalidToken := tokenString + "invalid" +// _, err = ParseJwt(invalidToken, secretKey) +// assert.Error(t, err, "Parsing an invalid token should return an error") +// } +// func TestParseJwte(t *testing.T) { +// // Define user and key +// user := &domain.User{ID: 1} +// key := "secretkey" + +// // Test valid token (not expired) +// validJwt, err := CreateJwt(user, key, 4) // Set expiry to 10 seconds +// assert.NoError(t, err) + +// // Test if the token is parsed correctly +// claims, err := ParseJwt(validJwt, key) +// assert.NoError(t, err) +// assert.Equal(t, "1", claims.UserId) + +// // Wait for token to expire +// time.Sleep(5 * time.Second) // Wait longer than the expiry time to test expiration + +// // Test expired token +// _, err = ParseJwt(validJwt, key) + +// assert.Error(t, jwt.ErrTokenExpired) // Expect an error because the token should be expired +// } diff --git a/internal/web_server/app_routes.go b/internal/web_server/routes.go similarity index 100% rename from internal/web_server/app_routes.go rename to internal/web_server/routes.go