impl service layer for auth

This commit is contained in:
lafetz 2025-03-28 01:30:55 +03:00
parent 63b443171d
commit e8f0e43836
16 changed files with 372 additions and 12 deletions

View File

@ -4,7 +4,7 @@ CREATE TABLE users (
last_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL,
phone_number VARCHAR(20) UNIQUE NOT NULL, phone_number VARCHAR(20) UNIQUE NOT NULL,
password TEXT NOT NULL, password BYTEA NOT NULL,
role VARCHAR(50) NOT NULL, role VARCHAR(50) NOT NULL,
verified BOOLEAN DEFAULT FALSE, verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP , created_at TIMESTAMP ,

View File

@ -14,7 +14,7 @@ type User struct {
LastName string LastName string
Email string Email string
PhoneNumber string PhoneNumber string
Password string Password []byte
Role string Role string
Verified pgtype.Bool Verified pgtype.Bool
CreatedAt pgtype.Timestamp CreatedAt pgtype.Timestamp

View File

@ -22,7 +22,7 @@ type CreateUserParams struct {
LastName string LastName string
Email string Email string
PhoneNumber string PhoneNumber string
Password string Password []byte
Role string Role string
Verified pgtype.Bool Verified pgtype.Bool
} }
@ -129,7 +129,7 @@ type UpdateUserParams struct {
LastName string LastName string
Email string Email string
PhoneNumber string PhoneNumber string
Password string Password []byte
Role string Role string
Verified pgtype.Bool Verified pgtype.Bool
} }

10
go.mod
View File

@ -5,32 +5,38 @@ go 1.24.1
require ( require (
github.com/bytedance/sonic v1.13.2 github.com/bytedance/sonic v1.13.2
github.com/gofiber/fiber/v2 v2.52.6 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/jackc/pgx/v5 v5.7.4
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/stretchr/testify v1.8.4
golang.org/x/crypto v0.32.0
) )
require ( require (
github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // 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/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.0.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-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // 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/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/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // 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/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.21.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

11
go.sum
View File

@ -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 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= 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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 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 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 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/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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 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= 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/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 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 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 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1 +1,12 @@
package domain package domain
import "time"
type RefreshToken struct {
ID int64
UserID int64
Token string
ExpiresAt time.Time
CreatedAt time.Time
Revoked bool
}

View File

@ -1,12 +1,16 @@
package domain package domain
import "time"
type User struct { type User struct {
ID int64 ID int64
FirstName string FirstName string
LastName string LastName string
Email string Email string
PhoneNumber string PhoneNumber string
Password string Password []byte
Role string Role string
Verified bool Verified bool
CreatedAt time.Time
UpdatedAt time.Time
} }

View File

@ -13,8 +13,8 @@ func (s *Store) CreateUser(ctx context.Context, firstName, lastName, email, phon
LastName: lastName, LastName: lastName,
Email: email, Email: email,
PhoneNumber: phoneNumber, PhoneNumber: phoneNumber,
Password: password, // Password: password,
Role: role, Role: role,
}) })
if err != nil { if err != nil {
return domain.User{}, err return domain.User{}, err
@ -71,8 +71,8 @@ func (s *Store) UpdateUser(ctx context.Context, id int64, firstName, lastName, e
LastName: lastName, LastName: lastName,
Email: email, Email: email,
PhoneNumber: phoneNumber, PhoneNumber: phoneNumber,
Password: password, // Password: password,
Role: role, Role: role,
}) })
return err return err
} }

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -12,4 +12,6 @@ type UserStore interface {
GetAllUsers(ctx context.Context) ([]domain.User, error) GetAllUsers(ctx context.Context) ([]domain.User, error)
UpdateUser(ctx context.Context, id int64, firstName, lastName, email, phoneNumber, password, role string, verified bool) error UpdateUser(ctx context.Context, id int64, firstName, lastName, email, phoneNumber, password, role string, verified bool) error
DeleteUser(ctx context.Context, id int64) error DeleteUser(ctx context.Context, id int64) error
//
//GetUserByEmailPhone(ctx context.Context, emailPhone EmailPhone) (domain.User, error)
} }

View File

@ -4,13 +4,14 @@ import (
"context" "context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"golang.org/x/crypto/bcrypt"
) )
type Service struct { type Service struct {
userStore UserStore userStore UserStore
} }
func NewService(userStore UserStore) *Service { func NewService(userStore UserStore, RefreshExpiry int) *Service {
return &Service{ return &Service{
userStore: userStore, 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 { func (s *Service) DeleteUser(ctx context.Context, id int64) error {
return s.userStore.DeleteUser(ctx, id) 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
}

View File

@ -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
}

View File

@ -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
// }