added password reset system

This commit is contained in:
Alex
2025-02-26 21:45:16 +01:00
parent 7c01b77445
commit c42adc858f
14 changed files with 350 additions and 154 deletions

View File

@@ -3,6 +3,7 @@ package services
import (
"bytes"
"html/template"
"os"
"gopkg.in/gomail.v2"
@@ -21,7 +22,7 @@ func NewEmailService(host string, port int, username string, password string) *E
return &EmailService{dialer: dialer}
}
func (s *EmailService) SendEmail(to string, subject string, body string, replyTo string) error {
func (s *EmailService) SendEmail(to string, subject string, body string, bodyTXT string, replyTo string) error {
msg := gomail.NewMessage()
msg.SetHeader("From", s.dialer.Username)
msg.SetHeader("To", to)
@@ -29,7 +30,12 @@ func (s *EmailService) SendEmail(to string, subject string, body string, replyTo
if replyTo != "" {
msg.SetHeader("REPLY_TO", replyTo)
}
msg.SetBody("text/html", body)
if bodyTXT != "" {
msg.SetBody("text/plain", bodyTXT)
}
msg.AddAlternative("text/html", body)
msg.WriteTo(os.Stdout)
if err := s.dialer.DialAndSend(msg); err != nil {
logger.Error.Printf("Could not send email to %s: %v", to, err)
@@ -79,7 +85,10 @@ func (s *EmailService) SendVerificationEmail(user *models.User, token *string) e
logger.Error.Print("Couldn't send verification mail")
return err
}
return s.SendEmail(user.Email, subject, body, "")
return s.SendEmail(user.Email, subject, body, "", "")
}
func (s *EmailService) SendChangePasswordEmail(user *models.User, token *string) error {
// Prepare data to be injected into the template
data := struct {
@@ -136,12 +145,17 @@ func (s *EmailService) SendWelcomeEmail(user *models.User) error {
}
subject := constants.MailWelcomeSubject
body, err := ParseTemplate("mail_welcome.tmpl", data)
htmlBody, err := ParseTemplate("mail_welcome.tmpl", data)
if err != nil {
logger.Error.Print("Couldn't send welcome mail")
return err
}
return s.SendEmail(user.Email, subject, body, "")
plainBody, err := ParseTemplate("mail_welcome.txt.tmpl", data)
if err != nil {
logger.Error.Print("Couldn't parse password mail")
return err
}
return s.SendEmail(user.Email, subject, htmlBody, plainBody, "")
}
func (s *EmailService) SendRegistrationNotification(user *models.User) error {
@@ -185,12 +199,17 @@ func (s *EmailService) SendRegistrationNotification(user *models.User) error {
}
subject := constants.MailRegistrationSubject
body, err := ParseTemplate("mail_registration.tmpl", data)
htmlBody, err := ParseTemplate("mail_registration.tmpl", data)
if err != nil {
logger.Error.Print("Couldn't send admin notification mail")
return err
}
return s.SendEmail(config.Recipients.UserRegistration, subject, body, "")
plainBody, err := ParseTemplate("mail_registration.txt.tmpl", data)
if err != nil {
logger.Error.Print("Couldn't parse password mail")
return err
}
return s.SendEmail(user.Email, subject, htmlBody, plainBody, "")
}
func (s *EmailService) RelayContactFormMessage(sender string, name string, message string) error {
@@ -208,10 +227,15 @@ func (s *EmailService) RelayContactFormMessage(sender string, name string, messa
WebsiteTitle: config.Site.WebsiteTitle,
}
subject := constants.MailContactSubject
body, err := ParseTemplate("mail_contact_form.tmpl", data)
htmlBody, err := ParseTemplate("mail_contact_form.tmpl", data)
if err != nil {
logger.Error.Print("Couldn't send contact form message mail")
return err
}
return s.SendEmail(config.Recipients.ContactForm, subject, body, sender)
plainBody, err := ParseTemplate("mail_contact_form.txt.tmpl", data)
if err != nil {
logger.Error.Print("Couldn't parse password mail")
return err
}
return s.SendEmail(config.Recipients.ContactForm, subject, htmlBody, plainBody, sender)
}

View File

@@ -0,0 +1,21 @@
package services
import (
"GoMembership/internal/constants"
"GoMembership/internal/models"
)
func (s *UserService) HandlePasswordChangeRequest(user *models.User) (token string, err error) {
// Deactivate user and reset Verification
if err := s.SetUserStatus(user.ID, constants.DisabledStatus); err != nil {
return "", err
}
if err := s.RevokeVerification(&user.ID, constants.VerificationTypes.Password); err != nil {
return "", err
}
// Generate a token
return s.SetVerificationToken(&user.ID, &constants.VerificationTypes.Password)
}

View File

@@ -0,0 +1,5 @@
package services
func (s *UserService) SetUserStatus(id uint, status uint) error {
return s.Repo.SetUserStatus(id, status)
}

View File

@@ -1,15 +1,12 @@
package services
import (
"net/http"
"strings"
"GoMembership/internal/constants"
"GoMembership/internal/models"
"GoMembership/internal/repositories"
"GoMembership/internal/utils"
"GoMembership/pkg/errors"
"GoMembership/pkg/logger"
"github.com/alexedwards/argon2id"
"gorm.io/gorm"
@@ -18,13 +15,17 @@ import (
)
type UserServiceInterface interface {
RegisterUser(user *models.User) (uint, string, error)
RegisterUser(user *models.User) (id uint, token string, err error)
GetUserByEmail(email string) (*models.User, error)
GetUserByID(id uint) (*models.User, error)
GetUsers(where map[string]interface{}) (*[]models.User, error)
VerifyUser(token *string) (*models.User, error)
UpdateUser(user *models.User) (*models.User, error)
DeleteUser(lastname string, id uint) error
SetUserStatus(id uint, status uint) error
VerifyUser(token *string, verificationType *string) (*models.Verification, error)
SetVerificationToken(id *uint, verificationType *string) (string, error)
RevokeVerification(id *uint, verificationType string) error
HandlePasswordChangeRequest(user *models.User) (token string, err error)
}
type UserService struct {
@@ -81,7 +82,7 @@ func (service *UserService) UpdateUser(user *models.User) (*models.User, error)
return updatedUser, nil
}
func (service *UserService) RegisterUser(user *models.User) (uint, string, error) {
func (service *UserService) RegisterUser(user *models.User) (id uint, token string, err error) {
setPassword(user.Password, user)
@@ -89,49 +90,20 @@ func (service *UserService) RegisterUser(user *models.User) (uint, string, error
user.CreatedAt = time.Now()
user.UpdatedAt = time.Now()
user.PaymentStatus = constants.AwaitingPaymentStatus
// if user.Licence == nil {
// user.Licence = &models.Licence{Status: constants.UnverifiedStatus}
// }
user.BankAccount.MandateDateSigned = time.Now()
id, err := service.Repo.CreateUser(user)
id, err = service.Repo.CreateUser(user)
if err != nil {
return 0, "", err
}
user.ID = id
token, err := utils.GenerateVerificationToken()
token, err = service.SetVerificationToken(&id, &constants.VerificationTypes.Email)
if err != nil {
return 0, "", err
}
logger.Info.Printf("TOKEN: %v", token)
// Check if user is already verified
verified, err := service.Repo.IsVerified(&user.ID)
if err != nil {
return 0, "", err
}
if verified {
return 0, "", errors.ErrAlreadyVerified
}
// Prepare the Verification record
verification := models.Verification{
UserID: user.ID,
VerificationToken: token,
}
if _, err = service.Repo.SetVerificationToken(&verification); err != nil {
return http.StatusInternalServerError, "", err
}
return id, token, nil
}
func (service *UserService) GetUserByID(id uint) (*models.User, error) {
return repositories.GetUserByID(&id)
}
@@ -146,35 +118,6 @@ func (service *UserService) GetUsers(where map[string]interface{}) (*[]models.Us
return service.Repo.GetUsers(where)
}
func (service *UserService) VerifyUser(token *string) (*models.User, error) {
verification, err := service.Repo.GetVerificationOfToken(token)
if err != nil {
return nil, err
}
// Check if the user is already verified
verified, err := service.Repo.IsVerified(&verification.UserID)
if err != nil {
return nil, err
}
user, err := repositories.GetUserByID(&verification.UserID)
if err != nil {
return nil, err
}
if verified {
return user, errors.ErrAlreadyVerified
}
// Update user status to active
t := time.Now()
verification.EmailVerifiedAt = &t
user.Status = constants.VerifiedStatus
user.Verification = *verification
user.ID = verification.UserID
service.Repo.UpdateUser(user)
return user, nil
}
func setPassword(plaintextPassword string, u *models.User) error {
hash, err := argon2id.CreateHash(plaintextPassword, argon2id.DefaultParams)
if err != nil {

View File

@@ -0,0 +1,60 @@
package services
import (
"GoMembership/internal/models"
"GoMembership/internal/utils"
"GoMembership/pkg/errors"
"time"
)
func (s *UserService) SetVerificationToken(id *uint, verificationType *string) (string, error) {
token, err := utils.GenerateVerificationToken()
if err != nil {
return "", err
}
// Check if user is already verified
verified, err := s.Repo.IsVerified(id)
if err != nil {
return "", err
}
if verified {
return "", errors.ErrAlreadyVerified
}
// Prepare the Verification record
verification := models.Verification{
UserID: *id,
VerificationToken: token,
Type: *verificationType,
}
return s.Repo.SetVerificationToken(&verification)
}
func (s *UserService) RevokeVerification(id *uint, verificationType string) error {
return s.Repo.DeleteVerification(*id, verificationType)
}
func (service *UserService) VerifyUser(token *string, verificationType *string) (*models.Verification, error) {
verification, err := service.Repo.GetVerificationOfToken(token, verificationType)
if err != nil {
return nil, err
}
// Check if the user is already verified
verified, err := service.Repo.IsVerified(&verification.UserID)
if err != nil {
return nil, err
}
if verified {
return nil, errors.ErrAlreadyVerified
}
t := time.Now()
verification.VerifiedAt = &t
// Update user status to active
return verification, nil
}