diff --git a/adapters/secondary/gateways/mail/templates/passwordReset.html b/adapters/secondary/gateways/mail/templates/passwordReset.html
new file mode 100644
index 0000000..8fe8f05
--- /dev/null
+++ b/adapters/secondary/gateways/mail/templates/passwordReset.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+ GoCMS
+
+
+ You or someone tried to create reset your GoCMS account with this e-mail address. If you are not responsible for
+ this, please do not validate the password reset.
+ If you want to reset your password, please click HERE.
+
+ If you can't click the link:
+ {{.Host}}/register/reset/validate?c={{.VerificationCode}}&email={{.Email}}
+
+
+
diff --git a/adapters/secondary/gateways/models/user.go b/adapters/secondary/gateways/models/user.go
index 9fc1779..141a836 100644
--- a/adapters/secondary/gateways/models/user.go
+++ b/adapters/secondary/gateways/models/user.go
@@ -7,13 +7,14 @@ import (
type User struct {
gorm.Model
- ID uint32 `gorm:"primaryKey;autoIncrement"`
- Username string `gorm:"unique;not null"`
- Password string `gorm:"not null"`
- Email string `gorm:"unique;not null"`
- IsVerified bool `gorm:"default=false;not null"`
- VerificationCode string `gorm:"unique;not null"`
- VerificationExpiration time.Time `gorm:"not null"`
+ ID uint32 `gorm:"primaryKey;autoIncrement"`
+ Username string `gorm:"unique;not null"`
+ Password string `gorm:"not null"`
+ PasswordResetCode string `gorm:"unique"`
+ Email string `gorm:"unique;not null"`
+ IsVerified bool `gorm:"default=false;not null"`
+ VerificationCode string `gorm:"unique"`
+ VerificationExpiration time.Time
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
diff --git a/adapters/secondary/gateways/userRepository.go b/adapters/secondary/gateways/userRepository.go
index ebb6d2c..b0e5561 100644
--- a/adapters/secondary/gateways/userRepository.go
+++ b/adapters/secondary/gateways/userRepository.go
@@ -22,6 +22,7 @@ func mapUserToDomain(user entity.User) domain.User {
user.ID,
user.Username,
user.Password,
+ user.PasswordResetCode,
user.Email,
user.IsVerified,
user.VerificationCode,
@@ -88,6 +89,16 @@ func (u *UserRepository) GetByUsername(username string) (domain.User, error) {
return mapUserToDomain(localUser), nil
}
+func (u *UserRepository) GetByEmail(email string) (domain.User, error) {
+ var localUser entity.User
+ err := u.db.Model(&entity.User{}).Where("email = ?", email).First(&localUser).Error
+ if err != nil {
+ return domain.User{}, err
+ }
+
+ return mapUserToDomain(localUser), nil
+}
+
func (u *UserRepository) UpdateVerificationStatus(userId uint32, isVerified bool) (domain.User, error) {
var localUser entity.User
err := u.db.Model(&entity.User{}).First(&localUser, userId).Error
@@ -104,4 +115,38 @@ func (u *UserRepository) UpdateVerificationStatus(userId uint32, isVerified bool
return mapUserToDomain(localUser), nil
}
+func (u *UserRepository) UpdatePassword(userId uint32, password string) (domain.User, error) {
+ var localUser entity.User
+ err := u.db.Model(&entity.User{}).First(&localUser, userId).Error
+ if err != nil {
+ return domain.User{}, err
+ }
+
+ hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), 12)
+ localUser.Password = string(hashedPassword)
+ err = u.db.Save(&localUser).Error
+ if err != nil {
+ return domain.User{}, err
+ }
+
+ return mapUserToDomain(localUser), nil
+}
+
+func (u *UserRepository) UpdatePasswordResetCode(userId uint32, code string) (domain.User, error) {
+ var localUser entity.User
+ err := u.db.Model(&entity.User{}).First(&localUser, userId).Error
+ if err != nil {
+ return domain.User{}, err
+ }
+
+ hashedCode, _ := bcrypt.GenerateFromPassword([]byte(code), 12)
+ localUser.PasswordResetCode = string(hashedCode)
+ err = u.db.Save(&localUser).Error
+ if err != nil {
+ return domain.User{}, err
+ }
+
+ 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 8923ee4..edab160 100644
--- a/adapters/secondary/gateways/web/templates/login.html
+++ b/adapters/secondary/gateways/web/templates/login.html
@@ -1,70 +1,82 @@
- GoCMS | Login
- {{.Head}}
+ GoCMS | Login
+ {{.Head}}
-
+
+
+
+
diff --git a/adapters/secondary/gateways/web/templates/passwordResetValidate.html b/adapters/secondary/gateways/web/templates/passwordResetValidate.html
new file mode 100644
index 0000000..d6b84bc
--- /dev/null
+++ b/adapters/secondary/gateways/web/templates/passwordResetValidate.html
@@ -0,0 +1,140 @@
+
+
+
+ GoCMS | Password reset
+ {{.Head}}
+
+
+
+
+
+
+
+
+
+
diff --git a/api/page.go b/api/page.go
index a8d9ba1..4370f47 100644
--- a/api/page.go
+++ b/api/page.go
@@ -105,30 +105,47 @@ func NewPageRouter() http.Handler {
r.Handle("/static/*", http.StripPrefix("/static/", fileServer))
r.Get("/", GetLogin)
+
r.Get(LoginRoute, GetLoginPageHandler(EmptyLoginPage))
r.Post(LoginRoute, GetLoginPageHandler(EmptyLoginPage))
+
r.Get("/register", GetRegisterPageHandler(EmptyRegisterPage))
r.Post("/register", GetRegisterPageHandler(EmptyRegisterPage))
+
r.Get("/logout", GetLogout)
+ r.Get("/register/reset/request", GetPasswordResetRequest)
+ r.Post("/register/reset/request", PostPasswordResetRequest)
+
+ r.Get("/register/reset/validate", GetPasswordResetValidate)
+ r.Post("/register/reset/validate", PostPasswordResetValidate)
+
r.Group(func(r chi.Router) {
r.Use(IsLoggedInMiddleware)
r.Use(IsNotVerifiedMiddleware)
+
r.Get("/register/pending", GetRegisterPendingPage)
r.Post("/register/pending", PostRegisterPendingPage)
+
r.Get("/register/validate", GetRegisterValidatePage)
})
r.Group(func(r chi.Router) {
r.Use(IsLoggedInMiddleware)
r.Use(IsVerifiedMiddleware)
+
r.Get("/home", GetHomePage)
+
r.Get("/post", GetPostsPage)
+
r.Get("/post/{id}/edit", GetPostEditPage)
r.Post("/post/{id}/edit", PostPostEditPage)
+
r.Get("/post/{id}/delete", GetPostDeletePage)
+
r.Get("/post/create", GetPostCreatePage)
r.Post("/post/create", PostPostCreatePage)
+
r.Post("/post/{id}/image/create", postImage)
})
diff --git a/api/pageLogin.go b/api/pageLogin.go
index 10c026d..0d4e862 100644
--- a/api/pageLogin.go
+++ b/api/pageLogin.go
@@ -2,6 +2,7 @@ package api
import (
"net/http"
+ "net/url"
)
const LoginRoute = "/login"
@@ -30,14 +31,13 @@ func GetLoginPageHandler(loginPage *LoginPage) http.HandlerFunc {
http.Redirect(w, r, "/register", http.StatusSeeOther)
return
}
- bs, err := Container.GetPageUseCase.GetPage("login", map[string]interface{}{
+ success, _ := url.QueryUnescape(r.URL.Query().Get("success"))
+ bs, _ := Container.GetPageUseCase.GetPage("login", map[string]interface{}{
"PageError": loginPage.PageError,
"Username": loginPage.Username,
"Head": headTmpl,
+ "Success": success,
})
- if err != nil {
- _, _ = w.Write([]byte(err.Error()))
- }
_, _ = w.Write(bs)
}
}
diff --git a/api/pagePasswordResetRequest.go b/api/pagePasswordResetRequest.go
new file mode 100644
index 0000000..e65ff7c
--- /dev/null
+++ b/api/pagePasswordResetRequest.go
@@ -0,0 +1,56 @@
+package api
+
+import (
+ "github.com/google/uuid"
+ "net/http"
+ "os"
+)
+
+type PasswordResetRequest struct {
+ Email string `json:"email"`
+}
+
+var successMessage = "An email has been sent to the provided email address if it exists."
+
+func GetPasswordResetRequest(w http.ResponseWriter, r *http.Request) {
+ success := r.URL.Query().Get("success")
+ email := r.URL.Query().Get("email")
+
+ bs, _ := Container.GetPageUseCase.GetPage("passwordResetRequest", map[string]interface{}{
+ "Head": headTmpl,
+ "Email": email,
+ "Success": success,
+ })
+ _, _ = w.Write(bs)
+}
+
+func PostPasswordResetRequest(w http.ResponseWriter, r *http.Request) {
+ _ = r.ParseForm()
+ passResetReq := PasswordResetRequest{
+ Email: r.FormValue("email"),
+ }
+ var getRedirectUrl = "/register/reset/request?success=" + successMessage + "&email=" + passResetReq.Email
+
+ err := validate.Struct(passResetReq)
+ if err != nil {
+ http.Redirect(w, r, getRedirectUrl, http.StatusSeeOther)
+ return
+ }
+
+ fetchedUser, err := Container.GetUserUseCase.GetUserByEmail(passResetReq.Email)
+ if err != nil {
+ http.Redirect(w, r, getRedirectUrl, http.StatusSeeOther)
+ return
+ }
+
+ verificationCode := uuid.NewString()
+ updatedUser, _ := Container.UpdateUserUseCase.UpdatePasswordResetCode(fetchedUser.ID, verificationCode)
+
+ _ = Container.SendMailUseCase.SendMail(updatedUser.Email, "passwordReset", map[string]string{
+ "Host": os.Getenv("HOST"),
+ "VerificationCode": verificationCode,
+ "Email": updatedUser.Email,
+ })
+
+ http.Redirect(w, r, getRedirectUrl, http.StatusSeeOther)
+}
diff --git a/api/pagePasswordResetValidate.go b/api/pagePasswordResetValidate.go
new file mode 100644
index 0000000..6218767
--- /dev/null
+++ b/api/pagePasswordResetValidate.go
@@ -0,0 +1,55 @@
+package api
+
+import (
+ "golang.org/x/crypto/bcrypt"
+ "net/http"
+)
+
+var PasswordLinkErrorMessage = "The reset password link you used is invalid."
+var PasswordLinkSuccessMessage = "Your account password was successfully updated."
+
+func GetPasswordResetValidate(w http.ResponseWriter, r *http.Request) {
+ failure := r.URL.Query().Get("failure")
+ pageError := NewPageError("")
+ if failure != "" {
+ pageError = NewPageError(failure)
+ }
+ template, _ := Container.GetPageUseCase.GetPage("passwordResetValidate", map[string]interface{}{
+ "Head": headTmpl,
+ "Error": pageError,
+ "Email": r.URL.Query().Get("email"),
+ "Code": r.URL.Query().Get("c"),
+ })
+ _, _ = w.Write(template)
+}
+
+func PostPasswordResetValidate(w http.ResponseWriter, r *http.Request) {
+ _ = r.ParseForm()
+ email := r.FormValue("email")
+ password := r.FormValue("password")
+ resetCode := r.FormValue("code")
+
+ var redirectionErrorLink = "/register/reset/validate?email=" + email + "&c=" + resetCode
+
+ if len(password) < 8 {
+ http.Redirect(w, r, redirectionErrorLink+"&failure=Password length should be at least 8 characters.", http.StatusSeeOther)
+ return
+ }
+
+ fetchedUser, err := Container.GetUserUseCase.GetUserByEmail(email)
+ if err != nil {
+ http.Redirect(w, r, redirectionErrorLink+"&failure="+PasswordLinkErrorMessage, http.StatusSeeOther)
+ return
+ }
+
+ err = bcrypt.CompareHashAndPassword([]byte(fetchedUser.PasswordResetCode), []byte(resetCode))
+ if err != nil {
+ http.Redirect(w, r, redirectionErrorLink+"&failure="+PasswordLinkErrorMessage, http.StatusSeeOther)
+ return
+ }
+
+ _, _ = Container.UpdateUserUseCase.UpdatePassword(fetchedUser.ID, password)
+ _, _ = Container.UpdateUserUseCase.UpdatePasswordResetCode(fetchedUser.ID, "")
+
+ http.Redirect(w, r, "/login?success="+PasswordLinkSuccessMessage, http.StatusSeeOther)
+}
diff --git a/domain/gateways/IUserRepository.go b/domain/gateways/IUserRepository.go
index 0213346..c96d084 100644
--- a/domain/gateways/IUserRepository.go
+++ b/domain/gateways/IUserRepository.go
@@ -5,8 +5,11 @@ import "GoCMS/domain/user"
type IUserRepository interface {
Get(id uint32) (user.User, error)
GetByUsername(username string) (user.User, error)
+ GetByEmail(email 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)
+ UpdatePassword(userId uint32, password string) (user.User, error)
+ UpdatePasswordResetCode(userId uint32, code string) (user.User, error)
}
diff --git a/domain/image/image.go b/domain/image/image.go
index 2f83483..4ffc779 100644
--- a/domain/image/image.go
+++ b/domain/image/image.go
@@ -5,11 +5,11 @@ import (
)
type Image struct {
- ID uint32
- Path string
- PostID uint32
- CreatedAt time.Time
- UpdatedAt time.Time
+ ID uint32 `json:"id"`
+ Path string `json:"path"`
+ PostID uint32 `json:"post_id"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
}
func FromDB(id uint32, path string, postId uint32, createdAt time.Time, updatedAt time.Time) Image {
diff --git a/domain/user/user.go b/domain/user/user.go
index 19cd249..ddb3689 100644
--- a/domain/user/user.go
+++ b/domain/user/user.go
@@ -8,10 +8,11 @@ type User struct {
ID uint32 `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
+ PasswordResetCode string `json:"password_reset_code"`
Email string `json:"email"`
- IsVerified bool `gorm:"default=false;not null"`
- VerificationCode string `gorm:"unique;not null"`
- VerificationExpiration time.Time `gorm:"not null"`
+ IsVerified bool `json:"is_verified"`
+ VerificationCode string `json:"verification_code"`
+ VerificationExpiration time.Time `json:"verification_expiration"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -19,6 +20,7 @@ type User struct {
func FromApi(
username string,
password string,
+ passwordResetCode string,
email string,
verificationCode string,
) User {
@@ -26,6 +28,7 @@ func FromApi(
return User{
Username: username,
Password: password,
+ PasswordResetCode: passwordResetCode,
Email: email,
IsVerified: false,
VerificationCode: verificationCode,
@@ -37,6 +40,7 @@ func FromDb(
id uint32,
username string,
password string,
+ passwordResetCode string,
email string,
isVerified bool,
verificationCode string,
@@ -48,6 +52,7 @@ func FromDb(
ID: id,
Username: username,
Password: password,
+ PasswordResetCode: passwordResetCode,
Email: email,
IsVerified: isVerified,
VerificationCode: verificationCode,
diff --git a/useCases/CreateUserUseCase.go b/useCases/CreateUserUseCase.go
index c2335b8..a25ba1a 100644
--- a/useCases/CreateUserUseCase.go
+++ b/useCases/CreateUserUseCase.go
@@ -27,6 +27,7 @@ func (g *CreateUserUseCase) CreateUser(createUser CreateUserCommand) (user.User,
return g.userRepository.Create(user.FromApi(
createUser.Username,
createUser.Password,
+ "",
createUser.Email,
createUser.VerificationCode,
))
diff --git a/useCases/GetUserUseCase.go b/useCases/GetUserUseCase.go
index 2a4b351..9d9694c 100644
--- a/useCases/GetUserUseCase.go
+++ b/useCases/GetUserUseCase.go
@@ -23,3 +23,7 @@ func (g *GetUserUseCase) GetUser(id uint32) (user.User, error) {
func (g *GetUserUseCase) GetUserByUsername(username string) (user.User, error) {
return g.userRepository.GetByUsername(username)
}
+
+func (g *GetUserUseCase) GetUserByEmail(email string) (user.User, error) {
+ return g.userRepository.GetByEmail(email)
+}
diff --git a/useCases/UpdateUserUseCase.go b/useCases/UpdateUserUseCase.go
index 1005819..8fe26c4 100644
--- a/useCases/UpdateUserUseCase.go
+++ b/useCases/UpdateUserUseCase.go
@@ -19,3 +19,11 @@ func NewUpdateUserUseCase(db *gorm.DB) *UpdateUserUseCase {
func (g *UpdateUserUseCase) UpdateVerificationStatus(userId uint32, isVerified bool) (user.User, error) {
return g.userRepository.UpdateVerificationStatus(userId, isVerified)
}
+
+func (g *UpdateUserUseCase) UpdatePasswordResetCode(userId uint32, code string) (user.User, error) {
+ return g.userRepository.UpdatePasswordResetCode(userId, code)
+}
+
+func (g *UpdateUserUseCase) UpdatePassword(userId uint32, password string) (user.User, error) {
+ return g.userRepository.UpdatePassword(userId, password)
+}