prompt 6
This commit is contained in:
@@ -51,7 +51,9 @@ func NewApp(cfg *config.Config) (*fiber.App, error) {
|
||||
}
|
||||
}
|
||||
|
||||
apphttp.RegisterRoutes(app, store, database, cfg)
|
||||
if err := apphttp.RegisterRoutes(app, store, database, cfg); err != nil {
|
||||
return nil, fmt.Errorf("register routes: %w", err)
|
||||
}
|
||||
|
||||
return app, nil
|
||||
}
|
||||
|
||||
196
internal/controllers/auth_controller.go
Normal file
196
internal/controllers/auth_controller.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
httpmw "trustcontact/internal/http/middleware"
|
||||
"trustcontact/internal/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type AuthController struct {
|
||||
authService *services.AuthService
|
||||
}
|
||||
|
||||
func NewAuthController(authService *services.AuthService) *AuthController {
|
||||
return &AuthController{authService: authService}
|
||||
}
|
||||
|
||||
func (ac *AuthController) ShowHome(c *fiber.Ctx) error {
|
||||
return renderPublic(c, "home.html", map[string]any{
|
||||
"Title": "Home",
|
||||
"NavSection": "public",
|
||||
})
|
||||
}
|
||||
|
||||
func (ac *AuthController) ShowSignup(c *fiber.Ctx) error {
|
||||
return renderPublic(c, "signup.html", map[string]any{
|
||||
"Title": "Sign up",
|
||||
"NavSection": "public",
|
||||
})
|
||||
}
|
||||
|
||||
func (ac *AuthController) Signup(c *fiber.Ctx) error {
|
||||
email := strings.TrimSpace(c.FormValue("email"))
|
||||
password := c.FormValue("password")
|
||||
|
||||
if err := ac.authService.Signup(c.UserContext(), email, password); err != nil {
|
||||
if errors.Is(err, services.ErrEmailAlreadyExists) {
|
||||
httpmw.SetTemplateData(c, "FlashError", "Email gia registrata")
|
||||
} else {
|
||||
httpmw.SetTemplateData(c, "FlashError", "Impossibile completare la registrazione")
|
||||
}
|
||||
return renderPublic(c, "signup.html", map[string]any{
|
||||
"Title": "Sign up",
|
||||
"NavSection": "public",
|
||||
"Email": email,
|
||||
})
|
||||
}
|
||||
|
||||
if err := httpmw.SetFlashSuccess(c, "Registrazione completata. Controlla la tua email per verificare l'account."); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Redirect("/verify-notice")
|
||||
}
|
||||
|
||||
func (ac *AuthController) ShowLogin(c *fiber.Ctx) error {
|
||||
return renderPublic(c, "login.html", map[string]any{
|
||||
"Title": "Login",
|
||||
"NavSection": "public",
|
||||
})
|
||||
}
|
||||
|
||||
func (ac *AuthController) Login(c *fiber.Ctx) error {
|
||||
email := strings.TrimSpace(c.FormValue("email"))
|
||||
password := c.FormValue("password")
|
||||
|
||||
user, err := ac.authService.Login(email, password)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrEmailNotVerified):
|
||||
httpmw.SetTemplateData(c, "FlashError", "Email non verificata. Controlla la posta.")
|
||||
case errors.Is(err, services.ErrInvalidCredentials):
|
||||
httpmw.SetTemplateData(c, "FlashError", "Credenziali non valide")
|
||||
default:
|
||||
httpmw.SetTemplateData(c, "FlashError", "Errore durante il login")
|
||||
}
|
||||
|
||||
return renderPublic(c, "login.html", map[string]any{
|
||||
"Title": "Login",
|
||||
"NavSection": "public",
|
||||
"Email": email,
|
||||
})
|
||||
}
|
||||
|
||||
if err := httpmw.SetSessionUserID(c, user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := httpmw.SetFlashSuccess(c, "Login effettuato"); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Redirect("/private")
|
||||
}
|
||||
|
||||
func (ac *AuthController) Logout(c *fiber.Ctx) error {
|
||||
if err := httpmw.ClearSessionUser(c); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := httpmw.SetFlashSuccess(c, "Logout effettuato"); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Redirect("/login")
|
||||
}
|
||||
|
||||
func (ac *AuthController) VerifyEmail(c *fiber.Ctx) error {
|
||||
token := strings.TrimSpace(c.Query("token"))
|
||||
if token == "" {
|
||||
httpmw.SetTemplateData(c, "FlashError", "Token non valido")
|
||||
return renderPublic(c, "verify_notice.html", map[string]any{
|
||||
"Title": "Verifica email",
|
||||
"NavSection": "public",
|
||||
})
|
||||
}
|
||||
|
||||
if err := ac.authService.VerifyEmail(token); err != nil {
|
||||
httpmw.SetTemplateData(c, "FlashError", "Token non valido o scaduto")
|
||||
return renderPublic(c, "verify_notice.html", map[string]any{
|
||||
"Title": "Verifica email",
|
||||
"NavSection": "public",
|
||||
})
|
||||
}
|
||||
|
||||
if err := httpmw.SetFlashSuccess(c, "Email verificata. Ora puoi accedere."); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Redirect("/login")
|
||||
}
|
||||
|
||||
func (ac *AuthController) ShowVerifyNotice(c *fiber.Ctx) error {
|
||||
return renderPublic(c, "verify_notice.html", map[string]any{
|
||||
"Title": "Verifica email",
|
||||
"NavSection": "public",
|
||||
})
|
||||
}
|
||||
|
||||
func (ac *AuthController) ShowForgotPassword(c *fiber.Ctx) error {
|
||||
return renderPublic(c, "forgot_password.html", map[string]any{
|
||||
"Title": "Forgot password",
|
||||
"NavSection": "public",
|
||||
})
|
||||
}
|
||||
|
||||
func (ac *AuthController) ForgotPassword(c *fiber.Ctx) error {
|
||||
email := strings.TrimSpace(c.FormValue("email"))
|
||||
if err := ac.authService.ForgotPassword(c.UserContext(), email); err != nil {
|
||||
httpmw.SetTemplateData(c, "FlashError", "Impossibile elaborare la richiesta")
|
||||
return renderPublic(c, "forgot_password.html", map[string]any{
|
||||
"Title": "Forgot password",
|
||||
"NavSection": "public",
|
||||
"Email": email,
|
||||
})
|
||||
}
|
||||
|
||||
httpmw.SetTemplateData(c, "FlashSuccess", "Se l'account esiste, riceverai una email con le istruzioni.")
|
||||
return renderPublic(c, "forgot_password.html", map[string]any{
|
||||
"Title": "Forgot password",
|
||||
"NavSection": "public",
|
||||
})
|
||||
}
|
||||
|
||||
func (ac *AuthController) ShowResetPassword(c *fiber.Ctx) error {
|
||||
token := strings.TrimSpace(c.Query("token"))
|
||||
return renderPublic(c, "reset_password.html", map[string]any{
|
||||
"Title": "Reset password",
|
||||
"NavSection": "public",
|
||||
"Token": token,
|
||||
})
|
||||
}
|
||||
|
||||
func (ac *AuthController) ResetPassword(c *fiber.Ctx) error {
|
||||
token := strings.TrimSpace(c.Query("token"))
|
||||
password := c.FormValue("password")
|
||||
|
||||
if token == "" {
|
||||
httpmw.SetTemplateData(c, "FlashError", "Token non valido")
|
||||
return renderPublic(c, "reset_password.html", map[string]any{
|
||||
"Title": "Reset password",
|
||||
"NavSection": "public",
|
||||
})
|
||||
}
|
||||
|
||||
if err := ac.authService.ResetPassword(token, password); err != nil {
|
||||
httpmw.SetTemplateData(c, "FlashError", "Token non valido o scaduto")
|
||||
return renderPublic(c, "reset_password.html", map[string]any{
|
||||
"Title": "Reset password",
|
||||
"NavSection": "public",
|
||||
"Token": token,
|
||||
})
|
||||
}
|
||||
|
||||
if err := httpmw.SetFlashSuccess(c, "Password aggiornata. Effettua il login."); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Redirect("/login")
|
||||
}
|
||||
53
internal/controllers/render.go
Normal file
53
internal/controllers/render.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func renderPublic(c *fiber.Ctx, page string, data map[string]any) error {
|
||||
viewData := map[string]any{}
|
||||
for k, v := range localsTemplateData(c) {
|
||||
viewData[k] = v
|
||||
}
|
||||
for k, v := range data {
|
||||
viewData[k] = v
|
||||
}
|
||||
|
||||
if _, ok := viewData["Title"]; !ok {
|
||||
viewData["Title"] = "Trustcontact"
|
||||
}
|
||||
if _, ok := viewData["NavSection"]; !ok {
|
||||
viewData["NavSection"] = "public"
|
||||
}
|
||||
|
||||
files := []string{
|
||||
"web/templates/layout.html",
|
||||
"web/templates/public/_flash.html",
|
||||
filepath.Join("web/templates/public", page),
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFiles(files...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := tmpl.ExecuteTemplate(&out, "layout.html", viewData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Type("html", "utf-8")
|
||||
return c.Send(out.Bytes())
|
||||
}
|
||||
|
||||
func localsTemplateData(c *fiber.Ctx) map[string]any {
|
||||
data, ok := c.Locals("template_data").(map[string]any)
|
||||
if !ok || data == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -1,46 +1,54 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"trustcontact/internal/config"
|
||||
"trustcontact/internal/controllers"
|
||||
httpmw "trustcontact/internal/http/middleware"
|
||||
"trustcontact/internal/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/session"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, _ *config.Config) {
|
||||
func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg *config.Config) error {
|
||||
app.Use(httpmw.SessionStoreMiddleware(store))
|
||||
app.Use(httpmw.CurrentUserMiddleware(store, database))
|
||||
app.Use(httpmw.ConsumeFlash())
|
||||
|
||||
authService, err := services.NewAuthService(database, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init auth service: %w", err)
|
||||
}
|
||||
authController := controllers.NewAuthController(authService)
|
||||
|
||||
app.Get("/healthz", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
})
|
||||
|
||||
app.Get("/", func(c *fiber.Ctx) error {
|
||||
c.Locals("nav_section", "public")
|
||||
httpmw.SetTemplateData(c, "NavSection", "public")
|
||||
return c.SendString("public area")
|
||||
})
|
||||
|
||||
app.Get("/login", func(c *fiber.Ctx) error {
|
||||
c.Locals("nav_section", "public")
|
||||
httpmw.SetTemplateData(c, "NavSection", "public")
|
||||
return c.SendString("login page")
|
||||
})
|
||||
app.Get("/", authController.ShowHome)
|
||||
app.Get("/signup", authController.ShowSignup)
|
||||
app.Post("/signup", authController.Signup)
|
||||
app.Get("/login", authController.ShowLogin)
|
||||
app.Post("/login", authController.Login)
|
||||
app.Post("/logout", authController.Logout)
|
||||
app.Get("/verify-email", authController.VerifyEmail)
|
||||
app.Get("/verify-notice", authController.ShowVerifyNotice)
|
||||
app.Get("/forgot-password", authController.ShowForgotPassword)
|
||||
app.Post("/forgot-password", authController.ForgotPassword)
|
||||
app.Get("/reset-password", authController.ShowResetPassword)
|
||||
app.Post("/reset-password", authController.ResetPassword)
|
||||
|
||||
private := app.Group("/private", httpmw.RequireAuth())
|
||||
private.Get("/", func(c *fiber.Ctx) error {
|
||||
c.Locals("nav_section", "private")
|
||||
httpmw.SetTemplateData(c, "NavSection", "private")
|
||||
return c.SendString("private area")
|
||||
})
|
||||
|
||||
admin := app.Group("/admin", httpmw.RequireAuth(), httpmw.RequireAdmin())
|
||||
admin.Get("/", func(c *fiber.Ctx) error {
|
||||
c.Locals("nav_section", "admin")
|
||||
httpmw.SetTemplateData(c, "NavSection", "admin")
|
||||
return c.SendString("admin area")
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
42
internal/repo/email_verification_token_repo.go
Normal file
42
internal/repo/email_verification_token_repo.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"trustcontact/internal/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type EmailVerificationTokenRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewEmailVerificationTokenRepo(db *gorm.DB) *EmailVerificationTokenRepo {
|
||||
return &EmailVerificationTokenRepo{db: db}
|
||||
}
|
||||
|
||||
func (r *EmailVerificationTokenRepo) Create(token *models.EmailVerificationToken) error {
|
||||
return r.db.Create(token).Error
|
||||
}
|
||||
|
||||
func (r *EmailVerificationTokenRepo) FindValidByHash(tokenHash string, now time.Time) (*models.EmailVerificationToken, error) {
|
||||
var token models.EmailVerificationToken
|
||||
err := r.db.Where("token_hash = ? AND expires_at > ?", tokenHash, now).First(&token).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
func (r *EmailVerificationTokenRepo) DeleteByID(id uint) error {
|
||||
return r.db.Delete(&models.EmailVerificationToken{}, id).Error
|
||||
}
|
||||
|
||||
func (r *EmailVerificationTokenRepo) DeleteByUserID(userID uint) error {
|
||||
return r.db.Where("user_id = ?", userID).Delete(&models.EmailVerificationToken{}).Error
|
||||
}
|
||||
42
internal/repo/password_reset_token_repo.go
Normal file
42
internal/repo/password_reset_token_repo.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"trustcontact/internal/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PasswordResetTokenRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPasswordResetTokenRepo(db *gorm.DB) *PasswordResetTokenRepo {
|
||||
return &PasswordResetTokenRepo{db: db}
|
||||
}
|
||||
|
||||
func (r *PasswordResetTokenRepo) Create(token *models.PasswordResetToken) error {
|
||||
return r.db.Create(token).Error
|
||||
}
|
||||
|
||||
func (r *PasswordResetTokenRepo) FindValidByHash(tokenHash string, now time.Time) (*models.PasswordResetToken, error) {
|
||||
var token models.PasswordResetToken
|
||||
err := r.db.Where("token_hash = ? AND expires_at > ?", tokenHash, now).First(&token).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
func (r *PasswordResetTokenRepo) DeleteByID(id uint) error {
|
||||
return r.db.Delete(&models.PasswordResetToken{}, id).Error
|
||||
}
|
||||
|
||||
func (r *PasswordResetTokenRepo) DeleteByUserID(userID uint) error {
|
||||
return r.db.Where("user_id = ?", userID).Delete(&models.PasswordResetToken{}).Error
|
||||
}
|
||||
@@ -27,3 +27,31 @@ func (r *UserRepo) FindByID(id uint) (*models.User, error) {
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepo) FindByEmail(email string) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := r.db.Where("email = ?", email).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepo) Create(user *models.User) error {
|
||||
return r.db.Create(user).Error
|
||||
}
|
||||
|
||||
func (r *UserRepo) SetEmailVerified(userID uint, verified bool) error {
|
||||
return r.db.Model(&models.User{}).
|
||||
Where("id = ?", userID).
|
||||
Update("email_verified", verified).Error
|
||||
}
|
||||
|
||||
func (r *UserRepo) UpdatePasswordHash(userID uint, passwordHash string) error {
|
||||
return r.db.Model(&models.User{}).
|
||||
Where("id = ?", userID).
|
||||
Update("password_hash", passwordHash).Error
|
||||
}
|
||||
|
||||
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