Merge pull request #31 from Floriansylvain/feature/registerCancel

feat: complete email verification flow
This commit is contained in:
Florian Sylvain
2024-05-15 19:19:43 +02:00
committed by GitHub
13 changed files with 228 additions and 124 deletions
+18 -13
View File
@@ -3,6 +3,7 @@ package gateways
import ( import (
entity "GohCMS2/adapters/secondary/gateways/models" entity "GohCMS2/adapters/secondary/gateways/models"
"GohCMS2/domain/gateways" "GohCMS2/domain/gateways"
"GohCMS2/domain/user"
domain "GohCMS2/domain/user" domain "GohCMS2/domain/user"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm" "gorm.io/gorm"
@@ -30,13 +31,13 @@ func mapUserToDomain(user entity.User) domain.User {
} }
func (u *UserRepository) Get(id uint32) (domain.User, error) { func (u *UserRepository) Get(id uint32) (domain.User, error) {
var user entity.User var localUser entity.User
err := u.db.Model(&entity.User{}).First(&user, id).Error err := u.db.Model(&entity.User{}).First(&localUser, id).Error
if err != nil { if err != nil {
return domain.User{}, err return domain.User{}, err
} }
return mapUserToDomain(user), nil return mapUserToDomain(localUser), nil
} }
func (u *UserRepository) Create(user domain.User) (domain.User, error) { 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 nil
} }
func (u *UserRepository) Delete(id uint32) error {
return u.db.Delete(&user.User{}, id).Error
}
func (u *UserRepository) GetAll() []domain.User { func (u *UserRepository) GetAll() []domain.User {
var users []entity.User var users []entity.User
u.db.Model(&entity.User{}).Find(&users) u.db.Model(&entity.User{}).Find(&users)
var domainUsers []domain.User var domainUsers []domain.User
for _, user := range users { for _, localUser := range users {
domainUsers = append(domainUsers, mapUserToDomain(user)) domainUsers = append(domainUsers, mapUserToDomain(localUser))
} }
return domainUsers return domainUsers
} }
func (u *UserRepository) GetByUsername(username string) (domain.User, error) { func (u *UserRepository) GetByUsername(username string) (domain.User, error) {
var user entity.User var localUser entity.User
err := u.db.Model(&entity.User{}).Where("username = ?", username).First(&user).Error err := u.db.Model(&entity.User{}).Where("username = ?", username).First(&localUser).Error
if err != nil { if err != nil {
return domain.User{}, err return domain.User{}, err
} }
return mapUserToDomain(user), nil return mapUserToDomain(localUser), nil
} }
func (u *UserRepository) UpdateVerificationStatus(userId uint32, isVerified bool) (domain.User, error) { func (u *UserRepository) UpdateVerificationStatus(userId uint32, isVerified bool) (domain.User, error) {
var user entity.User var localUser entity.User
err := u.db.Model(&entity.User{}).First(&user, userId).Error err := u.db.Model(&entity.User{}).First(&localUser, userId).Error
if err != nil { if err != nil {
return domain.User{}, err return domain.User{}, err
} }
user.IsVerified = isVerified localUser.IsVerified = isVerified
err = u.db.Save(&user).Error err = u.db.Save(&localUser).Error
if err != nil { if err != nil {
return domain.User{}, err return domain.User{}, err
} }
return mapUserToDomain(user), nil return mapUserToDomain(localUser), nil
} }
var _ gateways.IUserRepository = &UserRepository{} var _ gateways.IUserRepository = &UserRepository{}
@@ -12,12 +12,12 @@
</head> </head>
<body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark"> <body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark">
<div class="container"> <div class="container">
<div class="d-flex flex-column gap-5 m-auto form-container"> <div class="d-flex flex-column m-auto form-container">
<div> <div>
<h1>GohCMS</h1> <h1>GohCMS</h1>
<h2>Login</h2> <h2>Login</h2>
</div> </div>
<form action="login" method="POST"> <form action="login" method="POST" class="mt-5" id="loginForm">
<div class="d-flex flex-column gap-4"> <div class="d-flex flex-column gap-4">
<div class="form-floating"> <div class="form-floating">
<input class="form-control {{ if .PageError.IsError }} is-invalid {{ end }}" <input class="form-control {{ if .PageError.IsError }} is-invalid {{ end }}"
@@ -46,12 +46,18 @@
</button> </button>
</div> </div>
</form> </form>
<form action="register" method="get" class="mt-2 w-100">
<button class="btn btn-outline-primary w-100" type="submit">
Don't have any verified account? Register here.
</button>
</form>
</div> </div>
</div> </div>
<script> <script>
const button = document.querySelector("#loginFormButton") const button = document.querySelector("#loginFormButton")
const inputs = document.querySelectorAll('input') const inputs = document.querySelectorAll('input')
const form = document.querySelector("#loginForm")
function formFieldsEmpty() { function formFieldsEmpty() {
return Array.from(inputs).some((input) => input.value === "") return Array.from(inputs).some((input) => input.value === "")
@@ -77,7 +83,7 @@
if (event.target.tagName === "INPUT") setButtonDisabled() if (event.target.tagName === "INPUT") setButtonDisabled()
} }
window.addEventListener('submit', onLoginFormSubmit) form.addEventListener('submit', onLoginFormSubmit)
window.addEventListener('input', onInput) window.addEventListener('input', onInput)
setButtonDisabled() setButtonDisabled()
</script> </script>
@@ -12,15 +12,15 @@
</head> </head>
<body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark"> <body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark">
<div class="container"> <div class="container">
<div class="d-flex flex-column gap-5 m-auto form-container"> <div class="d-flex flex-column m-auto form-container">
<div> <div>
<h1>GohCMS</h1> <h1>GohCMS</h1>
<h2>Admin account creation</h2> <h2>Admin account creation</h2>
</div> </div>
<form action="register" class="" method="POST"> <form action="register" method="POST" class="mt-5" id="registerForm">
<div class="d-flex flex-column gap-4"> <div class="d-flex flex-column gap-4">
<div class="form-floating"> <div class="form-floating">
<input class="form-control {{ if .PageError.IsError }} is-invalid {{ end }}" <input class="form-control {{ if .PageError.Email }} is-invalid {{ end }}"
id="email" id="email"
name="email" name="email"
placeholder="E-mail" placeholder="E-mail"
@@ -29,10 +29,10 @@
value="{{.Email}}" value="{{.Email}}"
onblur="validateEmail()"> onblur="validateEmail()">
<label for="email">E-mail</label> <label for="email">E-mail</label>
<div class="invalid-feedback">The e-mail must be a valid e-mail.</div> <div class="invalid-feedback">The e-mail must be a valid e-mail and not already in use.</div>
</div> </div>
<div class="form-floating"> <div class="form-floating">
<input class="form-control {{ if .PageError.IsError }} is-invalid {{ end }}" <input class="form-control {{ if .PageError.Username }} is-invalid {{ end }}"
id="username" id="username"
name="username" name="username"
placeholder="Username" placeholder="Username"
@@ -43,7 +43,7 @@
<div class="invalid-feedback">The username must be between 3 and 20 characters long.</div> <div class="invalid-feedback">The username must be between 3 and 20 characters long.</div>
</div> </div>
<div class="form-floating"> <div class="form-floating">
<input class="form-control {{ if .PageError.IsError }} is-invalid {{ end }}" <input class="form-control {{ if .PageError.Password }} is-invalid {{ end }}"
id="password" id="password"
name="password" name="password"
placeholder="Password" placeholder="Password"
@@ -53,7 +53,7 @@
<div class="invalid-feedback">The password must contain at least 8 characters.</div> <div class="invalid-feedback">The password must contain at least 8 characters.</div>
</div> </div>
<div class="form-floating"> <div class="form-floating">
<input class="form-control {{ if .PageError.IsError }} is-invalid {{ end }}" <input class="form-control {{ if .PageError.Password }} is-invalid {{ end }}"
id="confirmPassword" id="confirmPassword"
name="confirmPassword" name="confirmPassword"
placeholder="Password confirmation" placeholder="Password confirmation"
@@ -71,11 +71,17 @@
</button> </button>
</div> </div>
</form> </form>
<form action="login" method="get" class="mt-2 w-100">
<button class="btn btn-outline-primary w-100" type="submit">
Already registered? Log in!
</button>
</form>
</div> </div>
</div> </div>
<script> <script>
const docElems = { const docElems = {
form: document.querySelector("#registerForm"),
button: document.querySelector("#registerFormButton"), button: document.querySelector("#registerFormButton"),
inputs: document.querySelectorAll("input"), inputs: document.querySelectorAll("input"),
email: document.querySelector("#email"), email: document.querySelector("#email"),
@@ -133,7 +139,7 @@
} }
} }
window.addEventListener('submit', onRegisterFormSubmit) docElems.form.addEventListener('submit', onRegisterFormSubmit)
window.addEventListener('input', onInput) window.addEventListener('input', onInput)
</script> </script>
@@ -12,21 +12,35 @@
</head> </head>
<body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark"> <body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark">
<div class="container"> <div class="container">
<div class="d-flex flex-column gap-5 m-auto form-container"> <div class="d-flex flex-column m-auto form-container">
<div> <div>
<h1>GohCMS</h1> <h1>GohCMS</h1>
<h2>Verify your email</h2> <h2>Verify your email</h2>
</div> </div>
<p>An e-mail with the validation link was sent to the address you just registered.</p> <p>An e-mail with the validation link was sent to the address you just registered.</p>
<button id="finalSetupFormButton" class="btn btn-outline-primary" type="button"> <p>If you misspelled the address or just didn't receive any e-mails, please check your spams or cancel the
<span class="finalSetupFormButtonLoading visually-hidden spinner-border spinner-border-sm" account creation.</p>
<form action="" method="post" class="d-flex">
<button class="btn btn-outline-primary w-100" type="submit">
<span class="visually-hidden spinner-border spinner-border-sm"
role="status"></span> role="status"></span>
<span class="finalSetupFormButtonDefault">Cancel</span> <span>Cancel</span>
</button> </button>
</form>
</div> </div>
</div> </div>
<script> <script>
const button = document.querySelector("button")
function setButtonLoading() {
button.classList.add("disabled")
button.querySelector("button > span + span").classList.add("visually-hidden")
button.querySelector("button > span:first-child").classList.remove("visually-hidden")
}
window.addEventListener('submit', setButtonLoading)
button.addEventListener('click', setButtonLoading)
</script> </script>
</body> </body>
@@ -41,12 +41,8 @@
button.querySelector("button > span:first-child").classList.remove("visually-hidden") button.querySelector("button > span:first-child").classList.remove("visually-hidden")
} }
function onSubmit(event) { window.addEventListener('submit', setButtonLoading)
setButtonLoading() button.addEventListener('click', setButtonLoading)
}
window.addEventListener('submit', onSubmit)
button.addEventListener('click', onSubmit)
</script> </script>
</body> </body>
+11 -1
View File
@@ -47,6 +47,16 @@ func SetJwtCookie(w *http.ResponseWriter, userId uint32) error {
return nil return nil
} }
func SomeUsersVerified() bool {
users := Container.ListUsersUseCase.ListUsers()
for _, localUser := range users {
if localUser.IsVerified {
return true
}
}
return false
}
func IsUserTableEmpty() bool { func IsUserTableEmpty() bool {
users := Container.ListUsersUseCase.ListUsers() users := Container.ListUsersUseCase.ListUsers()
return len(users) == 0 return len(users) == 0
@@ -131,7 +141,7 @@ func login(w http.ResponseWriter, r *http.Request) {
} }
func register(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) http.Error(w, "You are not allowed to create a user. Log in or reset database.", http.StatusForbidden)
return return
} }
+2
View File
@@ -17,6 +17,7 @@ type UseCases struct {
GetUserUseCase *useCases.GetUserUseCase GetUserUseCase *useCases.GetUserUseCase
CreateUserUseCase *useCases.CreateUserUseCase CreateUserUseCase *useCases.CreateUserUseCase
UpdateUserUseCase *useCases.UpdateUserUseCase UpdateUserUseCase *useCases.UpdateUserUseCase
DeleteUserUseCase *useCases.DeleteUserUseCase
ListUsersUseCase *useCases.ListUsersUseCase ListUsersUseCase *useCases.ListUsersUseCase
GetPageUseCase *useCases.GetPageUseCase GetPageUseCase *useCases.GetPageUseCase
SendMailUseCase *useCases.SendMailUseCase SendMailUseCase *useCases.SendMailUseCase
@@ -51,6 +52,7 @@ func InitContainer() {
GetUserUseCase: useCases.NewGetUserUseCase(db), GetUserUseCase: useCases.NewGetUserUseCase(db),
CreateUserUseCase: useCases.NewCreateUserUseCase(db), CreateUserUseCase: useCases.NewCreateUserUseCase(db),
UpdateUserUseCase: useCases.NewUpdateUserUseCase(db), UpdateUserUseCase: useCases.NewUpdateUserUseCase(db),
DeleteUserUseCase: useCases.NewDeleteUserUseCase(db),
ListUsersUseCase: useCases.NewListUsersUseCase(db), ListUsersUseCase: useCases.NewListUsersUseCase(db),
GetPageUseCase: useCases.NewGetPageUseCase(), GetPageUseCase: useCases.NewGetPageUseCase(),
SendMailUseCase: useCases.NewSendMailUseCase(), SendMailUseCase: useCases.NewSendMailUseCase(),
+1
View File
@@ -113,6 +113,7 @@ func NewPageRouter() http.Handler {
r.Use(IsLoggedInMiddleware) r.Use(IsLoggedInMiddleware)
r.Use(IsNotVerifiedMiddleware) r.Use(IsNotVerifiedMiddleware)
r.Get("/register/pending", GetRegisterPendingPage) r.Get("/register/pending", GetRegisterPendingPage)
r.Post("/register/pending", PostRegisterPendingPage)
r.Get("/register/validate", GetRegisterValidatePage) r.Get("/register/validate", GetRegisterValidatePage)
}) })
+2 -2
View File
@@ -53,7 +53,7 @@ func PostLoginPage(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
r.Method = http.MethodGet r.Method = http.MethodGet
GetLoginPageHandler(&LoginPage{ GetLoginPageHandler(&LoginPage{
PageError: NewPageError("Invalid form data format."), PageError: NewPageError("Invalid username or password format."),
Username: r.FormValue("username"), Username: r.FormValue("username"),
})(w, r) })(w, r)
return return
@@ -63,7 +63,7 @@ func PostLoginPage(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
r.Method = http.MethodGet r.Method = http.MethodGet
GetLoginPageHandler(&LoginPage{ GetLoginPageHandler(&LoginPage{
PageError: NewPageError("Invalid username or password."), PageError: NewPageError("Invalid username or password combination."),
Username: r.FormValue("username"), Username: r.FormValue("username"),
})(w, r) })(w, r)
return return
+29 -9
View File
@@ -6,21 +6,31 @@ import (
"os" "os"
) )
type RegisterPageError struct {
Email bool `json:"email"`
Password bool `json:"password"`
Username bool `json:"username"`
}
type RegisterPage struct { type RegisterPage struct {
PageError *PageError `json:"error"` PageError *RegisterPageError `json:"error"`
Username string `json:"username"` Username string `json:"username"`
Email string `json:"email"` Email string `json:"email"`
} }
var EmptyRegisterPage = &RegisterPage{ var EmptyRegisterPage = &RegisterPage{
PageError: NewPageError(""), PageError: &RegisterPageError{
Email: false,
Password: false,
Username: false,
},
Username: "", Username: "",
Email: "", Email: "",
} }
func GetRegisterPageHandler(registerPage *RegisterPage) http.HandlerFunc { func GetRegisterPageHandler(registerPage *RegisterPage) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { 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) http.Redirect(w, r, "/home", http.StatusSeeOther)
return return
} }
@@ -53,7 +63,11 @@ func PostRegisterPage(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
r.Method = http.MethodGet r.Method = http.MethodGet
GetRegisterPageHandler(&RegisterPage{ GetRegisterPageHandler(&RegisterPage{
PageError: NewPageError("Invalid register form data format."), PageError: &RegisterPageError{
Email: true,
Password: true,
Username: true,
},
Username: r.FormValue("username"), Username: r.FormValue("username"),
Email: r.FormValue("email"), Email: r.FormValue("email"),
})(w, r) })(w, r)
@@ -63,17 +77,23 @@ func PostRegisterPage(w http.ResponseWriter, r *http.Request) {
verificationCode := uuid.NewString() verificationCode := uuid.NewString()
createdUser, err := getNewUser(credentials, verificationCode) createdUser, err := getNewUser(credentials, verificationCode)
if err != nil { 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 return
} }
err = Container.SendMailUseCase.SendMail(createdUser.Email, "mailValidation", map[string]string{ _ = Container.SendMailUseCase.SendMail(createdUser.Email, "mailValidation", map[string]string{
"Host": os.Getenv("HOST"), "Host": os.Getenv("HOST"),
"VerificationCode": verificationCode, "VerificationCode": verificationCode,
}) })
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
_ = SetJwtCookie(&w, createdUser.ID) _ = SetJwtCookie(&w, createdUser.ID)
+24 -1
View File
@@ -1,6 +1,29 @@
package api 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) { func GetRegisterPendingPage(w http.ResponseWriter, _ *http.Request) {
registerPendingTmpl, _ := Container.GetPageUseCase.GetPage("registerPending", map[string]interface{}{ registerPendingTmpl, _ := Container.GetPageUseCase.GetPage("registerPending", map[string]interface{}{
+1
View File
@@ -7,5 +7,6 @@ type IUserRepository interface {
GetByUsername(username string) (user.User, error) GetByUsername(username string) (user.User, error)
GetAll() []user.User GetAll() []user.User
Create(user user.User) (user.User, error) Create(user user.User) (user.User, error)
Delete(id uint32) error
UpdateVerificationStatus(userId uint32, isVerified bool) (user.User, error) UpdateVerificationStatus(userId uint32, isVerified bool) (user.User, error)
} }
+20
View File
@@ -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)
}