package utils import ( "crypto/rand" "encoding/base64" "fmt" "time" "gopkg.in/gomail.v2" ) type ResetPasswordData struct { Token string `json:"token"` Email string `json:"email"` UserID string `json:"user_id"` ExpiresAt int64 `json:"expires_at"` Used bool `json:"used"` CreatedAt int64 `json:"created_at"` } const ( RESET_TOKEN_EXPIRY = 30 * time.Minute RESET_TOKEN_LENGTH = 32 ) // Generate secure reset token func GenerateResetToken() (string, error) { bytes := make([]byte, RESET_TOKEN_LENGTH) _, err := rand.Read(bytes) if err != nil { return "", fmt.Errorf("failed to generate reset token: %v", err) } return base64.URLEncoding.EncodeToString(bytes), nil } // Store reset password token di Redis func StoreResetToken(email, userID, token string) error { key := fmt.Sprintf("reset_password:%s", email) // Delete any existing reset token for this email DeleteCache(key) data := ResetPasswordData{ Token: token, Email: email, UserID: userID, ExpiresAt: time.Now().Add(RESET_TOKEN_EXPIRY).Unix(), Used: false, CreatedAt: time.Now().Unix(), } return SetCache(key, data, RESET_TOKEN_EXPIRY) } // Validate reset password token func ValidateResetToken(email, inputToken string) (*ResetPasswordData, error) { key := fmt.Sprintf("reset_password:%s", email) var data ResetPasswordData err := GetCache(key, &data) if err != nil { return nil, fmt.Errorf("token reset tidak ditemukan atau sudah kadaluarsa") } // Check if token is expired if time.Now().Unix() > data.ExpiresAt { DeleteCache(key) return nil, fmt.Errorf("token reset sudah kadaluarsa") } // Check if token is already used if data.Used { return nil, fmt.Errorf("token reset sudah digunakan") } // Validate token if !ConstantTimeCompare(data.Token, inputToken) { return nil, fmt.Errorf("token reset tidak valid") } return &data, nil } // Mark reset token as used func MarkResetTokenAsUsed(email string) error { key := fmt.Sprintf("reset_password:%s", email) var data ResetPasswordData err := GetCache(key, &data) if err != nil { return err } data.Used = true remaining := time.Until(time.Unix(data.ExpiresAt, 0)) return SetCache(key, data, remaining) } // Check if reset token exists and still valid func IsResetTokenValid(email string) bool { key := fmt.Sprintf("reset_password:%s", email) var data ResetPasswordData err := GetCache(key, &data) if err != nil { return false } return time.Now().Unix() <= data.ExpiresAt && !data.Used } // Get remaining reset token time func GetResetTokenRemainingTime(email string) (time.Duration, error) { key := fmt.Sprintf("reset_password:%s", email) var data ResetPasswordData err := GetCache(key, &data) if err != nil { return 0, err } remaining := time.Until(time.Unix(data.ExpiresAt, 0)) if remaining < 0 { return 0, fmt.Errorf("token expired") } return remaining, nil } // Send reset password email func (e *EmailService) SendResetPasswordEmail(email, name, token string) error { // Create reset URL - in real app this would be frontend URL resetURL := fmt.Sprintf("http://localhost:3000/reset-password?token=%s&email=%s", token, email) m := gomail.NewMessage() m.SetHeader("From", m.FormatAddress(e.from, e.fromName)) m.SetHeader("To", email) m.SetHeader("Subject", "Reset Password Administrator - Rijig") // Email template body := fmt.Sprintf(`

🔐 Reset Password

Halo %s,

Kami menerima permintaan untuk reset password akun Administrator Anda.

Klik tombol di bawah ini untuk reset password:

Reset Password

Atau copy paste link berikut ke browser Anda:

%s

Penting:

⚠️ Jika Anda tidak melakukan permintaan reset password, abaikan email ini dan password Anda tidak akan berubah.

`, name, resetURL, resetURL) m.SetBody("text/html", body) d := gomail.NewDialer(e.host, e.port, e.username, e.password) if err := d.DialAndSend(m); err != nil { return fmt.Errorf("failed to send reset password email: %v", err) } return nil }