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
Je pense à un flux comme celui-ci :
Frontend
Backend
Je vais commencer par le backend
Comme indiqué ci-dessus, nous avons besoin de deux API
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 :
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é
En écrivant du code, je vois que nous avons besoin de 5 constantes ici :
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!