diff --git a/controllers/auth.controller.go b/controllers/auth.controller.go
index 2416863..52cb1dd 100644
--- a/controllers/auth.controller.go
+++ b/controllers/auth.controller.go
@@ -3,6 +3,7 @@ package controllers
import (
"context"
"fmt"
+ "html/template"
"log"
"net/http"
"strings"
@@ -23,10 +24,11 @@ type AuthController struct {
userService services.UserService
ctx context.Context
collection *mongo.Collection
+ temp *template.Template
}
-func NewAuthController(authService services.AuthService, userService services.UserService, ctx context.Context, collection *mongo.Collection) AuthController {
- return AuthController{authService, userService, ctx, collection}
+func NewAuthController(authService services.AuthService, userService services.UserService, ctx context.Context, collection *mongo.Collection, temp *template.Template) AuthController {
+ return AuthController{authService, userService, ctx, collection, temp}
}
func (ac *AuthController) SignUpUser(ctx *gin.Context) {
@@ -79,7 +81,7 @@ func (ac *AuthController) SignUpUser(ctx *gin.Context) {
Subject: "Your account verification code",
}
- err = utils.SendEmail(newUser, &emailData, "verificationCode.html")
+ err = utils.SendEmail(newUser, &emailData, ac.temp, "verificationCode.html")
if err != nil {
ctx.JSON(http.StatusBadGateway, gin.H{"status": "success", "message": "There was an error sending email"})
return
@@ -264,15 +266,55 @@ func (ac *AuthController) ForgotPassword(ctx *gin.Context) {
// 👇 Send Email
emailData := utils.EmailData{
- URL: config.Origin + "/forgotPassword/" + resetToken,
+ URL: config.Origin + "/resetpassword/" + resetToken,
FirstName: firstName,
Subject: "Your password reset token (valid for 10min)",
}
- err = utils.SendEmail(user, &emailData, "resetPassword.html")
+ err = utils.SendEmail(user, &emailData, ac.temp, "resetPassword.html")
if err != nil {
ctx.JSON(http.StatusBadGateway, gin.H{"status": "success", "message": "There was an error sending email"})
return
}
ctx.JSON(http.StatusOK, gin.H{"status": "success", "message": message})
}
+
+func (ac *AuthController) ResetPassword(ctx *gin.Context) {
+ var userCredential *models.ResetPasswordInput
+ resetToken := ctx.Params.ByName("resetToken")
+
+ if err := ctx.ShouldBindJSON(&userCredential); err != nil {
+ ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
+ return
+ }
+
+ if userCredential.Password != userCredential.PasswordConfirm {
+ ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": "Passwords do not match"})
+ return
+ }
+
+ hashedPassword, _ := utils.HashPassword(userCredential.Password)
+
+ passwordResetToken := utils.Encode(resetToken)
+
+ // Update User in Database
+ query := bson.D{{Key: "passwordResetToken", Value: passwordResetToken}}
+ update := bson.D{{Key: "$set", Value: bson.D{{Key: "password", Value: hashedPassword}}}, {Key: "$unset", Value: bson.D{{Key: "passwordResetToken", Value: ""}, {Key: "passwordResetAt", Value: ""}}}}
+ result, err := ac.collection.UpdateOne(ac.ctx, query, update)
+
+ if result.MatchedCount == 0 {
+ ctx.JSON(http.StatusBadRequest, gin.H{"status": "success", "message": "Token is invalid or has expired"})
+ return
+ }
+
+ if err != nil {
+ ctx.JSON(http.StatusForbidden, gin.H{"status": "success", "message": err.Error()})
+ return
+ }
+
+ ctx.SetCookie("access_token", "", -1, "/", "localhost", false, true)
+ ctx.SetCookie("refresh_token", "", -1, "/", "localhost", false, true)
+ ctx.SetCookie("logged_in", "", -1, "/", "localhost", false, true)
+
+ ctx.JSON(http.StatusOK, gin.H{"status": "success", "message": "Password data updated successfully"})
+}
diff --git a/go.mod b/go.mod
index 8ff0e1b..433d072 100644
--- a/go.mod
+++ b/go.mod
@@ -7,9 +7,12 @@ require (
github.com/gin-gonic/gin v1.7.7
github.com/go-redis/redis/v8 v8.11.5
github.com/golang-jwt/jwt v3.2.2+incompatible
+ github.com/k3a/html2text v1.0.8
github.com/spf13/viper v1.11.0
+ github.com/thanhpk/randstr v1.0.4
go.mongodb.org/mongo-driver v1.9.1
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9
+ gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
)
require (
@@ -25,7 +28,6 @@ require (
github.com/golang/snappy v0.0.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/k3a/html2text v1.0.8 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/magiconair/properties v1.8.6 // indirect
@@ -41,7 +43,6 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
- github.com/thanhpk/randstr v1.0.4 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.0.2 // indirect
@@ -52,7 +53,6 @@ require (
golang.org/x/text v0.3.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
- gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
diff --git a/go.sum b/go.sum
index 787b1ed..b42fa99 100644
--- a/go.sum
+++ b/go.sum
@@ -153,6 +153,7 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -166,6 +167,7 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/k3a/html2text v1.0.8 h1:rVanLhKilpnJUJs/CNKWzMC4YaQINGxK0rSG8ssmnV0=
github.com/k3a/html2text v1.0.8/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA=
@@ -218,7 +220,9 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
diff --git a/main.go b/main.go
index a5d7e95..984b4e5 100644
--- a/main.go
+++ b/main.go
@@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
+ "html/template"
"log"
"net/http"
@@ -32,9 +33,12 @@ var (
authService services.AuthService
AuthController controllers.AuthController
AuthRouteController routes.AuthRouteController
+
+ temp *template.Template
)
func init() {
+ temp = template.Must(template.ParseGlob("templates/*.html"))
config, err := config.LoadConfig(".")
if err != nil {
log.Fatal("Could not load environment variables", err)
@@ -76,7 +80,7 @@ func init() {
authCollection = mongoclient.Database("golang_mongodb").Collection("users")
userService = services.NewUserServiceImpl(authCollection, ctx)
authService = services.NewAuthService(authCollection, ctx)
- AuthController = controllers.NewAuthController(authService, userService, ctx, authCollection)
+ AuthController = controllers.NewAuthController(authService, userService, ctx, authCollection, temp)
AuthRouteController = routes.NewAuthRouteController(AuthController)
UserController = controllers.NewUserController(userService)
@@ -103,7 +107,7 @@ func main() {
}
corsConfig := cors.DefaultConfig()
- corsConfig.AllowOrigins = []string{config.Origin}
+ corsConfig.AllowOrigins = []string{config.Origin, "http://127.0.0.1:3000/"}
corsConfig.AllowCredentials = true
server.Use(cors.New(corsConfig))
diff --git a/models/user.model.go b/models/user.model.go
index c6e6ff7..019465a 100644
--- a/models/user.model.go
+++ b/models/user.model.go
@@ -8,17 +8,14 @@ import (
// 👈 SignUpInput struct
type SignUpInput struct {
- Name string `json:"name" bson:"name" binding:"required"`
- Email string `json:"email" bson:"email" binding:"required"`
- Password string `json:"password" bson:"password" binding:"required,min=8"`
- PasswordConfirm string `json:"passwordConfirm" bson:"passwordConfirm,omitempty" binding:"required"`
- Role string `json:"role" bson:"role"`
- VerificationCode string `json:"verificationCode,omitempty" bson:"verificationCode,omitempty"`
- ResetPasswordToken string `json:"resetPasswordToken,omitempty" bson:"resetPasswordToken,omitempty"`
- ResetPasswordAt time.Time `json:"resetPasswordAt,omitempty" bson:"resetPasswordAt,omitempty"`
- Verified bool `json:"verified" bson:"verified"`
- CreatedAt time.Time `json:"created_at" bson:"created_at"`
- UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
+ Name string `json:"name" bson:"name" binding:"required"`
+ Email string `json:"email" bson:"email" binding:"required"`
+ Password string `json:"password" bson:"password" binding:"required,min=8"`
+ PasswordConfirm string `json:"passwordConfirm" bson:"passwordConfirm,omitempty" binding:"required"`
+ Role string `json:"role" bson:"role"`
+ Verified bool `json:"verified" bson:"verified"`
+ CreatedAt time.Time `json:"created_at" bson:"created_at"`
+ UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
}
// 👈 SignInInput struct
@@ -29,18 +26,15 @@ type SignInInput struct {
// 👈 DBResponse struct
type DBResponse struct {
- ID primitive.ObjectID `json:"id" bson:"_id"`
- Name string `json:"name" bson:"name"`
- Email string `json:"email" bson:"email"`
- Password string `json:"password" bson:"password"`
- PasswordConfirm string `json:"passwordConfirm,omitempty" bson:"passwordConfirm,omitempty"`
- Role string `json:"role" bson:"role"`
- VerificationCode string `json:"verificationCode,omitempty" bson:"verificationCode"`
- ResetPasswordToken string `json:"resetPasswordToken,omitempty" bson:"resetPasswordToken,omitempty"`
- ResetPasswordAt time.Time `json:"resetPasswordAt,omitempty" bson:"resetPasswordAt,omitempty"`
- Verified bool `json:"verified" bson:"verified"`
- CreatedAt time.Time `json:"created_at" bson:"created_at"`
- UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
+ ID primitive.ObjectID `json:"id" bson:"_id"`
+ Name string `json:"name" bson:"name"`
+ Email string `json:"email" bson:"email"`
+ Password string `json:"password" bson:"password"`
+ PasswordConfirm string `json:"passwordConfirm,omitempty" bson:"passwordConfirm,omitempty"`
+ Role string `json:"role" bson:"role"`
+ Verified bool `json:"verified" bson:"verified"`
+ CreatedAt time.Time `json:"created_at" bson:"created_at"`
+ UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
}
// 👈 UserResponse struct
@@ -55,13 +49,13 @@ type UserResponse struct {
// 👈 ForgotPasswordInput struct
type ForgotPasswordInput struct {
- Email string `json:"email" bson:"email" binding:"required"`
+ Email string `json:"email" binding:"required"`
}
// 👈 ResetPasswordInput struct
type ResetPasswordInput struct {
- Password string `json:"password" bson:"password"`
- PasswordConfirm string `json:"passwordConfirm,omitempty" bson:"passwordConfirm,omitempty"`
+ Password string `json:"password" binding:"required"`
+ PasswordConfirm string `json:"passwordConfirm" binding:"required"`
}
func FilteredResponse(user *DBResponse) UserResponse {
diff --git a/readMe.md b/readMe.md
index 3ab3782..378b853 100644
--- a/readMe.md
+++ b/readMe.md
@@ -1,4 +1,23 @@
-# API with Golang + MongoDB + Redis + Gin Gonic
+# API with Golang, Gin Gonic & MongoDB: Forget/Reset Password
+
+In this article, you'll learn how to implement forget/reset password functionality with Golang, Gin Gonic, Gomail, MongoDB-Go-driver, Redis, and Docker-compose.
+
+
+
+## Topics Covered
+
+- Forget/Reset Password with Golang, Gin, and MongoDB
+- Create the MongoDB Model Structs
+- Create the HTML Email Templates with Golang
+- Define a Utility Function to Parse the HTML Templates
+- Create a Function to Send the HTML Emails
+- Add the Forgot Password Controller
+- Add the Reset Password Controller
+- Register the Gin API Routes
+
+Read the entire article here: [https://codevoweb.com/api-golang-gin-gonic-mongodb-forget-reset-password](https://codevoweb.com/api-golang-gin-gonic-mongodb-forget-reset-password)
+
+Articles in this series:
### 1. API with Golang + MongoDB + Redis + Gin Gonic: Project Setup
@@ -11,3 +30,23 @@
### 3. API with Golang + MongoDB: Send HTML Emails with Gomail
[API with Golang + MongoDB: Send HTML Emails with Gomail](https://codevoweb.com/api-golang-mongodb-send-html-emails-gomail)
+
+### 4. API with Golang, Gin Gonic & MongoDB: Forget/Reset Password
+
+[API with Golang, Gin Gonic & MongoDB: Forget/Reset Password](https://codevoweb.com/api-golang-gin-gonic-mongodb-forget-reset-password)
+
+### 5. Build Golang gRPC Server and Client: SignUp User & Verify Email
+
+[Build Golang gRPC Server and Client: SignUp User & Verify Email](https://codevoweb.com/golang-grpc-server-and-client-signup-user-verify-email)
+
+### 6. Build Golang gRPC Server and Client: Access & Refresh Tokens
+
+[Build Golang gRPC Server and Client: Access & Refresh Tokens](https://codevoweb.com/golang-grpc-server-and-client-access-refresh-tokens)
+
+### 7. Build CRUD RESTful API Server with Golang, Gin, and MongoDB
+
+[Build CRUD RESTful API Server with Golang, Gin, and MongoDB](https://codevoweb.com/crud-restful-api-server-with-golang-and-mongodb)
+
+### 8. Build CRUD gRPC Server API & Client with Golang and MongoDB
+
+[Build CRUD gRPC Server API & Client with Golang and MongoDB](https://codevoweb.com/crud-grpc-server-api-client-with-golang-and-mongodb)
diff --git a/routes/auth.routes.go b/routes/auth.routes.go
index d480c8b..730ceae 100644
--- a/routes/auth.routes.go
+++ b/routes/auth.routes.go
@@ -23,5 +23,6 @@ func (rc *AuthRouteController) AuthRoute(rg *gin.RouterGroup, userService servic
router.GET("/refresh", rc.authController.RefreshAccessToken)
router.GET("/logout", middleware.DeserializeUser(userService), rc.authController.LogoutUser)
router.GET("/verifyemail/:verificationCode", rc.authController.VerifyEmail)
- router.POST("/forgotPassword", rc.authController.ForgotPassword)
+ router.POST("/forgotpassword", rc.authController.ForgotPassword)
+ router.PATCH("/resetpassword/:resetToken", rc.authController.ResetPassword)
}
diff --git a/templates/resetPassword.html b/templates/resetPassword.html
index b067c03..dd38d4a 100644
--- a/templates/resetPassword.html
+++ b/templates/resetPassword.html
@@ -1,54 +1,89 @@
-{{template "base" .}} {{define "content"}}
-
-
-
-
-
-
-
- Hi {{ .FirstName}},
-
- Forgot password? Send a PATCH request to with your password and
- passwordConfirm to {{.URL}}
-
-
- If you didn't forget your password, please ignore this email
- Good luck! Codevo CEO.
- |
-
-
- |
-
+
+
+
+
+
+ {{template "styles" .}}
+ {{ .Subject}}
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ Hi {{ .FirstName}},
+
+ Forgot password? Send a PATCH request to with your
+ password and passwordConfirm to {{.URL}}
+
+
+
+ If you didn't forget your password, please ignore this
+ email
+
+ Good luck! Codevo CEO.
+ |
+
+
+ |
+
-
-
-{{end}}
+
+ |
+
+
+
+ |
+
+
+
+
diff --git a/utils/email.go b/utils/email.go
index 4fabae8..4788a19 100644
--- a/utils/email.go
+++ b/utils/email.go
@@ -3,11 +3,8 @@ package utils
import (
"bytes"
"crypto/tls"
- "fmt"
+ "html/template"
"log"
- "os"
- "path/filepath"
- "text/template"
"github.com/k3a/html2text"
"github.com/wpcodevo/golang-mongodb/config"
@@ -22,29 +19,7 @@ type EmailData struct {
}
// 👇 Email template parser
-
-func ParseTemplateDir(dir string) (*template.Template, error) {
- var paths []string
- err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return err
- }
- if !info.IsDir() {
- paths = append(paths, path)
- }
- return nil
- })
-
- fmt.Println("Am parsing templates...")
-
- if err != nil {
- return nil, err
- }
-
- return template.ParseFiles(paths...)
-}
-
-func SendEmail(user *models.DBResponse, data *EmailData, templateName string) error {
+func SendEmail(user *models.DBResponse, data *EmailData, temp *template.Template, templateName string) error {
config, err := config.LoadConfig(".")
if err != nil {
@@ -61,15 +36,10 @@ func SendEmail(user *models.DBResponse, data *EmailData, templateName string) er
var body bytes.Buffer
- template, err := ParseTemplateDir("templates")
- if err != nil {
- log.Fatal("Could not parse template", err)
+ if err := temp.ExecuteTemplate(&body, templateName, &data); err != nil {
+ log.Fatal("Could not execute template", err)
}
- template = template.Lookup(templateName)
- template.Execute(&body, &data)
- fmt.Println(template.Name())
-
m := gomail.NewMessage()
m.SetHeader("From", from)