mirror of
https://github.com/Floriansylvain/RenewCMS.git
synced 2026-08-19 11:43:22 +02:00
feat: complete email verification flow
This commit is contained in:
@@ -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{}
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
</head>
|
||||
<body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark">
|
||||
<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>
|
||||
<h1>GohCMS</h1>
|
||||
<h2>Login</h2>
|
||||
</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="form-floating">
|
||||
<input class="form-control {{ if .PageError.IsError }} is-invalid {{ end }}"
|
||||
@@ -46,12 +46,18 @@
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
const button = document.querySelector("#loginFormButton")
|
||||
const inputs = document.querySelectorAll('input')
|
||||
const form = document.querySelector("#loginForm")
|
||||
|
||||
function formFieldsEmpty() {
|
||||
return Array.from(inputs).some((input) => input.value === "")
|
||||
@@ -77,7 +83,7 @@
|
||||
if (event.target.tagName === "INPUT") setButtonDisabled()
|
||||
}
|
||||
|
||||
window.addEventListener('submit', onLoginFormSubmit)
|
||||
form.addEventListener('submit', onLoginFormSubmit)
|
||||
window.addEventListener('input', onInput)
|
||||
setButtonDisabled()
|
||||
</script>
|
||||
|
||||
@@ -12,15 +12,15 @@
|
||||
</head>
|
||||
<body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark">
|
||||
<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>
|
||||
<h1>GohCMS</h1>
|
||||
<h2>Admin account creation</h2>
|
||||
</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="form-floating">
|
||||
<input class="form-control {{ if .PageError.IsError }} is-invalid {{ end }}"
|
||||
<input class="form-control {{ if .PageError.Email }} is-invalid {{ end }}"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="E-mail"
|
||||
@@ -29,10 +29,10 @@
|
||||
value="{{.Email}}"
|
||||
onblur="validateEmail()">
|
||||
<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 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"
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="invalid-feedback">The username must be between 3 and 20 characters long.</div>
|
||||
</div>
|
||||
<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"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
@@ -53,7 +53,7 @@
|
||||
<div class="invalid-feedback">The password must contain at least 8 characters.</div>
|
||||
</div>
|
||||
<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"
|
||||
name="confirmPassword"
|
||||
placeholder="Password confirmation"
|
||||
@@ -71,11 +71,17 @@
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
const docElems = {
|
||||
form: document.querySelector("#registerForm"),
|
||||
button: document.querySelector("#registerFormButton"),
|
||||
inputs: document.querySelectorAll("input"),
|
||||
email: document.querySelector("#email"),
|
||||
@@ -133,7 +139,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('submit', onRegisterFormSubmit)
|
||||
docElems.form.addEventListener('submit', onRegisterFormSubmit)
|
||||
window.addEventListener('input', onInput)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -12,21 +12,35 @@
|
||||
</head>
|
||||
<body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark">
|
||||
<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>
|
||||
<h1>GohCMS</h1>
|
||||
<h2>Verify your email</h2>
|
||||
</div>
|
||||
<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">
|
||||
<span class="finalSetupFormButtonLoading visually-hidden spinner-border spinner-border-sm"
|
||||
<p>If you misspelled the address or just didn't receive any e-mails, please check your spams or cancel the
|
||||
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>
|
||||
<span class="finalSetupFormButtonDefault">Cancel</span>
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -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)
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
+11
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+29
-9
@@ -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"`
|
||||
PageError *RegisterPageError `json:"error"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
var EmptyRegisterPage = &RegisterPage{
|
||||
PageError: NewPageError(""),
|
||||
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,7 +63,11 @@ func PostRegisterPage(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
r.Method = http.MethodGet
|
||||
GetRegisterPageHandler(&RegisterPage{
|
||||
PageError: NewPageError("Invalid register form data format."),
|
||||
PageError: &RegisterPageError{
|
||||
Email: true,
|
||||
Password: true,
|
||||
Username: true,
|
||||
},
|
||||
Username: r.FormValue("username"),
|
||||
Email: r.FormValue("email"),
|
||||
})(w, r)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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{}{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user