Files
apocapoc-api/internal/application/commands/resend_verification_email_handler.go
david 00f6b51228 Add optional email verification and registration control
Implemented email service infrastructure with SMTP support and optional email verification for self-hosted deployments. Registration flow now supports open/closed modes and hardcoded Apocapoc branding.

Key features:
- Email service with SMTP and template rendering
- Optional email verification (auto-verified without SMTP config)
- Registration modes: open/closed for access control
- Hardcoded Apocapoc branding (AppName, AppURL, DefaultFrom)
- Separate registration and login flows (registration no longer returns tokens)
2025-11-28 08:21:51 +01:00

96 lines
2.3 KiB
Go

package commands
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"time"
"apocapoc-api/internal/domain/repositories"
"apocapoc-api/internal/domain/services"
"apocapoc-api/internal/shared/errors"
)
type ResendVerificationEmailCommand struct {
Email string
}
type ResendVerificationEmailHandler struct {
userRepo repositories.UserRepository
emailService services.EmailService
appURL string
}
func NewResendVerificationEmailHandler(
userRepo repositories.UserRepository,
emailService services.EmailService,
appURL string,
) *ResendVerificationEmailHandler {
return &ResendVerificationEmailHandler{
userRepo: userRepo,
emailService: emailService,
appURL: appURL,
}
}
func (h *ResendVerificationEmailHandler) Handle(ctx context.Context, cmd ResendVerificationEmailCommand) error {
if cmd.Email == "" {
return errors.ErrInvalidInput
}
user, err := h.userRepo.FindByEmail(ctx, cmd.Email)
if err != nil {
return errors.ErrNotFound
}
if user.EmailVerified {
return errors.ErrAlreadyExists
}
token, err := generateVerificationToken()
if err != nil {
return fmt.Errorf("failed to generate verification token: %w", err)
}
expiry := time.Now().Add(24 * time.Hour)
user.EmailVerificationToken = &token
user.EmailVerificationExpiry = &expiry
user.UpdatedAt = time.Now()
if err := h.userRepo.Update(ctx, user); err != nil {
return fmt.Errorf("failed to update user: %w", err)
}
verificationLink := fmt.Sprintf("%s/verify-email?token=%s", h.appURL, token)
emailBody := fmt.Sprintf(`
<h2>Verify your email address</h2>
<p>Please click the link below to verify your email address:</p>
<p><a href="%s">Verify Email</a></p>
<p>This link will expire in 24 hours.</p>
<p>If you didn't create an account, you can safely ignore this email.</p>
`, verificationLink)
message := services.EmailMessage{
To: user.Email,
Subject: "Verify your email address",
Body: emailBody,
IsHTML: true,
}
if err := h.emailService.Send(message); err != nil {
return fmt.Errorf("failed to send verification email: %w", err)
}
return nil
}
func generateVerificationToken() (string, error) {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}