diff --git a/backend/handlers/auth_api.go b/backend/handlers/auth_api.go
index 84000cba..dceaa897 100644
--- a/backend/handlers/auth_api.go
+++ b/backend/handlers/auth_api.go
@@ -150,3 +150,39 @@ func ValidateTokenHandler(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
}
+
+func RequestResetPasswordHandler(c *fiber.Ctx) error {
+ email := c.FormValue("email")
+
+ // Generate the password reset token
+ token, err := services.GeneratePasswordResetToken(email)
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to request reset password: " + err.Error()})
+ }
+
+ // Use a goroutine to send the email without blocking
+ go func(email string) {
+
+ // Send the reset email
+ err = services.SendPasswordResetEmail(email, token)
+ if err != nil {
+ // Log the error; cannot respond to the client at this point
+ log.Printf("Error sending reset email to %s: %v", email, err)
+ return
+ }
+ }(email)
+
+ return c.SendStatus(fiber.StatusOK)
+}
+
+func ResetPasswordHandler(c *fiber.Ctx) error {
+ token := c.FormValue("token")
+ newPassword := c.FormValue("password")
+
+ err := services.ResetPassword(token, newPassword)
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to reset password"})
+ }
+
+ return c.SendStatus(fiber.StatusOK)
+}
diff --git a/backend/main.go b/backend/main.go
index d350e5d2..d62ad4bf 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -84,6 +84,10 @@ func main() {
authGroup.Get("/google/callback", handlers.GetGoogleCallbackHandler(conf))
authGroup.Post("/verify-email/send", handlers.SendVerificationEmailHandler)
authGroup.Post("/verify-email/confirm", handlers.ValidateTokenHandler)
+
+ // Finding password
+ authGroup.Post("/request-password-reset", handlers.RequestResetPasswordHandler)
+ authGroup.Post("/reset-password", handlers.ResetPasswordHandler)
}
// User routes
diff --git a/backend/services/marker_service.go b/backend/services/marker_service.go
index 67387c62..67fe1681 100644
--- a/backend/services/marker_service.go
+++ b/backend/services/marker_service.go
@@ -1,7 +1,6 @@
package services
import (
- "errors"
"fmt"
"math"
"mime/multipart"
@@ -41,66 +40,6 @@ const (
TsushimaMaxLong = 129.4938
)
-// CreateMarker creates a new marker in the database after checking for nearby markers
-func CreateMarker(markerDto *dto.MarkerRequest, userId int) (*models.Marker, error) {
- // Start a transaction
- tx, err := database.DB.Beginx()
- if err != nil {
- return nil, err
- }
- // Ensure the transaction is rolled back if any step fails
- defer func() {
- if p := recover(); p != nil {
- tx.Rollback()
- panic(p) // re-throw panic after Rollback
- } else if err != nil {
- tx.Rollback() // err is non-nil; don't change it
- } else {
- err = tx.Commit() // if Commit returns error update err with commit err
- }
- }()
-
- // First, check if there is a nearby marker
- nearby, err := IsMarkerNearby(markerDto.Latitude, markerDto.Longitude)
- if err != nil {
- return nil, err // Return any error encountered
- }
- if nearby {
- return nil, errors.New("a marker is already nearby")
- }
-
- // Insert the new marker within the transaction
- const insertQuery = `INSERT INTO Markers (UserID, Latitude, Longitude, Description, CreatedAt, UpdatedAt)
- VALUES (?, ?, ?, ?, NOW(), NOW())`
- res, err := tx.Exec(insertQuery, userId, markerDto.Latitude, markerDto.Longitude, markerDto.Description)
- if err != nil {
- return nil, fmt.Errorf("inserting marker: %w", err)
- }
-
- id, err := res.LastInsertId()
- if err != nil {
- return nil, fmt.Errorf("getting last insert ID: %w", err)
- }
-
- // Create a marker model instance to hold the full marker information
- marker := &models.Marker{
- MarkerID: int(id),
- UserID: userId,
- Latitude: markerDto.Latitude,
- Longitude: markerDto.Longitude,
- Description: markerDto.Description,
- }
-
- // Fetch the newly created marker to populate all fields, including CreatedAt and UpdatedAt
- // const selectQuery = `SELECT CreatedAt, UpdatedAt FROM Markers WHERE MarkerID = ?`
- // err = database.DB.QueryRow(selectQuery, marker.MarkerID).Scan(&marker.CreatedAt, &marker.UpdatedAt)
- // if err != nil {
- // return nil, fmt.Errorf("fetching created marker: %w", err)
- // }
-
- return marker, nil
-}
-
func CreateMarkerWithPhotos(markerDto *dto.MarkerRequest, userID int, form *multipart.Form) (*dto.MarkerResponse, error) {
// Begin a transaction for database operations
tx, err := database.DB.Beginx()
@@ -125,7 +64,7 @@ func CreateMarkerWithPhotos(markerDto *dto.MarkerRequest, userID int, form *mult
var photoURLs []string
// Process file uploads from the multipart form
- files := form.File["photos"] // Assuming "photos" is the field name for files
+ files := form.File["photos"]
for _, file := range files {
fileURL, err := UploadFileToS3(file)
if err != nil {
diff --git a/backend/services/smtp_service.go b/backend/services/smtp_service.go
index b2a61def..9ed02e49 100644
--- a/backend/services/smtp_service.go
+++ b/backend/services/smtp_service.go
@@ -13,10 +13,11 @@ import (
)
var (
- smtpServer = os.Getenv("SMTP_SERVER")
- smtpPort = os.Getenv("SMTP_PORT")
- smtpUsername = os.Getenv("SMTP_USERNAME")
- smtpPassword = os.Getenv("SMTP_PASSWORD")
+ smtpServer = os.Getenv("SMTP_SERVER")
+ smtpPort = os.Getenv("SMTP_PORT")
+ smtpUsername = os.Getenv("SMTP_USERNAME")
+ smtpPassword = os.Getenv("SMTP_PASSWORD")
+ frontendResetRouter = os.Getenv("FRONTEND_RESET_ROUTER")
)
var emailTemplate = `
@@ -53,6 +54,40 @@ var emailTemplate = `
+
+
+ Password Reset for chulbong-kr
+
+
+
+
+
+
+
+
+
+
+ Password Reset Request
+ |
+
+
+
+ You have requested to reset your password. Please click the link below to proceed:
+ |
+
+
+
+ Reset Password
+ |
+
+
+ |
+
+
+
+`
+
// GenerateToken generates a secure random token that is 6 digits long
func GenerateSixDigitToken() (string, error) {
// Define the maximum value (999999) for a 6-digit number
@@ -150,3 +185,23 @@ func SendVerificationEmail(to, token string) error {
}
return nil
}
+
+func SendPasswordResetEmail(to, token string) error {
+ // Define email headers
+ headers := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: Password Reset for chulbong-kr\r\nMIME-Version: 1.0;\r\nContent-Type: text/html; charset=\"UTF-8\";\r\n\r\n", smtpUsername, to)
+
+ // Replace the {{RESET_LINK}} placeholder with the actual reset link
+ clientUrl := fmt.Sprintf("%s?token=%s&email=%s", frontendResetRouter, token, to)
+ htmlBody := strings.Replace(emailTemplateForReset, "{{RESET_LINK}}", clientUrl, -1)
+
+ // Combine headers and HTML body into a single raw email message
+ message := []byte(headers + htmlBody)
+
+ // Connect to the SMTP server and send the email
+ auth := smtp.PlainAuth("", smtpUsername, smtpPassword, smtpServer)
+ err := smtp.SendMail(smtpServer+":"+smtpPort, auth, smtpUsername, []string{to}, message)
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/backend/services/user_service.go b/backend/services/user_service.go
index 7aea70a3..ae4487b0 100644
--- a/backend/services/user_service.go
+++ b/backend/services/user_service.go
@@ -201,3 +201,69 @@ func DeleteUserWithRelatedData(ctx context.Context, userID int) error {
return nil
}
+
+func ResetPassword(token string, newPassword string) error {
+ // Start a transaction
+ tx, err := database.DB.Beginx()
+ if err != nil {
+ return err
+ }
+
+ // Ensure the transaction is rolled back if an error occurs
+ defer func() {
+ if err != nil {
+ tx.Rollback()
+ }
+ }()
+
+ var userID int
+ // Use the transaction (tx) to perform the query
+ err = tx.Get(&userID, "SELECT UserID FROM PasswordResetTokens WHERE Token = ? AND ExpiresAt > NOW()", token)
+ if err != nil {
+ return err // Token not found or expired
+ }
+
+ hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
+ if err != nil {
+ return err
+ }
+
+ // Use the transaction (tx) to update the user's password
+ _, err = tx.Exec("UPDATE Users SET PasswordHash = ? WHERE UserID = ?", string(hashedPassword), userID)
+ if err != nil {
+ return err
+ }
+
+ // Use the transaction (tx) to delete the reset token
+ _, err = tx.Exec("DELETE FROM PasswordResetTokens WHERE Token = ?", token)
+ if err != nil {
+ return err
+ }
+
+ // Commit the transaction
+ return tx.Commit()
+}
+
+func GeneratePasswordResetToken(email string) (string, error) {
+ user := models.User{}
+ err := database.DB.Get(&user, "SELECT UserID FROM Users WHERE Email = ?", email)
+ if err != nil {
+ return "", err // User not found or db error
+ }
+
+ token, err := GenerateOpaqueToken()
+ if err != nil {
+ return "", err
+ }
+
+ _, err = database.DB.Exec(`
+ INSERT INTO PasswordResetTokens (UserID, Token, ExpiresAt)
+ VALUES (?, ?, ?)
+ ON DUPLICATE KEY UPDATE Token = VALUES(Token), ExpiresAt = VALUES(ExpiresAt)`,
+ user.UserID, token, time.Now().Add(24*time.Hour))
+ if err != nil {
+ return "", err
+ }
+
+ return token, nil
+}