diff --git a/api/.env.example b/api/.env.example
index 19830ad..0ba369e 100644
--- a/api/.env.example
+++ b/api/.env.example
@@ -6,4 +6,6 @@ EMAIL_FROM=my@email.com
SMTP_SERVER=smtp.example.com
SMTP_PORT=587
SMTP_PASSWORD=password
-JWT_SECRET=jwtSecret
\ No newline at end of file
+JWT_SECRET=jwtSecret
+VERIFY_URL=http://example.com/verify
+RESET_EMAIL_URL=http://example.com/reset-email
\ No newline at end of file
diff --git a/api/emails/resetPassword.go b/api/emails/resetPassword.go
new file mode 100644
index 0000000..9b9af77
--- /dev/null
+++ b/api/emails/resetPassword.go
@@ -0,0 +1,60 @@
+package emails
+
+import (
+ "fmt"
+ "net/smtp"
+ "os"
+ "strconv"
+)
+
+func SendResetPasswordEmail(to, resetPasswordLink string) error {
+
+ var (
+ smtpServer = os.Getenv("SMTP_SERVER")
+ smtpPort = os.Getenv("SMTP_PORT")
+ username = os.Getenv("EMAIL_FROM")
+ password = os.Getenv("SMTP_PASSWORD")
+ from = os.Getenv("EMAIL_FROM")
+ )
+
+ if smtpServer == "" || smtpPort == "" || username == "" || password == "" || from == "" {
+ return fmt.Errorf("environment variable not set: SMTP_SERVER, SMTP_PORT, EMAIL_FROM, or SMTP_PASSWORD")
+ }
+
+ port, err := strconv.Atoi(smtpPort)
+ if err != nil {
+ return fmt.Errorf("failed to convert smtpPort to int: %v", err)
+ }
+
+ body := fmt.Sprintf(`
+
+
+
+ Hello!
+
+
+ We received a request to reset your password. If you did not make this request, please ignore this email.
+
+
+ Please click the following link to reset your password:
+
+ %s
+
+
+
+
+
+ Best regards,
+
+ The Team
+
+
+
+ `, resetPasswordLink, resetPasswordLink)
+ msg := []byte(fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: Welcome\r\nContent-Type: text/html\r\n\r\n%s", from, to, body))
+ auth := smtp.PlainAuth("", username, password, smtpServer)
+ if err := smtp.SendMail(fmt.Sprintf("%s:%d", smtpServer, port), auth, from, []string{to}, msg); err != nil {
+ return fmt.Errorf("failed to send email: %v", err)
+ }
+ return nil
+}
diff --git a/api/email/welcome.go b/api/emails/welcome.go
similarity index 99%
rename from api/email/welcome.go
rename to api/emails/welcome.go
index 97b9e34..9f7e0da 100644
--- a/api/email/welcome.go
+++ b/api/emails/welcome.go
@@ -1,4 +1,4 @@
-package email
+package emails
import (
"fmt"
diff --git a/api/handlers/checkUsername.go b/api/handlers/checkUsername.go
index c3c4eb7..c9ed50c 100644
--- a/api/handlers/checkUsername.go
+++ b/api/handlers/checkUsername.go
@@ -28,8 +28,8 @@ func CheckUsernameGet(c echo.Context) error {
"username": username,
}).Decode(&existingUser)
if err == nil {
- return c.JSON(http.StatusOK, echo.Map{"message": "Username not available"})
+ return c.JSON(http.StatusOK, echo.Map{"message": "Username not available", "available": false})
}
- return c.JSON(http.StatusOK, echo.Map{"message": "Username available"})
+ return c.JSON(http.StatusOK, echo.Map{"message": "Username available", "available": true})
}
diff --git a/api/handlers/checkUsername.http b/api/handlers/checkUsername.http
new file mode 100644
index 0000000..c76fe63
--- /dev/null
+++ b/api/handlers/checkUsername.http
@@ -0,0 +1,4 @@
+ # Your request headers, e.g.
+ GET http://localhost:8080/check-username?username=bkawk
+ Content-Type: application/json
+
diff --git a/api/handlers/forgotPassword.go b/api/handlers/forgotPassword.go
index fc4d7ac..00e781c 100644
--- a/api/handlers/forgotPassword.go
+++ b/api/handlers/forgotPassword.go
@@ -1,34 +1,54 @@
package handlers
import (
- "net/http"
-
"bkawk/go-echo/api/models"
+ "context"
+ "fmt"
+ "net/http"
+ "time"
"github.com/labstack/echo/v4"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
)
// RegisterEndpoint handles user registration requests
func ForgotPasswordPost(c echo.Context) error {
- // bind the incoming request body to a User struct
+
+ // Get database connection from context
+ db := c.Get("db").(*mongo.Database)
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ // Validate input
u := new(models.User)
if err := c.Bind(u); err != nil {
- return err
+ return c.JSON(http.StatusInternalServerError, echo.Map{"error": "Failed to bind request body"})
}
- // validate user input
- if u.Username == "" || u.Password == "" || u.Email == "" {
- return c.JSON(http.StatusBadRequest, map[string]string{
- "error": "invalid request body",
- })
+ var user models.User
+ collection := db.Collection("users")
+ filter := bson.M{"email": u.Email}
+
+ if err := collection.FindOne(ctx, filter).Decode(&user); err != nil {
+ if err == mongo.ErrNoDocuments {
+ return echo.NewHTTPError(http.StatusNotFound, "Email not found")
+ }
+ return echo.NewHTTPError(http.StatusInternalServerError, "Error fetching user")
+ }
+
+ if time.Since(time.Unix(user.ForgotPassword, 0)) < (5 * time.Minute) {
+ waitTime := 5*time.Minute - time.Since(time.Unix(user.ForgotPassword, 0))
+ return echo.NewHTTPError(http.StatusTooManyRequests, fmt.Sprintf("Try again in %d minutes and %d seconds", int(waitTime.Minutes()), int(waitTime.Seconds())%60))
}
- // add the new user to the database
- // (this is a dummy implementation and would be replaced in a real application)
- // ...
+ // send email logic
+ fmt.Println("Sending forgot password email to", u.Email)
+
+ user.ForgotPassword = time.Now().Unix()
+ if _, err := collection.ReplaceOne(ctx, filter, user); err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, "Error updating user")
+ }
- // return a success response
- return c.JSON(http.StatusOK, map[string]string{
- "message": "user registered successfully",
- })
+ return c.JSON(http.StatusOK, "Forgot password email sent!")
}
diff --git a/api/handlers/forgotPassword.http b/api/handlers/forgotPassword.http
new file mode 100644
index 0000000..975a1db
--- /dev/null
+++ b/api/handlers/forgotPassword.http
@@ -0,0 +1,8 @@
+ # Your request headers, e.g.
+ POST http://localhost:8080/forgot-password
+ Content-Type: application/json
+
+ # The request body, if any
+ {
+ "email": "williamcharleshill@gmail.com"
+ }
\ No newline at end of file
diff --git a/api/handlers/register.go b/api/handlers/register.go
index 70c86ba..fda2fae 100644
--- a/api/handlers/register.go
+++ b/api/handlers/register.go
@@ -1,7 +1,7 @@
package handlers
import (
- "bkawk/go-echo/api/email"
+ "bkawk/go-echo/api/emails"
"bkawk/go-echo/api/models"
"bkawk/go-echo/api/utils"
"context"
@@ -82,8 +82,14 @@ func RegisterPost(c echo.Context) error {
return c.JSON(http.StatusInternalServerError, echo.Map{"error": "Failed to save user"})
}
+ // Get the verification URL from the environment
+ verifyUrl := os.Getenv("VERIFY_URL")
+ if verifyUrl == "" {
+ return fmt.Errorf("environment variable not set: VERIFY_URL")
+ }
+
// Send welcome email
- emailError := email.SendWelcomeEmail(u.Email, "http://example.com/verify?verificationCode="+u.VerificationCode)
+ emailError := emails.SendWelcomeEmail(u.Email, verifyUrl+"?verificationCode="+u.VerificationCode)
if emailError != nil {
fmt.Println(emailError)
return c.JSON(http.StatusInternalServerError, echo.Map{"error": emailError})
diff --git a/api/models/user.go b/api/models/user.go
index 3bc40c2..343938b 100644
--- a/api/models/user.go
+++ b/api/models/user.go
@@ -1,13 +1,15 @@
package models
type User struct {
- ID string `json:"id" bson:"_id" validate:"required"`
- Email string `json:"email" bson:"email" validate:"required,email,max=100"`
- Username string `json:"username" bson:"username" validate:"required,min=4,max=12"`
- Password string `json:"password" bson:"password" validate:"required,max=64,min=8"`
- RefreshToken string `json:"refreshToken,omitempty" bson:"refreshToken,omitempty"`
- CreatedAt int64 `json:"createdAt" bson:"createdAt"`
- VerificationCode string `json:"verificationCode,omitempty" bson:"verificationCode,omitempty"`
- LastSeen int64 `json:"lastSeen,omitempty" bson:"lastSeen,omitempty"`
- IsVerified bool `bson:"isVerified"`
+ ID string `json:"id" bson:"_id" validate:"required"`
+ Email string `json:"email" bson:"email" validate:"required,email,max=100"`
+ Username string `json:"username" bson:"username" validate:"min=4,max=12"`
+ Password string `json:"password" bson:"password" validate:"max=64,min=8"`
+ RefreshToken string `json:"refreshToken,omitempty" bson:"refreshToken,omitempty"`
+ CreatedAt int64 `json:"createdAt" bson:"createdAt"`
+ VerificationCode string `json:"verificationCode,omitempty" bson:"verificationCode,omitempty"`
+ PasswordResetToken string `json:"passwordResetToken,omitempty" bson:"passwordResetToken,omitempty"`
+ LastSeen int64 `json:"lastSeen,omitempty" bson:"lastSeen,omitempty"`
+ IsVerified bool `bson:"isVerified"`
+ ForgotPassword int64 `json:"forgotPassword,omitempty" bson:"forgotPassword,omitempty"`
}
diff --git a/api/utils/utils_test.go b/api/utils/utils_test.go
deleted file mode 100644
index 8c33016..0000000
--- a/api/utils/utils_test.go
+++ /dev/null
@@ -1,209 +0,0 @@
-package utils
-
-import (
- "fmt"
- "os"
- "testing"
- "time"
-)
-
-func TestGenerateJWT_Success(t *testing.T) {
- // Setup
- id := "user123"
- os.Setenv("JWT_SECRET", "secret_key")
-
- // Test
- token, err := GenerateJWT(id)
-
- // Assertions
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
- if token == "" {
- t.Errorf("Expected token, got empty string")
- }
-}
-
-func TestGenerateJWT_MissingSecret(t *testing.T) {
- // Setup
- id := "user123"
- os.Unsetenv("JWT_SECRET")
-
- // Test
- _, err := GenerateJWT(id)
-
- // Assertions
- if err == nil {
- t.Errorf("Expected error, got nil")
- }
-}
-
-func TestGenerateJWT_EmptyID(t *testing.T) {
- // Setup
- id := ""
- os.Setenv("JWT_SECRET", "secret_key")
-
- // Test
- _, err := GenerateJWT(id)
-
- // Assertions
- if err == nil {
- t.Errorf("Expected error, got nil")
- }
-}
-
-func TestCheckPasswordStrengthShortPassword(t *testing.T) {
- err := CheckPasswordStrength("pass")
- expected := fmt.Errorf("password must be at least 8 characters long")
-
- if err == nil || err.Error() != expected.Error() {
- t.Errorf("CheckPasswordStrength(\"pass\") = %v; want %v", err, expected)
- }
-}
-func TestCheckPasswordStrengthNoUppercase(t *testing.T) {
- err := CheckPasswordStrength("password")
- expected := fmt.Errorf("password must contain at least one uppercase letter")
-
- if err == nil || err.Error() != expected.Error() {
- t.Errorf("CheckPasswordStrength(\"password\") = %v; want %v", err, expected)
- }
-}
-
-func TestCheckPasswordStrengthNoLowercase(t *testing.T) {
- err := CheckPasswordStrength("PASSWORD")
- expected := fmt.Errorf("password must contain at least one lowercase letter")
-
- if err == nil || err.Error() != expected.Error() {
- t.Errorf("CheckPasswordStrength(\"PASSWORD\") = %v; want %v", err, expected)
- }
-}
-
-func TestCheckPasswordStrengthNoDigit(t *testing.T) {
- err := CheckPasswordStrength("Password")
- expected := fmt.Errorf("password must contain at least one digit")
-
- if err == nil || err.Error() != expected.Error() {
- t.Errorf("CheckPasswordStrength(\"Password\") = %v; want %v", err, expected)
- }
-}
-
-func TestCheckPasswordStrengthNoSpecialCharacter(t *testing.T) {
- err := CheckPasswordStrength("Password1")
- expected := fmt.Errorf("password must contain at least one special character")
-
- if err == nil || err.Error() != expected.Error() {
- t.Errorf("CheckPasswordStrength(\"Password1\") = %v; want %v", err, expected)
- }
-}
-
-func TestCheckPasswordStrengthEmptyPassword(t *testing.T) {
- err := CheckPasswordStrength("")
- expected := fmt.Errorf("password must be at least 8 characters long")
-
- if err == nil || err.Error() != expected.Error() {
- t.Errorf("CheckPasswordStrength(\"\") = %v; want %v", err, expected)
- }
-}
-
-func TestGenerateRefreshTokenSpeed(t *testing.T) {
- start := time.Now()
- _, err := GenerateRefreshToken()
- if err != nil {
- t.Fatalf("failed to generate refresh token: %v", err)
- }
- elapsed := time.Since(start)
-
- if elapsed > time.Millisecond*50 {
- t.Fatalf("GenerateRefreshToken took too long: %v", elapsed)
- }
-}
-
-func TestGenerateRefreshToken(t *testing.T) {
- token, err := GenerateRefreshToken()
- if err != nil {
- t.Errorf("GenerateRefreshToken returned error: %v", err)
- }
-
- if len(token) == 0 {
- t.Error("GenerateRefreshToken returned an empty string")
- }
-}
-
-func TestGenerateRefreshTokenSuccess(t *testing.T) {
- token, err := GenerateRefreshToken()
- if err != nil {
- t.Errorf("GenerateRefreshToken returned error: %v", err)
- }
-
- if len(token) != RefreshTokenLength {
- t.Errorf("GenerateRefreshToken returned a token of length %d, expected length %d", len(token), RefreshTokenLength)
- }
-}
-
-func TestGenerateRefreshTokenLength(t *testing.T) {
- token, err := GenerateRefreshToken()
- if err != nil {
- t.Errorf("GenerateRefreshToken returned error: %v", err)
- }
-
- if len(token) != RefreshTokenLength {
- t.Errorf("GenerateRefreshToken returned a token of length %d, expected length %d", len(token), RefreshTokenLength)
- }
-}
-
-func TestGenerateRefreshTokenUniqueness(t *testing.T) {
- tokens := make(map[string]bool)
-
- for i := 0; i < 100; i++ {
- token, err := GenerateRefreshToken()
- if err != nil {
- t.Errorf("GenerateRefreshToken returned error: %v", err)
- }
-
- if _, exists := tokens[token]; exists {
- t.Errorf("GenerateRefreshToken returned a duplicate token: %s", token)
- break
- }
-
- tokens[token] = true
- }
-}
-
-func TestGenerateUUID(t *testing.T) {
- ids := make(map[string]bool)
-
- for i := 0; i < 10000; i++ {
- id := GenerateUUID()
-
- if _, exists := ids[id]; exists {
- t.Errorf("GenerateUUID returned a duplicate identifier: %s", id)
- break
- }
-
- ids[id] = true
- }
-}
-
-func TestGenerateUUIDLength(t *testing.T) {
- expectedLength := 36
- for i := 0; i < 100; i++ {
- id := GenerateUUID()
- if len(id) != expectedLength {
- t.Errorf("GenerateUUID returned an unexpected length identifier: got %d, expected %d", len(id), expectedLength)
- break
- }
- }
-}
-
-func TestGenerateUUIDSpeed(t *testing.T) {
- numberOfIDs := 10000
- start := time.Now()
- for i := 0; i < numberOfIDs; i++ {
- GenerateUUID()
- }
- elapsed := time.Since(start)
-
- if elapsed > 100*time.Millisecond {
- t.Errorf("GenerateUUID took too long to generate %d UUIDs: %s", numberOfIDs, elapsed)
- }
-}