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 = ` ` +var emailTemplateForReset = ` + + + 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 +}