Skip to content

Commit

Permalink
feat: Implement user authentication
Browse files Browse the repository at this point in the history
Added JWT-based authentication, including login and registration functionalities.  A `GetUserByEmail` repository method was implemented to support login.  The user model now initializes with a default wallet balance of 0.  Updated dependencies to include JWT support and refreshed related packages.
  • Loading branch information
Ayobami6 committed Nov 9, 2024
1 parent 2ff7b1a commit 81ea29e
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 10 deletions.
9 changes: 5 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
Expand All @@ -33,11 +34,11 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver v1.17.1 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/crypto v0.29.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/sync v0.9.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.20.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
Expand Down Expand Up @@ -82,6 +84,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
Expand All @@ -92,6 +96,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand All @@ -101,6 +107,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand All @@ -109,6 +117,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
Expand Down
19 changes: 14 additions & 5 deletions internal/model/user.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package model

type User struct {
Id string `bson:"_id,omitempty" json:"id"`
Name string `bson:"name" json:"name"`
Email string `bson:"email" json:"email"`
Password string `bson:"password" json:"password"`
WalletBalance string `bson:"wallet_balance" json:"wallet_balance"`
Id string `bson:"_id,omitempty" json:"id"`
Name string `bson:"name" json:"name"`
Email string `bson:"email" json:"email"`
Password string `bson:"password" json:"password"`
WalletBalance float64 `bson:"wallet_balance" json:"wallet_balance"`
}

func NewUser(name string, email string, password string) *User {
return &User{
Name: name,
Email: email,
Password: password,
WalletBalance: 0.0,
}
}
10 changes: 10 additions & 0 deletions internal/repository/impls/user_repo_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,13 @@ func (u *UserRepositoryImpl) CreateUser(ctx context.Context, user *model.User) (
user.Id = newUser.InsertedID.(primitive.ObjectID).Hex()
return user, nil
}

func (u *UserRepositoryImpl) GetUserByEmail(ctx context.Context, email string) (*model.User, error) {
collection := u.db.Collection("users")
var user model.User
err := collection.FindOne(ctx, model.User{Email: email}).Decode(&user)
if err != nil {
return nil, err
}
return &user, nil
}
3 changes: 2 additions & 1 deletion internal/repository/user_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
)

type UserRepository interface {
GetUserByID(ctx context.Context, id string) (*model.User, error)
// GetUserByID(ctx context.Context, id string) (*model.User, error)
CreateUser(ctx context.Context, user *model.User) (*model.User, error)
GetUserByEmail(ctx context.Context, email string) (*model.User, error)
}
7 changes: 7 additions & 0 deletions internal/service/dto/create_user.dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dto

type CreateUserDTO struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
6 changes: 6 additions & 0 deletions internal/service/dto/login.dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dto

type LoginDTO struct {
Email string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
7 changes: 7 additions & 0 deletions internal/service/dto/login_response.dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dto

type LoginResponseDTO struct {
AccessToken string `json:"accessToken"`
ExpiresIn string `json:"expiresIn"`
TokenType string `json:"tokenType"`
}
92 changes: 92 additions & 0 deletions internal/service/impls/user_service_impl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package impls

import (
"context"
"errors"
"log"
"strconv"
"time"

"github.com/LoginX/SprayDash/config"
"github.com/LoginX/SprayDash/internal/model"
"github.com/LoginX/SprayDash/internal/repository"
"github.com/LoginX/SprayDash/internal/service/dto"
"github.com/LoginX/SprayDash/pkg/auth"
)

type UserServiceImpl struct {
// depends on
repo repository.UserRepository
}

func NewUserServiceImpl(repo repository.UserRepository) *UserServiceImpl {
return &UserServiceImpl{
repo: repo,
}
}

// implement interface methods

func (s *UserServiceImpl) Register(createUserDto dto.CreateUserDTO) (string, error) {
// need to hash the password
hashedPassword, hashErr := auth.HashPassword(createUserDto.Password)
if hashErr != nil {
return "", hashErr
}
// check if user already exists
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, existErr := s.repo.GetUserByEmail(ctx, createUserDto.Email)
if existErr == nil {
return "", errors.New("user already exists")
}

newUser := model.NewUser(createUserDto.Name, createUserDto.Email, hashedPassword)

// Call the CreateUser function with the context
_, err := s.repo.CreateUser(ctx, newUser)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return "", errors.New("request timed out")
}
return "", err
}
return "User registered successfully", nil

}

func (s *UserServiceImpl) Login(loginDto dto.LoginDTO) (dto.LoginResponseDTO, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// get user by the email
user, err := s.repo.GetUserByEmail(ctx, loginDto.Email)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return dto.LoginResponseDTO{}, errors.New("request timed out")
}
return dto.LoginResponseDTO{}, err
}
// compare password
if !auth.ComparePassword(user.Password, loginDto.Password) {
return dto.LoginResponseDTO{}, errors.New("invalid credentials")
}

secret := []byte(config.GetEnv("JWT_SECRET", "somesecret"))
exp, expErr := strconv.Atoi(config.GetEnv("JWT_EXP", "3600"))
if expErr != nil {
log.Println("Error converting JWT_EXP to int: ", expErr)
return dto.LoginResponseDTO{}, expErr
}
token, tokenErr := auth.CreateJWT(secret, exp, user)
if tokenErr != nil {
log.Println("Error creating JWT: ", tokenErr)
return dto.LoginResponseDTO{}, errors.New("error creating a token")
}
// return token
return dto.LoginResponseDTO{
AccessToken: token["token"],
ExpiresIn: token["expiresAt"],
TokenType: "jwt",
}, nil

}
8 changes: 8 additions & 0 deletions internal/service/user_service.go
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
package service

import "github.com/LoginX/SprayDash/internal/service/dto"

// user service interface
type UserService interface {
Login(loginDto dto.LoginDTO) (string, error)
Register(createUserDto dto.CreateUserDTO) (dto.LoginResponseDTO, error)
}
29 changes: 29 additions & 0 deletions pkg/auth/auth.go
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
package auth

import (
"time"

"github.com/LoginX/SprayDash/internal/model"
"github.com/golang-jwt/jwt/v5"
)

type contextKey string

var UserKey contextKey = "user"

func CreateJWT(secret []byte, expiration int, user *model.User) (map[string]string, error) {
expAt := time.Now().Add(time.Duration(expiration) * time.Minute).Unix()
// create te token claim
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user": user,
"exp": expAt,
})
tokenString, err := token.SignedString(secret)
if err != nil {
return nil, err
}
data := map[string]string{
"token": tokenString,
"expiresAt": time.Unix(expAt, 0).String(),
}
return data, nil
}
18 changes: 18 additions & 0 deletions pkg/auth/password.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package auth

import (
"golang.org/x/crypto/bcrypt"
)

func HashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}

func ComparePassword(hashedPassword string, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}
Binary file removed tmp/main.exe
Binary file not shown.

0 comments on commit 81ea29e

Please sign in to comment.