Skip to content

Commit

Permalink
[Backend] Forgot password APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
Alfex4936 committed Mar 2, 2024
1 parent f4de0fc commit cc0e213
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 66 deletions.
36 changes: 36 additions & 0 deletions backend/handlers/auth_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
4 changes: 4 additions & 0 deletions backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
63 changes: 1 addition & 62 deletions backend/services/marker_service.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package services

import (
"errors"
"fmt"
"math"
"mime/multipart"
Expand Down Expand Up @@ -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()
Expand All @@ -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 {
Expand Down
63 changes: 59 additions & 4 deletions backend/services/smtp_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
Expand Down Expand Up @@ -53,6 +54,40 @@ var emailTemplate = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//
</body>
</html>`

var emailTemplateForReset = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="ko" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Password Reset for chulbong-kr</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<table style="background-color: #f3f3f3; color: #333; font-size: 16px; text-align: center; margin: 0; padding: 0;" width="100%%" cellspacing="0" cellpadding="0">
<tr>
<td align="center">
<table style="margin: 40px auto; border-collapse: separate;" width="600" cellspacing="0" cellpadding="0">
<tr>
<td style="padding-bottom: 20px;" align="center">
<h1 style="color: #e5b000;">Password Reset Request</h1>
</td>
</tr>
<tr>
<td style="padding-bottom: 20px;" align="center">
<p>You have requested to reset your password. Please click the link below to proceed:</p>
</td>
</tr>
<tr>
<td align="center">
<a href="{{RESET_LINK}}" style="display: inline-block; background-color: #e5b000; color: #fff; padding: 10px 20px; margin-top: 20px; font-size: 20px; font-weight: bold; text-decoration: none; letter-spacing: 2px;">Reset Password</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>`

// 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
Expand Down Expand Up @@ -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
}
66 changes: 66 additions & 0 deletions backend/services/user_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit cc0e213

Please sign in to comment.