Skip to content

Commit

Permalink
introduce user registeration API
Browse files Browse the repository at this point in the history
  • Loading branch information
khanzadimahdi committed Jun 23, 2024
1 parent 5cf2428 commit ded2c83
Show file tree
Hide file tree
Showing 13 changed files with 347 additions and 2 deletions.
1 change: 1 addition & 0 deletions backend/application/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const (
AccessToken = "access"
RefreshToken = "refresh"
ResetPasswordToken = "reset-password"
RegistrationToken = "registration"
)

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

import "regexp"

var (
emailRegex = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
)

type validationErrors map[string]string

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

func (r *Request) Validate() (bool, validationErrors) {
if !emailRegex.MatchString(r.Identity) {
return false, validationErrors{
"identity": "identity is not a valid email address",
}
}

return true, nil
}
5 changes: 5 additions & 0 deletions backend/application/auth/register/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package register

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

import (
"bytes"
"encoding/base64"
"errors"
"github.com/khanzadimahdi/testproject/application/auth"
"github.com/khanzadimahdi/testproject/domain"
"github.com/khanzadimahdi/testproject/domain/user"
"github.com/khanzadimahdi/testproject/infrastructure/jwt"
"time"
)

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) Register(request Request) (*Response, error) {
if ok, validation := request.Validate(); !ok {
return &Response{
ValidationErrors: validation,
}, nil
}

exists, err := uc.IdentityExists(request.Identity)
if err != nil {
return nil, err
}

if exists {
return &Response{
ValidationErrors: map[string]string{
"identity": "user with given email already exists",
},
}, nil
}

resetPasswordToken, err := uc.registrationToken(request.Identity)
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, request.Identity, "Registration", msg.Bytes()); err != nil {
return nil, err
}

return &Response{}, nil
}

func (uc *UseCase) IdentityExists(identity string) (bool, error) {
_, err := uc.userRepository.GetOneByIdentity(identity)
if errors.Is(err, domain.ErrNotExists) {
return true, nil
}

return false, err
}

func (uc *UseCase) registrationToken(identity string) (string, error) {
b := jwt.NewClaimsBuilder()
b.SetSubject(identity)
b.SetNotBefore(time.Now())
b.SetExpirationTime(time.Now().Add(24 * time.Hour))
b.SetIssuedAt(time.Now())
b.SetAudience([]string{auth.RegistrationToken})

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

type validationErrors map[string]string

type Request struct {
Token string `json:"token"`
Name string `json:"name"`
Username string `json:"username"`
Password string `json:"password"`
Repassword string `json:"repassword"`
}

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

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

import (
"crypto/rand"
"errors"

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

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

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

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

claims, err := uc.jwt.Verify(request.Token)
if err != nil {
return &Response{
ValidationErrors: validationErrors{
"token": err.Error(),
},
}, nil
}

if audiences, err := claims.GetAudience(); err != nil || len(audiences) == 0 || audiences[0] != auth.RegistrationToken {
return &Response{
ValidationErrors: validationErrors{
"token": "registration token is not valid",
},
}, nil
}

identity, err := claims.GetSubject()
if err != nil {
return &Response{
ValidationErrors: validationErrors{
"token": err.Error(),
},
}, nil
}

if exists, err := uc.IdentityExists(identity); err != nil {
return nil, err
} else if exists {
return &Response{
ValidationErrors: map[string]string{
"identity": "user with given email already exists",
},
}, nil
}

if exists, err := uc.IdentityExists(request.Username); err != nil {
return nil, err
} else if exists {
return &Response{
ValidationErrors: map[string]string{
"user": "user with given username already exists",
},
}, nil
}

salt := make([]byte, 64)
if _, err := rand.Read(salt); err != nil {
return nil, err
}

u := user.User{
Name: request.Name,
Username: request.Username,
Email: identity,
PasswordHash: password.Hash{
Value: uc.hasher.Hash([]byte(request.Password), salt),
Salt: salt,
},
}

if err := uc.userRepository.Save(&u); err != nil {
return nil, err
}

return &Response{}, nil
}

func (uc *UseCase) IdentityExists(identity string) (bool, error) {
_, err := uc.userRepository.GetOneByIdentity(identity)
if errors.Is(err, domain.ErrNotExists) {
return true, nil
}

return false, err
}
1 change: 1 addition & 0 deletions backend/application/auth/verify/usecase_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package verify
4 changes: 2 additions & 2 deletions backend/infrastructure/jwt/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestJWT(t *testing.T) {
// Here, we extract the public key from the private key just for demonstration purposes.
publicKey := privateKey.Public()

// Generate a JWT token
// Generate a jwt token
claims := jwt.MapClaims{
"sub": "1234567890",
"name": "John Doe",
Expand All @@ -34,7 +34,7 @@ func TestJWT(t *testing.T) {
t.Error("unexpected error", err)
}

// Verify the JWT token
// Verify the jwt token
tokenClaims, err := JWTToken.Verify(tokenString)
if err != nil {
t.Error("unexpected error", err)
Expand Down
6 changes: 6 additions & 0 deletions backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import (
"github.com/khanzadimahdi/testproject/application/auth/forgetpassword"
"github.com/khanzadimahdi/testproject/application/auth/login"
"github.com/khanzadimahdi/testproject/application/auth/refresh"
"github.com/khanzadimahdi/testproject/application/auth/register"
"github.com/khanzadimahdi/testproject/application/auth/resetpassword"
"github.com/khanzadimahdi/testproject/application/auth/verify"
dashboardCreateArticle "github.com/khanzadimahdi/testproject/application/dashboard/article/createArticle"
dashboardDeleteArticle "github.com/khanzadimahdi/testproject/application/dashboard/article/deleteArticle"
dashboardGetArticle "github.com/khanzadimahdi/testproject/application/dashboard/article/getArticle"
Expand Down Expand Up @@ -141,6 +143,8 @@ func httpHandler() http.Handler {
refreshUseCase := refresh.NewUseCase(userRepository, j)
forgetPasswordUseCase := forgetpassword.NewUseCase(userRepository, j, mailer, mailFromAddress)
resetPasswordUseCase := resetpassword.NewUseCase(userRepository, hasher, j)
registerUseCase := register.NewUseCase(userRepository, j, mailer, mailFromAddress)
verifyUseCase := verify.NewUseCase(userRepository, hasher, j)

getArticleUsecase := getArticle.NewUseCase(articlesRepository, elementsRepository)
getArticlesUsecase := getArticles.NewUseCase(articlesRepository)
Expand All @@ -155,6 +159,8 @@ func httpHandler() http.Handler {
router.Handler(http.MethodPost, "/api/auth/token/refresh", auth.NewRefreshHandler(refreshUseCase))
router.Handler(http.MethodPost, "/api/auth/password/forget", auth.NewForgetPasswordHandler(forgetPasswordUseCase))
router.Handler(http.MethodPost, "/api/auth/password/reset", auth.NewResetPasswordHandler(resetPasswordUseCase))
router.Handler(http.MethodPost, "/api/auth/register", auth.NewRegisterHandler(registerUseCase))
router.Handler(http.MethodPost, "/api/auth/verify", auth.NewVerifyHandler(verifyUseCase))

// articles
router.Handler(http.MethodGet, "/api/articles", articleAPI.NewIndexHandler(getArticlesUsecase))
Expand Down
43 changes: 43 additions & 0 deletions backend/presentation/http/api/auth/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package auth

import (
"encoding/json"
"errors"
"net/http"

"github.com/khanzadimahdi/testproject/application/auth/register"
"github.com/khanzadimahdi/testproject/domain"
)

type registerHandler struct {
useCase *register.UseCase
}

func NewRegisterHandler(useCase *register.UseCase) *registerHandler {
return &registerHandler{
useCase: useCase,
}
}

func (h *registerHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
var request register.Request
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
rw.WriteHeader(http.StatusBadRequest)
return
}

response, err := h.useCase.Register(request)

switch true {
case errors.Is(err, domain.ErrNotExists):
rw.WriteHeader(http.StatusNotFound)
case err != nil:
rw.WriteHeader(http.StatusInternalServerError)
case len(response.ValidationErrors) > 0:
rw.WriteHeader(http.StatusBadRequest)
json.NewEncoder(rw).Encode(response)
default:
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusNoContent)
}
}
Loading

0 comments on commit ded2c83

Please sign in to comment.