diff --git a/.env.local b/.env.local index 21c9a09..1740021 100644 --- a/.env.local +++ b/.env.local @@ -1,3 +1,4 @@ MONGO_URI="YOUR_MONGODB_CONNECTION_KEY" JWT_SECRET="YOUR_JWT_SECRET_KEY" - +SENDER_EMAIL="SENDER_EMAIL" +GMAIL_APP_PASSWORD="YOUR_GMAIL_APP_PASSWORD" diff --git a/go.mod b/go.mod index 38de511..9d9a0f0 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/joho/godotenv v1.5.1 go.mongodb.org/mongo-driver v1.17.6 golang.org/x/crypto v0.40.0 + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df ) require ( @@ -48,4 +49,5 @@ require ( golang.org/x/text v0.27.0 // indirect golang.org/x/tools v0.34.0 // indirect google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect ) diff --git a/go.sum b/go.sum index 3534f08..d6b72c5 100644 --- a/go.sum +++ b/go.sum @@ -126,7 +126,11 @@ golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers/auth.go b/handlers/auth.go index ec90b04..66ea25d 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -5,7 +5,8 @@ import ( "net/http" "strings" "time" - + "fmt" + "goth/helpers" "goth/middlewares" "goth/models" @@ -14,6 +15,7 @@ import ( "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" "golang.org/x/crypto/bcrypt" ) @@ -41,6 +43,34 @@ func (h *AuthHandler) Signup (c *gin.Context) { user.ID = primitive.NewObjectID() user.CreatedAt = time.Now() user.Role = "user" + user.IsVerified = false + + // generating a token to send in email + tokenString, err := helpers.GenerateToken(user.ID.Hex(), user.Email, user.UserName, user.Role) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error generating token"}) + return + } + VerificationURL := fmt.Sprintf(`http://localhost:8080/api/auth/verify?token=%s`, tokenString) + + // email's preq + userEmail := user.Email + emailSubject := "Verifing the account" + emailBody := fmt.Sprintf(` +
Thanks for signing up.
+ + Verify + `, VerificationURL) + + // spawn a background worker to send email + go func(){ + err := helpers.SendEmail(userEmail, emailSubject, emailBody) + if err != nil { + fmt.Printf("Error sending the email to %s\n%s", userEmail, err) + } else { + fmt.Printf("Email sent to %s", userEmail) + } + }() _, err = h.Collection.InsertOne(context.TODO(), user) if err != nil { @@ -59,7 +89,7 @@ func (h *AuthHandler) Signup (c *gin.Context) { } c.JSON(http.StatusCreated, gin.H{ - "success": "Signup success!", + "success": "Check your mail inbox for verifying your email", "user": map[string]interface{} { "userName": user.UserName, "email": user.Email, @@ -140,3 +170,39 @@ func (h *AuthHandler) Logout (c *gin.Context) { c.SetCookie("token", " ", -1, "/", "localhost", false, true) c.JSON(http.StatusOK, gin.H{"success": "Logged out successfully!"}) } + +func (h *AuthHandler) VerifyEmail (c *gin.Context) { + + // get token from query parameters + tokenString := c.Query("token") + + // verify the tokenString + claims, err := helpers.VerifyToken(tokenString) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token res"}) + return + } + + var updatedUser models.User + + filter := bson.M{"email": claims.Email} + update := bson.M{ + "$set": bson.M{ + "isVerified": true, + }, + } + + opts := options.FindOneAndUpdate().SetReturnDocument(options.After) + + err = h.Collection.FindOneAndUpdate(context.TODO(), filter, update, opts).Decode(&updatedUser) + if err != nil { + if err == mongo.ErrNoDocuments { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error iterating"}) + return + } + + c.JSON(http.StatusOK, gin.H{"success": "Account verified successfully!"}) +} diff --git a/helpers/email.go b/helpers/email.go new file mode 100644 index 0000000..77d4d6b --- /dev/null +++ b/helpers/email.go @@ -0,0 +1,28 @@ +package helpers + +import ( + "fmt" + + "gopkg.in/gomail.v2" +) + +func SendEmail (to, subject, body string) error { + + senderEmail := GetEnvVar("SENDER_EMAIL") + appPass := GetEnvVar("GMAIL_APP_PASSWORD") + + // Set up a new message + m := gomail.NewMessage() + m.SetHeader("From", senderEmail) + m.SetHeader("To", to) + m.SetHeader("Subject", subject) + m.SetBody("text/html", body) + + // set up the dialer + d := gomail.NewDialer("smtp.gmail.com", 587, senderEmail, appPass) + + if err := d.DialAndSend(m); err != nil { + return fmt.Errorf("could not send email %s", err) + } + return nil +} diff --git a/helpers/env.go b/helpers/env.go new file mode 100644 index 0000000..7e0808d --- /dev/null +++ b/helpers/env.go @@ -0,0 +1,21 @@ +package helpers + +import ( + "os" + "fmt" + "github.com/joho/godotenv" +) + +func GetEnvVar(env string) string { + if err := godotenv.Load(); err != nil { + fmt.Printf("Error loading the env variables %s", err) + os.Exit(1) + } + + key := os.Getenv(env) + if key == "" { + fmt.Printf("No env variable with key = %s found", key) + os.Exit(1) + } + return key +} diff --git a/models/user.go b/models/user.go index 0cdf9e4..009b6b0 100644 --- a/models/user.go +++ b/models/user.go @@ -6,10 +6,11 @@ import ( ) type User struct { - ID primitive.ObjectID `json:"id" bson:"_id, omitempty"` - UserName string `json:"userName" bson:"userName" binding:"required"` - Email string `json:"email" bson:"email" binding:"required"` - Role string `json:"role" bson:"role"` - Password string `json:"password" bson:"password" binding:"required"` - CreatedAt time.Time `json:"created_at" bson:"created_at"` + ID primitive.ObjectID `json:"id" bson:"_id, omitempty"` + UserName string `json:"userName" bson:"userName" binding:"required"` + Email string `json:"email" bson:"email" binding:"required"` + Role string `json:"role" bson:"role"` + Password string `json:"password" bson:"password" binding:"required"` + IsVerified bool `json:"isVerified" bson:"isVerified"` + CreatedAt time.Time `json:"created_at" bson:"created_at"` } diff --git a/routes/auth.go b/routes/auth.go index 3f81a47..9fc1e8b 100644 --- a/routes/auth.go +++ b/routes/auth.go @@ -11,6 +11,7 @@ func AuthRoute (router *gin.Engine, authHandler *handlers.AuthHandler) { api := router.Group("/api/auth") { api.POST("/signup", authHandler.Signup) + api.GET("/verify", authHandler.VerifyEmail) api.POST("/login", authHandler.Login) api.POST("/logout", middlewares.AuthMiddleware(), authHandler.Logout) api.GET("/users", middlewares.AuthMiddleware(), authHandler.GetUsers)