prompt 6
This commit is contained in:
247
internal/services/auth_service.go
Normal file
247
internal/services/auth_service.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"trustcontact/internal/auth"
|
||||
"trustcontact/internal/config"
|
||||
"trustcontact/internal/mailer"
|
||||
"trustcontact/internal/models"
|
||||
"trustcontact/internal/repo"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmailAlreadyExists = errors.New("email already exists")
|
||||
ErrInvalidCredentials = errors.New("invalid credentials")
|
||||
ErrEmailNotVerified = errors.New("email not verified")
|
||||
ErrInvalidOrExpiredToken = errors.New("invalid or expired token")
|
||||
)
|
||||
|
||||
type AuthService struct {
|
||||
cfg *config.Config
|
||||
users *repo.UserRepo
|
||||
verifyTokens *repo.EmailVerificationTokenRepo
|
||||
resetTokens *repo.PasswordResetTokenRepo
|
||||
mailer mailer.Mailer
|
||||
templateRender *mailer.TemplateRenderer
|
||||
nowFn func() time.Time
|
||||
}
|
||||
|
||||
func NewAuthService(database *gorm.DB, cfg *config.Config) (*AuthService, error) {
|
||||
sender, err := mailer.NewMailer(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AuthService{
|
||||
cfg: cfg,
|
||||
users: repo.NewUserRepo(database),
|
||||
verifyTokens: repo.NewEmailVerificationTokenRepo(database),
|
||||
resetTokens: repo.NewPasswordResetTokenRepo(database),
|
||||
mailer: sender,
|
||||
templateRender: mailer.NewTemplateRenderer(""),
|
||||
nowFn: func() time.Time {
|
||||
return time.Now().UTC()
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) Signup(ctx context.Context, email, password string) error {
|
||||
email = normalizeEmail(email)
|
||||
if email == "" || strings.TrimSpace(password) == "" {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
|
||||
existing, err := s.users.FindByEmail(email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing != nil {
|
||||
return ErrEmailAlreadyExists
|
||||
}
|
||||
|
||||
passwordHash, err := auth.HashPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := &models.User{
|
||||
Email: email,
|
||||
PasswordHash: passwordHash,
|
||||
EmailVerified: false,
|
||||
Role: models.RoleUser,
|
||||
}
|
||||
if err := s.users.Create(user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.issueVerifyEmail(ctx, user)
|
||||
}
|
||||
|
||||
func (s *AuthService) Login(email, password string) (*models.User, error) {
|
||||
email = normalizeEmail(email)
|
||||
user, err := s.users.FindByEmail(email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
ok, err := auth.ComparePassword(user.PasswordHash, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
if !user.EmailVerified {
|
||||
return nil, ErrEmailNotVerified
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) VerifyEmail(token string) error {
|
||||
hash := auth.HashToken(token)
|
||||
record, err := s.verifyTokens.FindValidByHash(hash, s.nowFn())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if record == nil {
|
||||
return ErrInvalidOrExpiredToken
|
||||
}
|
||||
|
||||
if err := s.users.SetEmailVerified(record.UserID, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.verifyTokens.DeleteByID(record.ID)
|
||||
}
|
||||
|
||||
func (s *AuthService) ForgotPassword(ctx context.Context, email string) error {
|
||||
email = normalizeEmail(email)
|
||||
if email == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
user, err := s.users.FindByEmail(email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user == nil || !user.EmailVerified {
|
||||
return nil
|
||||
}
|
||||
|
||||
plainToken, err := auth.NewToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.resetTokens.DeleteByUserID(user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := &models.PasswordResetToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: auth.HashToken(plainToken),
|
||||
ExpiresAt: auth.ResetTokenExpiresAt(s.nowFn()),
|
||||
}
|
||||
if err := s.resetTokens.Create(record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.sendResetEmail(ctx, user, plainToken)
|
||||
}
|
||||
|
||||
func (s *AuthService) ResetPassword(token, newPassword string) error {
|
||||
if strings.TrimSpace(newPassword) == "" {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
|
||||
hash := auth.HashToken(token)
|
||||
record, err := s.resetTokens.FindValidByHash(hash, s.nowFn())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if record == nil {
|
||||
return ErrInvalidOrExpiredToken
|
||||
}
|
||||
|
||||
passwordHash, err := auth.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.users.UpdatePasswordHash(record.UserID, passwordHash); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.resetTokens.DeleteByID(record.ID)
|
||||
}
|
||||
|
||||
func (s *AuthService) issueVerifyEmail(ctx context.Context, user *models.User) error {
|
||||
plainToken, err := auth.NewToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.verifyTokens.DeleteByUserID(user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := &models.EmailVerificationToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: auth.HashToken(plainToken),
|
||||
ExpiresAt: auth.VerifyTokenExpiresAt(s.nowFn()),
|
||||
}
|
||||
if err := s.verifyTokens.Create(record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
verifyURL := strings.TrimRight(s.cfg.BaseURL, "/") + "/verify-email?token=" + url.QueryEscape(plainToken)
|
||||
htmlBody, textBody, err := s.templateRender.RenderVerifyEmail(mailer.TemplateData{
|
||||
AppName: s.cfg.AppName,
|
||||
BaseURL: s.cfg.BaseURL,
|
||||
VerifyURL: verifyURL,
|
||||
UserEmail: user.Email,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("render verify email: %w", err)
|
||||
}
|
||||
|
||||
if err := s.mailer.Send(ctx, user.Email, "Verify your email", htmlBody, textBody); err != nil {
|
||||
return fmt.Errorf("send verify email: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AuthService) sendResetEmail(ctx context.Context, user *models.User, plainToken string) error {
|
||||
resetURL := strings.TrimRight(s.cfg.BaseURL, "/") + "/reset-password?token=" + url.QueryEscape(plainToken)
|
||||
htmlBody, textBody, err := s.templateRender.RenderResetPassword(mailer.TemplateData{
|
||||
AppName: s.cfg.AppName,
|
||||
BaseURL: s.cfg.BaseURL,
|
||||
ResetURL: resetURL,
|
||||
UserEmail: user.Email,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("render reset email: %w", err)
|
||||
}
|
||||
|
||||
if err := s.mailer.Send(ctx, user.Email, "Reset your password", htmlBody, textBody); err != nil {
|
||||
return fmt.Errorf("send reset email: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeEmail(email string) string {
|
||||
return strings.ToLower(strings.TrimSpace(email))
|
||||
}
|
||||
Reference in New Issue
Block a user