mirror of
https://github.com/Floriansylvain/RenewCMS.git
synced 2026-08-19 11:43:22 +02:00
feat: email verification flow (not 100% complete)
This commit is contained in:
@@ -60,3 +60,7 @@ TODO
|
|||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
|
## TODOs
|
||||||
|
|
||||||
|
- Cancel button when email validation pending or expired
|
||||||
|
|||||||
@@ -44,10 +44,11 @@ func (u *UserRepository) Create(user domain.User) (domain.User, error) {
|
|||||||
hashedVerificationCode, _ := bcrypt.GenerateFromPassword([]byte(user.VerificationCode), 12)
|
hashedVerificationCode, _ := bcrypt.GenerateFromPassword([]byte(user.VerificationCode), 12)
|
||||||
|
|
||||||
creationResult := u.db.Create(&entity.User{
|
creationResult := u.db.Create(&entity.User{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Password: string(hashedPassword),
|
Password: string(hashedPassword),
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
VerificationCode: string(hashedVerificationCode),
|
VerificationCode: string(hashedVerificationCode),
|
||||||
|
VerificationExpiration: user.VerificationExpiration,
|
||||||
})
|
})
|
||||||
if creationResult.Error != nil {
|
if creationResult.Error != nil {
|
||||||
return domain.User{}, creationResult.Error
|
return domain.User{}, creationResult.Error
|
||||||
@@ -82,4 +83,20 @@ func (u *UserRepository) GetByUsername(username string) (domain.User, error) {
|
|||||||
return mapUserToDomain(user), nil
|
return mapUserToDomain(user), 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
|
||||||
|
if err != nil {
|
||||||
|
return domain.User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.IsVerified = isVerified
|
||||||
|
err = u.db.Save(&user).Error
|
||||||
|
if err != nil {
|
||||||
|
return domain.User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapUserToDomain(user), nil
|
||||||
|
}
|
||||||
|
|
||||||
var _ gateways.IUserRepository = &UserRepository{}
|
var _ gateways.IUserRepository = &UserRepository{}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<h1>GohCMS</h1>
|
<h1>GohCMS</h1>
|
||||||
<h2>Login</h2>
|
<h2>Login</h2>
|
||||||
</div>
|
</div>
|
||||||
<form action="login" class="" method="POST">
|
<form action="login" method="POST">
|
||||||
<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 }}"
|
||||||
|
|||||||
@@ -1,27 +1,52 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>GohCMS | Setup</title>
|
<title>GohCMS | Setup</title>
|
||||||
{{.Head}}
|
{{.Head}}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.form-container {
|
.form-container {
|
||||||
max-width: 24rem;
|
max-width: 24rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</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 gap-5 m-auto form-container">
|
||||||
<div>
|
<div>
|
||||||
<h1>GohCMS</h1>
|
<h1>GohCMS</h1>
|
||||||
<h2>E-mail verification</h2>
|
<h2>E-mail verification</h2>
|
||||||
</div>
|
</div>
|
||||||
<p>Verifying your e-mail, please wait...</p>
|
{{ if .PageError.IsError }}
|
||||||
</div>
|
<p>{{.PageError.Message}}</p>
|
||||||
|
{{ else }}
|
||||||
|
<p>Your e-mail was successfully validated!</p>
|
||||||
|
<form action="../home" method="get" class="d-flex">
|
||||||
|
<button class="btn btn-primary w-100" type="submit">
|
||||||
|
<span class="visually-hidden spinner-border spinner-border-sm"
|
||||||
|
role="status"></span>
|
||||||
|
<span>Continue</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
</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")
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit(event) {
|
||||||
|
setButtonLoading()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('submit', onSubmit)
|
||||||
|
button.addEventListener('click', onSubmit)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+11
-3
@@ -63,9 +63,17 @@ func IsLoggedIn(r *http.Request) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func IsVerified(r *http.Request) bool {
|
func IsVerified(r *http.Request) bool {
|
||||||
_, claims, _ := jwtauth.FromContext(r.Context())
|
token, err := jwtauth.VerifyRequest(
|
||||||
userIDClaim, _ := claims["user_id"].(uint32)
|
TokenAuth,
|
||||||
currentUser, _ := Container.GetUserUseCase.GetUser(userIDClaim)
|
r,
|
||||||
|
jwtauth.TokenFromCookie,
|
||||||
|
jwtauth.TokenFromHeader,
|
||||||
|
jwtauth.TokenFromQuery)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
userId := token.PrivateClaims()["user_id"].(float64)
|
||||||
|
currentUser, _ := Container.GetUserUseCase.GetUser(uint32(userId))
|
||||||
return currentUser.IsVerified
|
return currentUser.IsVerified
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type UseCases struct {
|
|||||||
ListPostsUseCase *useCases.ListPostsUseCase
|
ListPostsUseCase *useCases.ListPostsUseCase
|
||||||
GetUserUseCase *useCases.GetUserUseCase
|
GetUserUseCase *useCases.GetUserUseCase
|
||||||
CreateUserUseCase *useCases.CreateUserUseCase
|
CreateUserUseCase *useCases.CreateUserUseCase
|
||||||
|
UpdateUserUseCase *useCases.UpdateUserUseCase
|
||||||
ListUsersUseCase *useCases.ListUsersUseCase
|
ListUsersUseCase *useCases.ListUsersUseCase
|
||||||
GetPageUseCase *useCases.GetPageUseCase
|
GetPageUseCase *useCases.GetPageUseCase
|
||||||
SendMailUseCase *useCases.SendMailUseCase
|
SendMailUseCase *useCases.SendMailUseCase
|
||||||
@@ -49,6 +50,7 @@ func InitContainer() {
|
|||||||
ListPostsUseCase: useCases.NewListPostsUseCase(db),
|
ListPostsUseCase: useCases.NewListPostsUseCase(db),
|
||||||
GetUserUseCase: useCases.NewGetUserUseCase(db),
|
GetUserUseCase: useCases.NewGetUserUseCase(db),
|
||||||
CreateUserUseCase: useCases.NewCreateUserUseCase(db),
|
CreateUserUseCase: useCases.NewCreateUserUseCase(db),
|
||||||
|
UpdateUserUseCase: useCases.NewUpdateUserUseCase(db),
|
||||||
ListUsersUseCase: useCases.NewListUsersUseCase(db),
|
ListUsersUseCase: useCases.NewListUsersUseCase(db),
|
||||||
GetPageUseCase: useCases.NewGetPageUseCase(),
|
GetPageUseCase: useCases.NewGetPageUseCase(),
|
||||||
SendMailUseCase: useCases.NewSendMailUseCase(),
|
SendMailUseCase: useCases.NewSendMailUseCase(),
|
||||||
|
|||||||
+11
@@ -57,6 +57,16 @@ func IsVerifiedMiddleware(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsNotVerifiedMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if IsVerified(r) {
|
||||||
|
http.Redirect(w, r, "/register/pending", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func GetLogin(w http.ResponseWriter, r *http.Request) {
|
func GetLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, LoginRoute, http.StatusPermanentRedirect)
|
http.Redirect(w, r, LoginRoute, http.StatusPermanentRedirect)
|
||||||
}
|
}
|
||||||
@@ -101,6 +111,7 @@ func NewPageRouter() http.Handler {
|
|||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(IsLoggedInMiddleware)
|
r.Use(IsLoggedInMiddleware)
|
||||||
|
r.Use(IsNotVerifiedMiddleware)
|
||||||
r.Get("/register/pending", GetRegisterPendingPage)
|
r.Get("/register/pending", GetRegisterPendingPage)
|
||||||
r.Get("/register/validate", GetRegisterValidatePage)
|
r.Get("/register/validate", GetRegisterValidatePage)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,17 +14,25 @@ func GetRegisterValidatePage(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, claims, _ := jwtauth.FromContext(r.Context())
|
token, _ := jwtauth.VerifyRequest(
|
||||||
userId, _ := claims["user_id"].(uint32)
|
TokenAuth,
|
||||||
|
r,
|
||||||
|
jwtauth.TokenFromCookie,
|
||||||
|
jwtauth.TokenFromHeader,
|
||||||
|
jwtauth.TokenFromQuery)
|
||||||
|
|
||||||
user, _ := Container.GetUserUseCase.GetUser(userId)
|
userId := token.PrivateClaims()["user_id"].(float64)
|
||||||
|
user, _ := Container.GetUserUseCase.GetUser(uint32(userId))
|
||||||
errorMessage := ""
|
errorMessage := ""
|
||||||
|
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(user.VerificationCode), []byte(queryVerificationCode))
|
err := bcrypt.CompareHashAndPassword([]byte(user.VerificationCode), []byte(queryVerificationCode))
|
||||||
if err != nil || user.VerificationExpiration.Before(time.Now()) {
|
if err != nil || user.VerificationExpiration.Before(time.Now()) {
|
||||||
errorMessage = "Verification link is incorrect or has expired."
|
errorMessage = "Verification link is incorrect or has expired."
|
||||||
} else {
|
} else {
|
||||||
// TODO New usecase "UpdateUserUseCase" to update its verification status
|
_, err := Container.UpdateUserUseCase.UpdateVerificationStatus(user.ID, true)
|
||||||
|
if err != nil {
|
||||||
|
errorMessage = "Something went wrong server-side. User account may not exist."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerValidateTmpl, _ := Container.GetPageUseCase.GetPage("registerValidate", map[string]interface{}{
|
registerValidateTmpl, _ := Container.GetPageUseCase.GetPage("registerValidate", map[string]interface{}{
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ 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)
|
||||||
|
UpdateVerificationStatus(userId uint32, isVerified bool) (user.User, error)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -22,13 +22,14 @@ func FromApi(
|
|||||||
email string,
|
email string,
|
||||||
verificationCode string,
|
verificationCode string,
|
||||||
) User {
|
) User {
|
||||||
|
expiration := time.Now().Add(2 * time.Hour)
|
||||||
return User{
|
return User{
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: password,
|
Password: password,
|
||||||
Email: email,
|
Email: email,
|
||||||
IsVerified: false,
|
IsVerified: false,
|
||||||
VerificationCode: verificationCode,
|
VerificationCode: verificationCode,
|
||||||
VerificationExpiration: time.Now().Add(2 * time.Hour),
|
VerificationExpiration: expiration,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package useCases
|
||||||
|
|
||||||
|
import (
|
||||||
|
"GohCMS2/adapters/secondary/gateways"
|
||||||
|
"GohCMS2/domain/user"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateUserUseCase struct {
|
||||||
|
userRepository gateways.UserRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateVerificationStatusCommand struct {
|
||||||
|
isVerified bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdateUserUseCase(db *gorm.DB) *UpdateUserUseCase {
|
||||||
|
return &UpdateUserUseCase{
|
||||||
|
userRepository: *gateways.NewUserRepository(db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *UpdateUserUseCase) UpdateVerificationStatus(userId uint32, isVerified bool) (user.User, error) {
|
||||||
|
return g.userRepository.UpdateVerificationStatus(userId, isVerified)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user