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(`
Halo %s,
Kami menerima permintaan untuk reset password akun Administrator Anda.
Klik tombol di bawah ini untuk reset password:
Atau copy paste link berikut ke browser Anda:
%s
Penting:
- Link ini berlaku selama 30 menit
- Link hanya dapat digunakan sekali
- Jangan bagikan link ini kepada siapapun
⚠️ 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
}