Movatterモバイル変換


[0]ホーム

URL:


CodevoWeb

PressESC to close

API with Golang, Gin Gonic & MongoDB: Forget/Reset Password

1Comment76

In this article, you’ll learn how to implementforget/reset password functionality with Golang, Gin Gonic, Gomail, MongoDB-Go-driver, Redis, and Docker-compose.

CRUD RESTful API with Golang + MongoDB Series:

  1. API with Golang + MongoDB + Redis + Gin Gonic: Project Setup
  2. Golang & MongoDB: JWT Authentication and Authorization
  3. API with Golang + MongoDB: Send HTML Emails with Gomail
  4. API with Golang, Gin Gonic & MongoDB: Forget/Reset Password
  5. Build Golang gRPC Server and Client: SignUp User & Verify Email
  6. Build Golang gRPC Server and Client: Access & Refresh Tokens
  7. Build CRUD RESTful API Server with Golang, Gin, and MongoDB

Related Article:

https://github.com/wpcodevo/golang-mongodb-api/tree/golang-mongodb-reset-password

Forget/Reset Password with Golang, Gin, and MongoDB

HTTP METHODROUTEDESCRIPTION
POST/api/auth/forgotpasswordTo request a reset token
PATCH/api/auth/resetpassword/:resetTokenTo reset the password

To request a password reset token, the user will make aPOSTrequest with his email address to the Golang API server.

node.js, prisma, postgresql forgot password page

The Golang server then validates the email, generates the password reset token, and sends the reset token to the user’s email.

Also, the server returns a message to the frontend application indicating that the password reset email has been successfully sent.

node.js, prisma, postgresql forgot password success message

The user then opens the email and clicks on the“Reset Password” button.

node.js, prisma, postgresql reset password email

Upon clicking the“Reset Password” button, the user is taken to the password reset page where he is required to enter his new password and confirm the password before making aPATCH request to the server.

node.js, prisma, postgresql reset password page

The Golang server then validates the password reset token and updates the user’s password in the database before returning a success message to the frontend application.

The frontend application receives the success message and redirects the user to the login page.

node.js, prisma, postgresql reset password success message

Update the MongoDB Model Structs

Add the following structs to themodels/user.model.go file. We’ll use these structs to validate the request body when implementing theforget/reset password functionality.

models/user.model.go

// ? ForgotPasswordInput structtype ForgotPasswordInput struct {Email string `json:"email" binding:"required"`}// ? ResetPasswordInput structtype ResetPasswordInput struct {Password        string `json:"password" binding:"required"`PasswordConfirm string `json:"passwordConfirm" binding:"required"`}

Themodels/user.model.go file should now look like this:

models/user.model.go

// ? SignUpInput structtype 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"`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 structtype SignInInput struct {Email    string `json:"email" bson:"email" binding:"required"`Password string `json:"password" bson:"password" binding:"required"`}// ? DBResponse structtype 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"`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 structtype UserResponse struct {ID        primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"`Name      string             `json:"name,omitempty" bson:"name,omitempty"`Email     string             `json:"email,omitempty" bson:"email,omitempty"`Role      string             `json:"role,omitempty" bson:"role,omitempty"`CreatedAt time.Time          `json:"created_at" bson:"created_at"`UpdatedAt time.Time          `json:"updated_at" bson:"updated_at"`}// ? ForgotPasswordInput structtype ForgotPasswordInput struct {Email string `json:"email" binding:"required"`}// ? ResetPasswordInput structtype ResetPasswordInput struct {Password        string `json:"password" binding:"required"`PasswordConfirm string `json:"passwordConfirm" binding:"required"`}func FilteredResponse(user *DBResponse) UserResponse {return UserResponse{ID:        user.ID,Email:     user.Email,Name:      user.Name,Role:      user.Role,CreatedAt: user.CreatedAt,UpdatedAt: user.UpdatedAt,}}

Create the HTML Email Templates with Golang

To send the HTML Emails, we’ll use the officialhtml/template package that comes with Golang to generate the HTML Email templates.

There are other template engines like jigopongo2, or mustache, that we could have used but I decided to use the standardhtml/template package instead.

These Email templates are based on afree HTML email template I cloned from GitHub.

Now create a templates folder in the root directory of your project and create astyles.html file.

templates/styles.html

{{define "styles"}}<style>  /* -------------------------------------          GLOBAL RESETS      ------------------------------------- */  /*All the styling goes here*/  img {    border: none;    -ms-interpolation-mode: bicubic;    max-width: 100%;  }  body {    background-color: #f6f6f6;    font-family: sans-serif;    -webkit-font-smoothing: antialiased;    font-size: 14px;    line-height: 1.4;    margin: 0;    padding: 0;    -ms-text-size-adjust: 100%;    -webkit-text-size-adjust: 100%;  }  table {    border-collapse: separate;    mso-table-lspace: 0pt;    mso-table-rspace: 0pt;    width: 100%;  }  table td {    font-family: sans-serif;    font-size: 14px;    vertical-align: top;  }  /* -------------------------------------          BODY & CONTAINER      ------------------------------------- */  .body {    background-color: #f6f6f6;    width: 100%;  }  /* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */  .container {    display: block;    margin: 0 auto !important;    /* makes it centered */    max-width: 580px;    padding: 10px;    width: 580px;  }  /* This should also be a block element, so that it will fill 100% of the .container */  .content {    box-sizing: border-box;    display: block;    margin: 0 auto;    max-width: 580px;    padding: 10px;  }  /* -------------------------------------          HEADER, FOOTER, MAIN      ------------------------------------- */  .main {    background: #ffffff;    border-radius: 3px;    width: 100%;  }  .wrapper {    box-sizing: border-box;    padding: 20px;  }  .content-block {    padding-bottom: 10px;    padding-top: 10px;  }  .footer {    clear: both;    margin-top: 10px;    text-align: center;    width: 100%;  }  .footer td,  .footer p,  .footer span,  .footer a {    color: #999999;    font-size: 12px;    text-align: center;  }  /* -------------------------------------          TYPOGRAPHY      ------------------------------------- */  h1,  h2,  h3,  h4 {    color: #000000;    font-family: sans-serif;    font-weight: 400;    line-height: 1.4;    margin: 0;    margin-bottom: 30px;  }  h1 {    font-size: 35px;    font-weight: 300;    text-align: center;    text-transform: capitalize;  }  p,  ul,  ol {    font-family: sans-serif;    font-size: 14px;    font-weight: normal;    margin: 0;    margin-bottom: 15px;  }  p li,  ul li,  ol li {    list-style-position: inside;    margin-left: 5px;  }  a {    color: #3498db;    text-decoration: underline;  }  /* -------------------------------------          BUTTONS      ------------------------------------- */  .btn {    box-sizing: border-box;    width: 100%;  }  .btn > tbody > tr > td {    padding-bottom: 15px;  }  .btn table {    width: auto;  }  .btn table td {    background-color: #ffffff;    border-radius: 5px;    text-align: center;  }  .btn a {    background-color: #ffffff;    border: solid 1px #3498db;    border-radius: 5px;    box-sizing: border-box;    color: #3498db;    cursor: pointer;    display: inline-block;    font-size: 14px;    font-weight: bold;    margin: 0;    padding: 12px 25px;    text-decoration: none;    text-transform: capitalize;  }  .btn-primary table td {    background-color: #3498db;  }  .btn-primary a {    background-color: #3498db;    border-color: #3498db;    color: #ffffff;  }  /* -------------------------------------          OTHER STYLES THAT MIGHT BE USEFUL      ------------------------------------- */  .last {    margin-bottom: 0;  }  .first {    margin-top: 0;  }  .align-center {    text-align: center;  }  .align-right {    text-align: right;  }  .align-left {    text-align: left;  }  .clear {    clear: both;  }  .mt0 {    margin-top: 0;  }  .mb0 {    margin-bottom: 0;  }  .preheader {    color: transparent;    display: none;    height: 0;    max-height: 0;    max-width: 0;    opacity: 0;    overflow: hidden;    mso-hide: all;    visibility: hidden;    width: 0;  }  .powered-by a {    text-decoration: none;  }  hr {    border: 0;    border-bottom: 1px solid #f6f6f6;    margin: 20px 0;  }  /* -------------------------------------          RESPONSIVE AND MOBILE FRIENDLY STYLES      ------------------------------------- */  @media only screen and (max-width: 620px) {    table.body h1 {      font-size: 28px !important;      margin-bottom: 10px !important;    }    table.body p,    table.body ul,    table.body ol,    table.body td,    table.body span,    table.body a {      font-size: 16px !important;    }    table.body .wrapper,    table.body .article {      padding: 10px !important;    }    table.body .content {      padding: 0 !important;    }    table.body .container {      padding: 0 !important;      width: 100% !important;    }    table.body .main {      border-left-width: 0 !important;      border-radius: 0 !important;      border-right-width: 0 !important;    }    table.body .btn table {      width: 100% !important;    }    table.body .btn a {      width: 100% !important;    }    table.body .img-responsive {      height: auto !important;      max-width: 100% !important;      width: auto !important;    }  }  /* -------------------------------------          PRESERVE THESE STYLES IN THE HEAD      ------------------------------------- */  @media all {    .ExternalClass {      width: 100%;    }    .ExternalClass,    .ExternalClass p,    .ExternalClass span,    .ExternalClass font,    .ExternalClass td,    .ExternalClass div {      line-height: 100%;    }    .apple-link a {      color: inherit !important;      font-family: inherit !important;      font-size: inherit !important;      font-weight: inherit !important;      line-height: inherit !important;      text-decoration: none !important;    }    #MessageViewBody a {      color: inherit;      text-decoration: none;      font-size: inherit;      font-family: inherit;      font-weight: inherit;      line-height: inherit;    }    .btn-primary table td:hover {      background-color: #34495e !important;    }    .btn-primary a:hover {      background-color: #34495e !important;      border-color: #34495e !important;    }  }</style>{{end}}

Next, create abase.html file that will be extended by other HTML templates to generate different kinds of HTML templates like the Welcome Email template, Verification Email template, Password Reset Email template, and more.

To include thestyles.html file in thebase.html file, I used the Golangtemplate action which is similar to theJinja2 include tag.

Lastly, I created a block named content which will be overridden by a child template.

templates/base.html

{{define "base"}}<!DOCTYPE html><html>  <head>    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />    {{template "styles" .}}    <title>{{ .Subject}}</title>  </head>  <body>    <table      role="presentation"      border="0"      cellpadding="0"      cellspacing="0"      class="body"    >      <tr>        <td>&nbsp;</td>        <td class="container">          <div class="content">            <!-- START CENTERED WHITE CONTAINER -->            {{block "content" .}}{{end}}            <!-- END CENTERED WHITE CONTAINER -->          </div>        </td>        <td>&nbsp;</td>      </tr>    </table>  </body></html>{{end}}

Now create aresetPassword.html child template to extend thebase.html template and override thecontent block in the base template.

templates/resetPassword.html

{{template "base" .}} {{define "content"}}<table role="presentation" class="main">  <!-- START MAIN CONTENT AREA -->  <tr>    <td class="wrapper">      <table role="presentation" border="0" cellpadding="0" cellspacing="0">        <tr>          <td>            <p>Hi {{ .FirstName}},</p>            <p>              Forgot password? Send a PATCH request to with your password and              passwordConfirm to {{.URL}}            </p>            <table              role="presentation"              border="0"              cellpadding="0"              cellspacing="0"              class="btn btn-primary"            >              <tbody>                <tr>                  <td align="left">                    <table                      role="presentation"                      border="0"                      cellpadding="0"                      cellspacing="0"                    >                      <tbody>                        <tr>                          <td>                            <a href="{{.URL}}" target="_blank"                              >Reset password</a                            >                          </td>                        </tr>                      </tbody>                    </table>                  </td>                </tr>              </tbody>            </table>            <p>If you didn't forget your password, please ignore this email</p>            <p>Good luck! Codevo CEO.</p>          </td>        </tr>      </table>    </td>  </tr>  <!-- END MAIN CONTENT AREA --></table>{{end}}

Define a Utility Function to Parse the HTML Templates

Next, create aParseTemplateDir() function to load and parse all the HTML files in the templates folder.

TheParseTemplateDir() function accepts the path to the HTML files and returns the parsed HTML templates.

utils/email.go

// ? Email template parserfunc ParseTemplateDir(dir string) (*template.Template, error) {var paths []stringerr := 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...)}

Create a Function to Send the HTML Emails

Run the following command to install theGomail package that will be used to send the SMTP emails.

go get gopkg.in/gomail.v2

Also, install theHTML2Text package to convert the HTML template to text.

go get github.com/k3a/html2text

Now let’s create a function to dynamically access and populate a specific parsed template before sending the HTML Email to the user.

utils/email.go

type EmailData struct {URL       stringFirstName stringSubject   string}// ? Email template parserfunc SendEmail(user *models.DBResponse, data *EmailData, templateName string) error {config, err := config.LoadConfig(".")if err != nil {log.Fatal("could not load config", err)}// Sender data.from := config.EmailFromsmtpPass := config.SMTPPasssmtpUser := config.SMTPUserto := user.EmailsmtpHost := config.SMTPHostsmtpPort := config.SMTPPortvar body bytes.Buffertemplate, err := ParseTemplateDir("templates")if err != nil {log.Fatal("Could not parse template", err)}template = template.Lookup(templateName)template.Execute(&body, &data)fmt.Println(template.Name())m := gomail.NewMessage()m.SetHeader("From", from)m.SetHeader("To", to)m.SetHeader("Subject", data.Subject)m.SetBody("text/html", body.String())m.AddAlternative("text/plain", html2text.HTML2Text(body.String()))d := gomail.NewDialer(smtpHost, smtpPort, smtpUser, smtpPass)d.TLSConfig = &tls.Config{InsecureSkipVerify: true}// Send Emailif err := d.DialAndSend(m); err != nil {return err}return nil}

Add the Forgot Password Controller

Create aForgotPassword() controller in thecontrollers folder.

controllers/auth.controller.go

func (ac *AuthController) ForgotPassword(ctx *gin.Context) {var userCredential *models.ForgotPasswordInputif err := ctx.ShouldBindJSON(&userCredential); err != nil {ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})return}message := "You will receive a reset email if user with that email exist"user, err := ac.userService.FindUserByEmail(userCredential.Email)if err != nil {if err == mongo.ErrNoDocuments {ctx.JSON(http.StatusOK, gin.H{"status": "fail", "message": message})return}ctx.JSON(http.StatusBadGateway, gin.H{"status": "error", "message": err.Error()})return}if !user.Verified {ctx.JSON(http.StatusUnauthorized, gin.H{"status": "error", "message": "Account not verified"})return}config, err := config.LoadConfig(".")if err != nil {log.Fatal("Could not load config", err)}// Generate Verification CoderesetToken := randstr.String(20)passwordResetToken := utils.Encode(resetToken)// Update User in Databasequery := bson.D{{Key: "email", Value: strings.ToLower(userCredential.Email)}}update := bson.D{{Key: "$set", Value: bson.D{{Key: "passwordResetToken", Value: passwordResetToken}, {Key: "passwordResetAt", Value: time.Now().Add(time.Minute * 15)}}}}result, err := ac.collection.UpdateOne(ac.ctx, query, update)if result.MatchedCount == 0 {ctx.JSON(http.StatusBadGateway, gin.H{"status": "success", "message": "There was an error sending email"})return}if err != nil {ctx.JSON(http.StatusForbidden, gin.H{"status": "success", "message": err.Error()})return}var firstName = user.Nameif strings.Contains(firstName, " ") {firstName = strings.Split(firstName, " ")[1]}// ? Send EmailemailData := utils.EmailData{URL:       config.Origin + "/resetpassword/" + resetToken,FirstName: firstName,Subject:   "Your password reset token (valid for 10min)",}err = utils.SendEmail(user, &emailData, "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})}

Here is a breakdown of what I did in the above snippets:

  • First, I passed theForgotPasswordInput struct we defined in theuser.model.go to theShouldBindJSON() function to help Gin return a validation error if the email was not provided in the request body.
  • Then I made a query to the MongoDB database to check if a user with that email exists before checking if the user is verified.
  • Next, I generated the password reset token with theEncode() and hashed it.
    ReadAPI with Golang + MongoDB: Send HTML Emails with Gomail to know more about theEncode() function.
  • Finally, I sent the unhashed password reset token to the user’s email and stored the hashed one in the MongoDB database.

Add the Reset Password Controller

Now let’s create theResetPassword controller to validate the reset token and update the user’s password in the MongoDB database.

controllers/auth.controller.go

func (ac *AuthController) ResetPassword(ctx *gin.Context) {        resetToken := ctx.Params.ByName("resetToken")var userCredential *models.ResetPasswordInputif 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 Databasequery := 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"})}

Here is a summary of what I did in the above code snippets:

  • First, I retrieved the password reset token from the requestparameters and hashed it.
  • Next, I passed theResetPasswordInput struct toShouldBindJSON() method to check if the user provided the required fields in the request body.
  • Then I validated the new password against the password confirm and returned an error if they are not equal.
  • Next, I hashed the new password with theHashPassword() function before calling theUpdateOne() MongoDB function to update the user’s password in the database.
    ReadGolang & MongoDB: JWT Authentication and Authorization to know about theHashPassword() function.
  • Also, I used the$unset operator to delete thepasswordResetToken andpasswordResetAt fields from the database.
  • Finally, I sent expired cookies to the user’s browser.

Update the Routes

Next, add the following routes to theAuthRoute() method.

To request a password reset token, the user will make aPOST request to/api/auth/forgotpassword with his email.

After receiving the token, the user will then make aPATCH request with the new password and password confirm to/api/auth/resetpassword/:resetToken .

routes/auth.routes.go

router.POST("/forgotpassword", rc.authController.ForgotPassword)router.PATCH("/resetpassword/:resetToken", rc.authController.ResetPassword)

TheAuthRoute() function should now look like this:

routes/auth.routes.go

type AuthRouteController struct {authController controllers.AuthController}func NewAuthRouteController(authController controllers.AuthController) AuthRouteController {return AuthRouteController{authController}}func (rc *AuthRouteController) AuthRoute(rg *gin.RouterGroup, userService services.UserService) {router := rg.Group("/auth")router.POST("/register", rc.authController.SignUpUser)router.POST("/login", rc.authController.SignInUser)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.PATCH("/resetpassword/:resetToken", rc.authController.ResetPassword)}

Conclusion

Congrats on reaching the end. In this article, you learned how to implement forget/reset password functionality with Golang, Gin Gonic, MongoDB, Redis, and Docker-compose.

Check out the source codes:

Share Article:

API with Node.js, Prisma & PostgreSQL: Forget/Reset Password

Left Arrow

Build Golang gRPC Server and Client: SignUp User & Verify Email

Right Arrow

One Comment

  1. kukuhon March 3, 2025

    Could not load environment variablesConfig File “app” Not Found
    this my error message in terminal vscode in API with Golang, Gin Gonic & MongoDB: Forget/Reset Password articles can you help me for fix this error? thank you

    Reply

Leave a ReplyCancel reply

This site is protected by reCAPTCHA and the GooglePrivacy Policy andTerms of Service apply.

This site uses Akismet to reduce spam.Learn how your comment data is processed.

Support Me!

paypal donate button

Recent posts

Categories


[8]ページ先頭

©2009-2025 Movatter.jp