Funktion zum Zurücksetzen des Passworts: Senden von E-Mails in Golang

Mary-Kate Olsen
Freigeben: 2024-10-01 06:12:30
Original
1131 Leute haben es durchsucht

Password Reset Feature: Sending Email in Golang

Während ich diesen Beitrag schreibe, implementiere ich eine Funktion zum Zurücksetzen des Passworts für den Benutzer in meiner App Task-inator 3000. Ich protokolliere nur meinen Denkprozess und die unternommenen Schritte


Planung

Ich denke an einen Ablauf wie diesen:

  1. Benutzer klickt auf „Passwort vergessen?“ Schaltfläche
  2. Zeigen Sie dem Benutzer ein Modal an, das nach einer E-Mail fragt
  3. Überprüfen Sie, ob eine E-Mail vorhanden ist, und senden Sie ein 10 Zeichen langes OTP an die E-Mail
  4. Modal fragt jetzt nach OTP und neuem Passwort
  5. Das Passwort wird für den Benutzer gehasht und aktualisiert

Trennung von Belangen

Frontend

  • Erstellen Sie ein Modal zur E-Mail-Eingabe
  • Das gleiche Modal übernimmt dann OTP und neues Passwort

Backend

  • API zum Senden von E-Mails erstellen
  • API zum Zurücksetzen des Passworts erstellen

Ich beginne mit dem Backend

Backend

Wie oben erwähnt, benötigen wir zwei APIs

1. E-Mail senden

Die API muss nur die E-Mail des Benutzers aufnehmen und bei Erfolg keinen Inhalt zurückgeben. Erstellen Sie daher den Controller wie folgt:

// 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)
}
Nach dem Login kopieren

Fügen Sie jetzt eine Route dafür hinzu:

// routes/routes.go

// password reset
api.Post("/send-otp", controllers.SendPasswordResetEmail)
Nach dem Login kopieren

Ich verwende net/smtp aus der Standardbibliothek von Golang.

Nachdem ich die Dokumentation gelesen habe, denke ich, dass es am besten wäre, bei der Initialisierung des Projekts einen SMTPClient zu erstellen. Daher würde ich eine Datei smtpConnection.go im Verzeichnis /config erstellen.

Davor füge ich die folgenden Umgebungsvariablen entweder zu meiner .env-Datei oder zum Produktionsserver hinzu.

SMTP_HOST="smtp.zoho.in"
SMTP_PORT="587"
SMTP_EMAIL="<myemail>"
SMTP_PASSWORD="<mypassword>"
Nach dem Login kopieren

Ich verwende Zohomail, daher deren SMTP-Host und Port (für TLS), wie hier angegeben.

// 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")
}
Nach dem Login kopieren

Zur Abstraktion erstelle ich eine Datei „passwordReset.go“ in /utils. Diese Datei hätte vorerst folgende Funktionen:

  • OTP generieren: Zum Generieren eines eindeutigen alphanumerischen 10-stelligen OTP zum Senden in der E-Mail
  • AddOTPtoRedis: Zum Hinzufügen von OTP zu Redis in einem Schlüsselwertformat
key -> password-reset:<email>
value -> hashed otp
expiry -> 10 mins
Nach dem Login kopieren

Aus Sicherheitsgründen speichere ich den Hash des OTP anstelle des OTP selbst

  • SendOTP: Um das generierte OTP an die E-Mail-Adresse des Benutzers zu senden

Beim Schreiben von Code sehe ich, dass wir hier 5 Konstanten benötigen:

  • Präfix für Redis-Schlüssel für OTP
  • Ablaufzeit für OTP
  • Zeichensatz für die OTP-Generierung
  • Vorlage für die E-Mail
  • Länge des OTP

Ich füge sie sofort zu /utils/constants.go hinzu

// 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
)
Nach dem Login kopieren

(Beachten Sie, dass wir ausKrypto/Rand importieren und nicht ausMathe/Rand, da dies echte Zufälligkeit liefert)

// 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
}
Nach dem Login kopieren

Die Funktion GenerateOTP() ist ohne Mocks testbar (Unit-Testing), daher wurde ein einfacher Test dafür geschrieben

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)
    }
}
Nach dem Login kopieren

Jetzt müssen wir alles im Controller zusammenfügen. Zuvor müssen wir sicherstellen, dass die angegebene E-Mail-Adresse in der Datenbank vorhanden ist.

Der vollständige Code für den Controller lautet wie folgt:

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)
}
Nach dem Login kopieren

Wir können die API testen, indem wir eine POST-Anfrage an die richtige URL senden. Ein cURL-Beispiel wäre:

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

Wir werden die nächste API – zum Zurücksetzen des Passworts – im nächsten Teil der Serie erstellen

Das obige ist der detaillierte Inhalt vonFunktion zum Zurücksetzen des Passworts: Senden von E-Mails in Golang. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Neueste Artikel des Autors
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage