Skip to content

Commit

Permalink
implement forget/reset password
Browse files Browse the repository at this point in the history
  • Loading branch information
khanzadimahdi committed Apr 5, 2024
1 parent bd965de commit 003f990
Show file tree
Hide file tree
Showing 32 changed files with 1,047 additions and 65 deletions.
7 changes: 7 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ MONGO_HOST=mongodb
MONGO_PORT=27017
MONGO_DATABASE_NAME=blog

# mail
MAIL_SMTP_FROM=[email protected]
MAIL_SMTP_USERNAME=test
MAIL_SMTP_PASSWORD=test
MAIL_SMTP_HOST=mailserver
MAIL_SMTP_PORT=1025

## frontend
NUXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8000
NUXT_INTERNAL_API_BASE_URL=http://app
7 changes: 7 additions & 0 deletions backend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,10 @@ MONGO_PASSWORD=test
MONGO_HOST=mongodb
MONGO_PORT=27017
MONGO_DATABASE_NAME=blog

# mail
MAIL_SMTP_FROM=[email protected]
MAIL_SMTP_USERNAME=test
MAIL_SMTP_PASSWORD=test
MAIL_SMTP_HOST=mailserver
MAIL_SMTP_PORT=1025
5 changes: 3 additions & 2 deletions backend/application/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import (
)

const (
AccessToken = "access"
RefreshToken = "refresh"
AccessToken = "access"
RefreshToken = "refresh"
ResetPasswordToken = "reset-password"
)

type authKey struct{}
Expand Down
11 changes: 11 additions & 0 deletions backend/application/auth/forgetpassword/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package forgetpassword

type validationErrors map[string]string

type Request struct {
Identity string `json:"identity"`
}

func (r *Request) Validate() (bool, validationErrors) {
return true, nil
}
5 changes: 5 additions & 0 deletions backend/application/auth/forgetpassword/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package forgetpassword

type ForgetResponse struct {
ValidationErrors validationErrors `json:"errors,omitempty"`
}
75 changes: 75 additions & 0 deletions backend/application/auth/forgetpassword/usecase.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package forgetpassword

import (
"bytes"
"encoding/base64"
"time"

"github.com/khanzadimahdi/testproject/application/auth"
"github.com/khanzadimahdi/testproject/domain"
"github.com/khanzadimahdi/testproject/domain/user"
"github.com/khanzadimahdi/testproject/infrastructure/jwt"
)

type UseCase struct {
userRepository user.Repository
jwt *jwt.JWT
mailer domain.Mailer
mailFrom string
}

func NewUseCase(
userRepository user.Repository,
JWT *jwt.JWT,
mailer domain.Mailer,
mailFrom string,
) *UseCase {
return &UseCase{
userRepository: userRepository,
jwt: JWT,
mailer: mailer,
mailFrom: mailFrom,
}
}

func (uc *UseCase) SendResetToken(request Request) (*ForgetResponse, error) {
if ok, validation := request.Validate(); !ok {
return &ForgetResponse{
ValidationErrors: validation,
}, nil
}

u, err := uc.userRepository.GetOneByIdentity(request.Identity)
if err != nil {
return nil, err
}

resetPasswordToken, err := uc.resetPasswordToken(u)
if err != nil {
return nil, err
}

resetPasswordToken = base64.URLEncoding.EncodeToString([]byte(resetPasswordToken))

var msg bytes.Buffer
if _, err := msg.WriteString(resetPasswordToken); err != nil {
return nil, err
}

if err := uc.mailer.SendMail(uc.mailFrom, u.Email, "Reset Password", msg.Bytes()); err != nil {
return nil, err
}

return &ForgetResponse{}, nil
}

func (uc *UseCase) resetPasswordToken(u user.User) (string, error) {
b := jwt.NewClaimsBuilder()
b.SetSubject(u.UUID)
b.SetNotBefore(time.Now())
b.SetExpirationTime(time.Now().Add(10 * time.Minute))
b.SetIssuedAt(time.Now())
b.SetAudience([]string{auth.ResetPasswordToken})

return uc.jwt.Generate(b.Build())
}
101 changes: 101 additions & 0 deletions backend/application/auth/forgetpassword/usecase_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package forgetpassword

import (
"errors"
"testing"

"github.com/khanzadimahdi/testproject/domain"
"github.com/khanzadimahdi/testproject/domain/user"
"github.com/khanzadimahdi/testproject/infrastructure/crypto/ecdsa"
"github.com/khanzadimahdi/testproject/infrastructure/jwt"
)

func TestUseCase_SendResetToken(t *testing.T) {
privateKey, err := ecdsa.Generate()
if err != nil {
t.Error("unexpected error")
}
j := jwt.NewJWT(privateKey, privateKey.Public())

mailFrom := "[email protected]"

request := Request{
Identity: "[email protected]",
}

t.Run("mails reset password token", func(t *testing.T) {
userRepository := &MockUserRepository{}
mailer := &MockMailer{}

usecase := NewUseCase(userRepository, j, mailer, mailFrom)
response, err := usecase.SendResetToken(request)

if err != nil {
t.Error("unexpected error")
}

if response == nil {
t.Errorf("response not exists")
}
})

t.Run("error on finding user", func(t *testing.T) {
userRepository := &MockUserRepository{
GetOneByIdentityErr: domain.ErrNotExists,
}
mailer := &MockMailer{}

usecase := NewUseCase(userRepository, j, mailer, mailFrom)
response, err := usecase.SendResetToken(request)

if err != userRepository.GetOneByIdentityErr {
t.Error("expected an error", userRepository.GetOneByIdentityErr)
}

if response != nil {
t.Errorf("expected response to be nil but got %#v", response)
}
})

t.Run("sending email fails", func(t *testing.T) {
userRepository := &MockUserRepository{}
mailer := &MockMailer{SendMailErr: errors.New("can't send mail")}

usecase := NewUseCase(userRepository, j, mailer, mailFrom)
response, err := usecase.SendResetToken(request)

if err != mailer.SendMailErr {
t.Error("expected an error", mailer.SendMailErr)
}

if response != nil {
t.Errorf("expected response to be nil but got %#v", response)
}
})

}

type MockUserRepository struct {
user.Repository

GetOneByIdentityErr error
}

func (r *MockUserRepository) GetOneByIdentity(username string) (user.User, error) {
if r.GetOneByIdentityErr != nil {
return user.User{}, r.GetOneByIdentityErr
}

return user.User{
UUID: "018ead22-d9d3-7e78-8b52-174c06ee1528",
}, nil
}

type MockMailer struct {
domain.Mailer
SendMailErr error
}

func (r *MockMailer) SendMail(from string, to string, subject string, body []byte) error {
return r.SendMailErr
}
2 changes: 1 addition & 1 deletion backend/application/auth/login/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package login
type validationErrors map[string]string

type Request struct {
Username string `json:"username"`
Identity string `json:"identity"`
Password string `json:"password"`
}

Expand Down
15 changes: 9 additions & 6 deletions backend/application/auth/login/usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@ import (
"time"

"github.com/khanzadimahdi/testproject/application/auth"
"github.com/khanzadimahdi/testproject/domain/password"
"github.com/khanzadimahdi/testproject/domain/user"
"github.com/khanzadimahdi/testproject/infrastructure/jwt"
)

type UseCase struct {
userRepository user.Repository
JWT *jwt.JWT
Hasher password.Hasher
}

func NewUseCase(userRepository user.Repository, JWT *jwt.JWT) *UseCase {
func NewUseCase(userRepository user.Repository, JWT *jwt.JWT, hasher password.Hasher) *UseCase {
return &UseCase{
userRepository: userRepository,
JWT: JWT,
Hasher: hasher,
}
}

Expand All @@ -27,15 +30,15 @@ func (uc *UseCase) Login(request Request) (*LoginResponse, error) {
}, nil
}

u, err := uc.userRepository.GetOneByUsername(request.Username)
u, err := uc.userRepository.GetOneByIdentity(request.Identity)
if err != nil {
return nil, err
}

if !uc.passwordIsValid(u, request.Password) {
if !uc.passwordIsValid(u, []byte(request.Password)) {
return &LoginResponse{
ValidationErrors: validationErrors{
"username": "username or password is not wrong",
"identity": "your identity or password is wrong",
},
}, nil
}
Expand All @@ -56,8 +59,8 @@ func (uc *UseCase) Login(request Request) (*LoginResponse, error) {
}, nil
}

func (uc *UseCase) passwordIsValid(u user.User, password string) bool {
return u.Password == password
func (uc *UseCase) passwordIsValid(u user.User, password []byte) bool {
return uc.Hasher.Equal(password, u.PasswordHash.Value, u.PasswordHash.Salt)
}

func (uc *UseCase) generateAccessToken(u user.User) (string, error) {
Expand Down
38 changes: 27 additions & 11 deletions backend/application/auth/login/usecase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,34 @@ import (
"errors"
"testing"

"github.com/khanzadimahdi/testproject/domain/password"
"github.com/khanzadimahdi/testproject/domain/user"
"github.com/khanzadimahdi/testproject/infrastructure/crypto/ecdsa"
"github.com/khanzadimahdi/testproject/infrastructure/jwt"
)

func TestUseCase_GetArticle(t *testing.T) {
func TestUseCase_Login(t *testing.T) {
privateKey, err := ecdsa.Generate()
if err != nil {
t.Error("unexpected error")
}

j := jwt.NewJWT(privateKey, privateKey.Public())
h := &MockHasher{}

t.Run("returns jwt tokens", func(t *testing.T) {
repository := MockUserRepository{}

usecase := NewUseCase(&repository, j)
usecase := NewUseCase(&repository, j, h)

request := Request{
Username: "test-username",
Identity: "test-username",
Password: "test-password",
}

response, err := usecase.Login(request)

if repository.GetOneByUsernameCount != 1 {
if repository.GetOneByIdentityCount != 1 {
t.Error("unexpected number of calls")
}

Expand Down Expand Up @@ -74,16 +76,16 @@ func TestUseCase_GetArticle(t *testing.T) {
GetOneErr: errors.New("user with given username found"),
}

usecase := NewUseCase(&repository, j)
usecase := NewUseCase(&repository, j, h)

request := Request{
Username: "test-username",
Identity: "test-username",
Password: "test-password",
}

response, err := usecase.Login(request)

if repository.GetOneByUsernameCount != 1 {
if repository.GetOneByIdentityCount != 1 {
t.Error("unexpected number of calls")
}

Expand All @@ -100,19 +102,33 @@ func TestUseCase_GetArticle(t *testing.T) {
type MockUserRepository struct {
user.Repository

GetOneByUsernameCount uint
GetOneByIdentityCount uint
GetOneErr error
}

func (r *MockUserRepository) GetOneByUsername(username string) (user.User, error) {
r.GetOneByUsernameCount++
func (r *MockUserRepository) GetOneByIdentity(username string) (user.User, error) {
r.GetOneByIdentityCount++

if r.GetOneErr != nil {
return user.User{}, r.GetOneErr
}

return user.User{
Username: username,
Password: "test-password",
PasswordHash: password.Hash{
Value: []byte("test-password"),
Salt: []byte("test-salt"),
},
}, nil
}

type MockHasher struct {
}

func (m *MockHasher) Hash(value, salt []byte) []byte {
return []byte("random hash")
}

func (m *MockHasher) Equal(value, hash, salt []byte) bool {
return true
}
Loading

0 comments on commit 003f990

Please sign in to comment.