From a35e7c4283e9daca327a2f3fd364aa5bcdf1a753 Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Wed, 15 May 2024 19:16:55 +0200 Subject: [PATCH] feat: complete email verification flow --- adapters/secondary/gateways/userRepository.go | 31 ++-- .../gateways/web/templates/login.html | 12 +- .../gateways/web/templates/register.html | 140 +++++++++--------- .../web/templates/registerPending.html | 46 ++++-- .../web/templates/registerValidate.html | 8 +- api/auth.go | 12 +- api/dependecyInjection.go | 2 + api/page.go | 1 + api/pageLogin.go | 4 +- api/pageRegister.go | 50 +++++-- api/pageRegisterPending.go | 25 +++- domain/gateways/IUserRepository.go | 1 + useCases/DeleteUserUseCase.go | 20 +++ 13 files changed, 228 insertions(+), 124 deletions(-) create mode 100644 useCases/DeleteUserUseCase.go diff --git a/adapters/secondary/gateways/userRepository.go b/adapters/secondary/gateways/userRepository.go index 2551078..44134ea 100644 --- a/adapters/secondary/gateways/userRepository.go +++ b/adapters/secondary/gateways/userRepository.go @@ -3,6 +3,7 @@ package gateways import ( entity "GohCMS2/adapters/secondary/gateways/models" "GohCMS2/domain/gateways" + "GohCMS2/domain/user" domain "GohCMS2/domain/user" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" @@ -30,13 +31,13 @@ func mapUserToDomain(user entity.User) domain.User { } func (u *UserRepository) Get(id uint32) (domain.User, error) { - var user entity.User - err := u.db.Model(&entity.User{}).First(&user, id).Error + var localUser entity.User + err := u.db.Model(&entity.User{}).First(&localUser, id).Error if err != nil { return domain.User{}, err } - return mapUserToDomain(user), nil + return mapUserToDomain(localUser), nil } func (u *UserRepository) Create(user domain.User) (domain.User, error) { @@ -61,42 +62,46 @@ func (u *UserRepository) Create(user domain.User) (domain.User, error) { nil } +func (u *UserRepository) Delete(id uint32) error { + return u.db.Delete(&user.User{}, id).Error +} + func (u *UserRepository) GetAll() []domain.User { var users []entity.User u.db.Model(&entity.User{}).Find(&users) var domainUsers []domain.User - for _, user := range users { - domainUsers = append(domainUsers, mapUserToDomain(user)) + for _, localUser := range users { + domainUsers = append(domainUsers, mapUserToDomain(localUser)) } return domainUsers } func (u *UserRepository) GetByUsername(username string) (domain.User, error) { - var user entity.User - err := u.db.Model(&entity.User{}).Where("username = ?", username).First(&user).Error + var localUser entity.User + err := u.db.Model(&entity.User{}).Where("username = ?", username).First(&localUser).Error if err != nil { return domain.User{}, err } - return mapUserToDomain(user), nil + return mapUserToDomain(localUser), nil } func (u *UserRepository) UpdateVerificationStatus(userId uint32, isVerified bool) (domain.User, error) { - var user entity.User - err := u.db.Model(&entity.User{}).First(&user, userId).Error + var localUser entity.User + err := u.db.Model(&entity.User{}).First(&localUser, userId).Error if err != nil { return domain.User{}, err } - user.IsVerified = isVerified - err = u.db.Save(&user).Error + localUser.IsVerified = isVerified + err = u.db.Save(&localUser).Error if err != nil { return domain.User{}, err } - return mapUserToDomain(user), nil + return mapUserToDomain(localUser), nil } var _ gateways.IUserRepository = &UserRepository{} diff --git a/adapters/secondary/gateways/web/templates/login.html b/adapters/secondary/gateways/web/templates/login.html index a17a9b7..4c58c13 100644 --- a/adapters/secondary/gateways/web/templates/login.html +++ b/adapters/secondary/gateways/web/templates/login.html @@ -12,12 +12,12 @@
-
+

GohCMS

Login

-
+
+
+ +
diff --git a/adapters/secondary/gateways/web/templates/register.html b/adapters/secondary/gateways/web/templates/register.html index 6985091..461e90b 100644 --- a/adapters/secondary/gateways/web/templates/register.html +++ b/adapters/secondary/gateways/web/templates/register.html @@ -1,81 +1,87 @@ - GohCMS | Setup - {{.Head}} + GohCMS | Setup + {{.Head}} - +
-
-
-

GohCMS

-

Admin account creation

-
-
-
-
- - -
The e-mail must be a valid e-mail.
-
-
- - -
The username must be between 3 and 20 characters long.
-
-
- - -
The password must contain at least 8 characters.
-
-
- - -
The passwords do not match.
-
- -
-
-
+ role="status"> + Next + +
+ +
+ +
+
diff --git a/adapters/secondary/gateways/web/templates/registerPending.html b/adapters/secondary/gateways/web/templates/registerPending.html index 812de85..763acd1 100644 --- a/adapters/secondary/gateways/web/templates/registerPending.html +++ b/adapters/secondary/gateways/web/templates/registerPending.html @@ -1,32 +1,46 @@ - GohCMS | Setup - {{.Head}} + GohCMS | Setup + {{.Head}} - +
-
-
-

GohCMS

-

Verify your email

-
-

An e-mail with the validation link was sent to the address you just registered.

- -
+
+
+

GohCMS

+

Verify your email

+
+

An e-mail with the validation link was sent to the address you just registered.

+

If you misspelled the address or just didn't receive any e-mails, please check your spams or cancel the + account creation.

+
+ +
+
diff --git a/adapters/secondary/gateways/web/templates/registerValidate.html b/adapters/secondary/gateways/web/templates/registerValidate.html index 1ff2b68..aca4d52 100644 --- a/adapters/secondary/gateways/web/templates/registerValidate.html +++ b/adapters/secondary/gateways/web/templates/registerValidate.html @@ -41,12 +41,8 @@ button.querySelector("button > span:first-child").classList.remove("visually-hidden") } - function onSubmit(event) { - setButtonLoading() - } - - window.addEventListener('submit', onSubmit) - button.addEventListener('click', onSubmit) + window.addEventListener('submit', setButtonLoading) + button.addEventListener('click', setButtonLoading) diff --git a/api/auth.go b/api/auth.go index 167f61a..2ed9d6b 100644 --- a/api/auth.go +++ b/api/auth.go @@ -47,6 +47,16 @@ func SetJwtCookie(w *http.ResponseWriter, userId uint32) error { return nil } +func SomeUsersVerified() bool { + users := Container.ListUsersUseCase.ListUsers() + for _, localUser := range users { + if localUser.IsVerified { + return true + } + } + return false +} + func IsUserTableEmpty() bool { users := Container.ListUsersUseCase.ListUsers() return len(users) == 0 @@ -131,7 +141,7 @@ func login(w http.ResponseWriter, r *http.Request) { } func register(w http.ResponseWriter, r *http.Request) { - if !IsLoggedIn(r) && !IsUserTableEmpty() { + if !IsLoggedIn(r) && !SomeUsersVerified() { http.Error(w, "You are not allowed to create a user. Log in or reset database.", http.StatusForbidden) return } diff --git a/api/dependecyInjection.go b/api/dependecyInjection.go index de582aa..6a05030 100644 --- a/api/dependecyInjection.go +++ b/api/dependecyInjection.go @@ -17,6 +17,7 @@ type UseCases struct { GetUserUseCase *useCases.GetUserUseCase CreateUserUseCase *useCases.CreateUserUseCase UpdateUserUseCase *useCases.UpdateUserUseCase + DeleteUserUseCase *useCases.DeleteUserUseCase ListUsersUseCase *useCases.ListUsersUseCase GetPageUseCase *useCases.GetPageUseCase SendMailUseCase *useCases.SendMailUseCase @@ -51,6 +52,7 @@ func InitContainer() { GetUserUseCase: useCases.NewGetUserUseCase(db), CreateUserUseCase: useCases.NewCreateUserUseCase(db), UpdateUserUseCase: useCases.NewUpdateUserUseCase(db), + DeleteUserUseCase: useCases.NewDeleteUserUseCase(db), ListUsersUseCase: useCases.NewListUsersUseCase(db), GetPageUseCase: useCases.NewGetPageUseCase(), SendMailUseCase: useCases.NewSendMailUseCase(), diff --git a/api/page.go b/api/page.go index ec39ab1..0dfe53a 100644 --- a/api/page.go +++ b/api/page.go @@ -113,6 +113,7 @@ func NewPageRouter() http.Handler { r.Use(IsLoggedInMiddleware) r.Use(IsNotVerifiedMiddleware) r.Get("/register/pending", GetRegisterPendingPage) + r.Post("/register/pending", PostRegisterPendingPage) r.Get("/register/validate", GetRegisterValidatePage) }) diff --git a/api/pageLogin.go b/api/pageLogin.go index a8b9cdb..10c026d 100644 --- a/api/pageLogin.go +++ b/api/pageLogin.go @@ -53,7 +53,7 @@ func PostLoginPage(w http.ResponseWriter, r *http.Request) { if err != nil { r.Method = http.MethodGet GetLoginPageHandler(&LoginPage{ - PageError: NewPageError("Invalid form data format."), + PageError: NewPageError("Invalid username or password format."), Username: r.FormValue("username"), })(w, r) return @@ -63,7 +63,7 @@ func PostLoginPage(w http.ResponseWriter, r *http.Request) { if err != nil { r.Method = http.MethodGet GetLoginPageHandler(&LoginPage{ - PageError: NewPageError("Invalid username or password."), + PageError: NewPageError("Invalid username or password combination."), Username: r.FormValue("username"), })(w, r) return diff --git a/api/pageRegister.go b/api/pageRegister.go index 4658c0a..c063c2c 100644 --- a/api/pageRegister.go +++ b/api/pageRegister.go @@ -6,21 +6,31 @@ import ( "os" ) +type RegisterPageError struct { + Email bool `json:"email"` + Password bool `json:"password"` + Username bool `json:"username"` +} + type RegisterPage struct { - PageError *PageError `json:"error"` - Username string `json:"username"` - Email string `json:"email"` + PageError *RegisterPageError `json:"error"` + Username string `json:"username"` + Email string `json:"email"` } var EmptyRegisterPage = &RegisterPage{ - PageError: NewPageError(""), - Username: "", - Email: "", + PageError: &RegisterPageError{ + Email: false, + Password: false, + Username: false, + }, + Username: "", + Email: "", } func GetRegisterPageHandler(registerPage *RegisterPage) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if IsLoggedIn(r) || !IsUserTableEmpty() { + if (IsLoggedIn(r) && IsVerified(r)) || SomeUsersVerified() { http.Redirect(w, r, "/home", http.StatusSeeOther) return } @@ -53,9 +63,13 @@ func PostRegisterPage(w http.ResponseWriter, r *http.Request) { if err != nil { r.Method = http.MethodGet GetRegisterPageHandler(&RegisterPage{ - PageError: NewPageError("Invalid register form data format."), - Username: r.FormValue("username"), - Email: r.FormValue("email"), + PageError: &RegisterPageError{ + Email: true, + Password: true, + Username: true, + }, + Username: r.FormValue("username"), + Email: r.FormValue("email"), })(w, r) return } @@ -63,17 +77,23 @@ func PostRegisterPage(w http.ResponseWriter, r *http.Request) { verificationCode := uuid.NewString() createdUser, err := getNewUser(credentials, verificationCode) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + r.Method = http.MethodGet + GetRegisterPageHandler(&RegisterPage{ + PageError: &RegisterPageError{ + Email: true, + Password: false, + Username: false, + }, + Username: r.FormValue("username"), + Email: r.FormValue("email"), + })(w, r) return } - err = Container.SendMailUseCase.SendMail(createdUser.Email, "mailValidation", map[string]string{ + _ = Container.SendMailUseCase.SendMail(createdUser.Email, "mailValidation", map[string]string{ "Host": os.Getenv("HOST"), "VerificationCode": verificationCode, }) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } _ = SetJwtCookie(&w, createdUser.ID) diff --git a/api/pageRegisterPending.go b/api/pageRegisterPending.go index 48efea2..c9259d0 100644 --- a/api/pageRegisterPending.go +++ b/api/pageRegisterPending.go @@ -1,6 +1,29 @@ package api -import "net/http" +import ( + "github.com/go-chi/jwtauth/v5" + "log" + "net/http" +) + +func PostRegisterPendingPage(w http.ResponseWriter, r *http.Request) { + token, _ := jwtauth.VerifyRequest( + TokenAuth, r, + jwtauth.TokenFromCookie, + jwtauth.TokenFromHeader, + jwtauth.TokenFromQuery) + userId := token.PrivateClaims()["user_id"].(float64) + err := Container.DeleteUserUseCase.DeleteUser(uint32(userId)) + if err != nil { + log.Println(err) + r.Method = http.MethodGet + http.Redirect(w, r, "/register/pending", http.StatusSeeOther) + } + + RemoveJwtCookie(w) + + http.Redirect(w, r, "/register", http.StatusSeeOther) +} func GetRegisterPendingPage(w http.ResponseWriter, _ *http.Request) { registerPendingTmpl, _ := Container.GetPageUseCase.GetPage("registerPending", map[string]interface{}{ diff --git a/domain/gateways/IUserRepository.go b/domain/gateways/IUserRepository.go index 1d43a9f..39e7693 100644 --- a/domain/gateways/IUserRepository.go +++ b/domain/gateways/IUserRepository.go @@ -7,5 +7,6 @@ type IUserRepository interface { GetByUsername(username string) (user.User, error) GetAll() []user.User Create(user user.User) (user.User, error) + Delete(id uint32) error UpdateVerificationStatus(userId uint32, isVerified bool) (user.User, error) } diff --git a/useCases/DeleteUserUseCase.go b/useCases/DeleteUserUseCase.go new file mode 100644 index 0000000..c861c29 --- /dev/null +++ b/useCases/DeleteUserUseCase.go @@ -0,0 +1,20 @@ +package useCases + +import ( + "GohCMS2/adapters/secondary/gateways" + "gorm.io/gorm" +) + +type DeleteUserUseCase struct { + userRepository gateways.UserRepository +} + +func NewDeleteUserUseCase(db *gorm.DB) *DeleteUserUseCase { + return &DeleteUserUseCase{ + userRepository: *gateways.NewUserRepository(db), + } +} + +func (g *DeleteUserUseCase) DeleteUser(userId uint32) error { + return g.userRepository.Delete(userId) +}