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
Libérer: 2024-10-01 06:12:30
original
1188 Les gens l'ont consulté

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)
}
Copier après la connexion

Ajoutez maintenant un itinéraire pour cela :

// routes/routes.go

// password reset
api.Post("/send-otp", controllers.SendPasswordResetEmail)
Copier après la connexion

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>"
Copier après la connexion

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")
}
Copier après la connexion

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
Copier après la connexion

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
)
Copier après la connexion

(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
}
Copier après la connexion

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)
    }
}
Copier après la connexion

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)
}
Copier après la connexion

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"
}'
Copier après la connexion

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!

source:dev.to
Déclaration de ce site Web
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
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal