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. + +![API with Golang, Gin Gonic & MongoDB: Forget/Reset Password](https://codevoweb.com/wp-content/uploads/2022/05/API-with-Golang-Gin-Gonic-MongoDB-Forget-Reset-Password.webp) + +## 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"}} - - - - - + + + + + + {{template "styles" .}} + {{ .Subject}} + + + + + + + + + +   + + + + 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)