Maison >développement back-end >Golang >Fonction de réinitialisation du mot de passe : envoi d'e-mails dans Golang

Fonction de réinitialisation du mot de passe : envoi d'e-mails dans Golang

Mary-Kate Olsen
Mary-Kate Olsenoriginal
2024-10-01 06:12:301189parcourir

Password Reset Feature: Sending Email in Golang

J'implémente une fonctionnalité permettant de réinitialiser le mot de passe de l'utilisateur dans mon application Task-inator 3000 au moment où j'écris cet article. Je consigne simplement mon processus de réflexion et les mesures prises


Planification

Je pense à un flux comme celui-ci :

  1. L'utilisateur clique sur « Mot de passe oublié ? » bouton
  2. Afficher un modal à l'utilisateur demandant un e-mail
  3. Vérifiez si l'e-mail existe et envoyez un OTP de 10 caractères à l'e-mail
  4. Modal demande maintenant OTP et un nouveau mot de passe
  5. Le mot de passe est haché et mis à jour pour l'utilisateur

Séparation des préoccupations

Frontend

  • Créez un modal pour saisir l'e-mail
  • Le même modal prend ensuite OTP et le nouveau mot de passe

Backend

  • Créer une API pour l'envoi d'e-mails
  • Créer une API pour réinitialiser le mot de passe

Je vais commencer par le backend

Back-end

Comme indiqué ci-dessus, nous avons besoin de deux API

1. Envoi d'un e-mail

L'API doit récupérer uniquement l'e-mail de l'utilisateur et ne renvoyer aucun contenu en cas de succès. Par conséquent, créez le contrôleur comme suit :

// controllers/passwordReset.go
func SendPasswordResetEmail(c *fiber.Ctx) error {
    type Input struct {
        Email string `json:"email"`
    }

    var input Input

    err := c.BodyParser(&input)
    if err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "invalid data",
        })
    }

    // TODO: send email with otp to user

    return c.SendStatus(fiber.StatusNoContent)
}

Ajoutez maintenant un itinéraire pour cela :

// routes/routes.go

// password reset
api.Post("/send-otp", controllers.SendPasswordResetEmail)

J'utiliserai net/smtp de la bibliothèque standard de Golang.

A la lecture de la documentation, je pense qu'il serait préférable de créer un SMTPClient dès l'initialisation du projet. Par conséquent, je créerais un fichier smtpConnection.go dans le répertoire /config.

Avant cela, j'ajouterai les variables d'environnement suivantes soit à mon .env, soit au serveur de production.

SMTP_HOST="smtp.zoho.in"
SMTP_PORT="587"
SMTP_EMAIL="<myemail>"
SMTP_PASSWORD="<mypassword>"

J'utilise Zohomail, d'où leur hôte et port smtp (pour TLS) comme indiqué ici.

// config/smtpConnection.go
package config

import (
    "crypto/tls"
    "fmt"
    "net/smtp"
    "os"
)

var SMTPClient *smtp.Client

func SMTPConnect() {
    host := os.Getenv("SMTP_HOST")
    port := os.Getenv("SMTP_PORT")
    email := os.Getenv("SMTP_EMAIL")
    password := os.Getenv("SMTP_PASSWORD")

    smtpAuth := smtp.PlainAuth("", email, password, host)

    // connect to smtp server
    client, err := smtp.Dial(host + ":" + port)
    if err != nil {
        panic(err)
    }

    SMTPClient = client
    client = nil

    // initiate TLS handshake
    if ok, _ := SMTPClient.Extension("STARTTLS"); ok {
        config := &tls.Config{ServerName: host}
        if err = SMTPClient.StartTLS(config); err != nil {
            panic(err)
        }
    }

    // authenticate
    err = SMTPClient.Auth(smtpAuth)
    if err != nil {
        panic(err)
    }

    fmt.Println("SMTP Connected")
}

Pour l'abstraction, je vais créer un fichier passwordReset.go dans /utils. Ce fichier aurait pour l'instant les fonctions suivantes :

  • Générer OTP : pour générer un OTP alphanumérique unique à 10 chiffres à envoyer dans l'e-mail
  • AddOTPtoRedis : pour ajouter OTP à Redis dans un format de valeur clé où
key -> password-reset:<email>
value -> hashed otp
expiry -> 10 mins

Je stocke le hachage de l'OTP au lieu de l'OTP lui-même pour des raisons de sécurité

  • SendOTP : pour envoyer l'OTP généré à l'adresse e-mail de l'utilisateur

En écrivant du code, je vois que nous avons besoin de 5 constantes ici :

  • Préfixe de la clé Redis pour OTP
  • Délai d'expiration pour OTP
  • Jeu de caractères pour la génération OTP
  • Modèle pour l'e-mail
  • Durée de l'OTP

Je les ajouterai immédiatement à /utils/constants.go

// utils/constants.go
package utils

import "time"

const (
    authTokenExp       = time.Minute * 10
    refreshTokenExp    = time.Hour * 24 * 30 // 1 month
    blacklistKeyPrefix = "blacklisted:"
    otpKeyPrefix       = "password-reset:"
    otpExp             = time.Minute * 10
    otpCharSet         = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
    emailTemplate      = "To: %s\r\n" +
        "Subject: Task-inator 3000 Password Reset\r\n" +
        "\r\n" +
        "Your OTP for password reset is %s\r\n"

    // public because needed for testing
    OTPLength = 10
)

(Notez que nous importerons depuiscrypto/rand, et nonmath/rand, car cela fournira un véritable caractère aléatoire)

// utils/passwordReset.go
package utils

import (
    "context"
    "crypto/rand"
    "fmt"
    "math/big"
    "os"
    "task-inator3000/config"

    "golang.org/x/crypto/bcrypt"
)

func GenerateOTP() string {
    result := make([]byte, OTPLength)
    charsetLength := big.NewInt(int64(len(otpCharSet)))

    for i := range result {
        // generate a secure random number in the range of the charset length
        num, _ := rand.Int(rand.Reader, charsetLength)
        result[i] = otpCharSet[num.Int64()]
    }

    return string(result)
}

func AddOTPtoRedis(otp string, email string, c context.Context) error {
    key := otpKeyPrefix + email

    // hashing the OTP
    data, _ := bcrypt.GenerateFromPassword([]byte(otp), 10)

    // storing otp with expiry
    err := config.RedisClient.Set(c, key, data, otpExp).Err()
    if err != nil {
        return err
    }

    return nil
}

func SendOTP(otp string, recipient string) error {
    sender := os.Getenv("SMTP_EMAIL")
    client := config.SMTPClient

    // setting the sender
    err := client.Mail(sender)
    if err != nil {
        return err
    }

    // set recipient
    err = client.Rcpt(recipient)
    if err != nil {
        return err
    }

    // start writing email
    writeCloser, err := client.Data()
    if err != nil {
        return err
    }

    // contents of the email
    msg := fmt.Sprintf(emailTemplate, recipient, otp)

    // write the email
    _, err = writeCloser.Write([]byte(msg))
    if err != nil {
        return err
    }

    // close writecloser and send email
    err = writeCloser.Close()
    if err != nil {
        return err
    }

    return nil
}

La fonction GenerateOTP() est testable sans simulations (tests unitaires), c'est pourquoi elle a écrit un test simple

package utils_test

import (
    "task-inator3000/utils"
    "testing"
)

func TestGenerateOTP(t *testing.T) {
    result := utils.GenerateOTP()

    if len(result) != utils.OTPLength {
        t.Errorf("Length of OTP was not %v. OTP: %v", utils.OTPLength, result)
    }
}

Maintenant, nous devons tout rassembler dans le contrôleur. Avant tout cela, nous devons nous assurer que l'adresse e-mail fournie existe dans la base de données.

Le code complet du contrôleur est le suivant :

func SendPasswordResetEmail(c *fiber.Ctx) error {
    type Input struct {
        Email string `json:"email"`
    }

    var input Input

    err := c.BodyParser(&input)
    if err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "invalid data",
        })
    }

    // check if user with email exists
    users := config.DB.Collection("users")
    filter := bson.M{"_id": input.Email}
    err = users.FindOne(c.Context(), filter).Err()
    if err != nil {
        if err == mongo.ErrNoDocuments {
            return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
                "error": "user with given email not found",
            })
        }

        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": "error while finding in the database:\n" + err.Error(),
        })
    }

    // generate otp and add it to redis
    otp := utils.GenerateOTP()
    err = utils.AddOTPtoRedis(otp, input.Email, c.Context())
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": err.Error(),
        })
    }

    // send the otp to user through email
    err = utils.SendOTP(otp, input.Email)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": err.Error(),
        })
    }

    return c.SendStatus(fiber.StatusNoContent)
}

Nous pouvons tester l'API en envoyant une requête POST à ​​la bonne URL. Un exemple de cURL serait :

curl --location 'localhost:3000/api/send-otp' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "yashjaiswal.cse@gmail.com"
}'

Nous créerons la prochaine API - pour réinitialiser le mot de passe - dans la prochaine partie de la série

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn