monorepo/cloud/maplefile-backend/internal/service/auth/verify_email.go

127 lines
4.1 KiB
Go

// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth/verify_email.go
package auth
import (
"context"
"fmt"
"strings"
"time"
"go.uber.org/zap"
uc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/auditlog"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
)
type VerifyEmailRequestDTO struct {
Code string `json:"code"`
}
type VerifyEmailResponseDTO struct {
Message string `json:"message"`
Success bool `json:"success"`
UserRole int8 `json:"user_role"`
}
type VerifyEmailService interface {
Execute(ctx context.Context, req *VerifyEmailRequestDTO) (*VerifyEmailResponseDTO, error)
}
type verifyEmailServiceImpl struct {
logger *zap.Logger
auditLogger auditlog.AuditLogger
userGetByVerificationCodeUC uc_user.UserGetByVerificationCodeUseCase
userUpdateUC uc_user.UserUpdateUseCase
}
func NewVerifyEmailService(
logger *zap.Logger,
auditLogger auditlog.AuditLogger,
userGetByVerificationCodeUC uc_user.UserGetByVerificationCodeUseCase,
userUpdateUC uc_user.UserUpdateUseCase,
) VerifyEmailService {
return &verifyEmailServiceImpl{
logger: logger.Named("VerifyEmailService"),
auditLogger: auditLogger,
userGetByVerificationCodeUC: userGetByVerificationCodeUC,
userUpdateUC: userUpdateUC,
}
}
func (s *verifyEmailServiceImpl) Execute(ctx context.Context, req *VerifyEmailRequestDTO) (*VerifyEmailResponseDTO, error) {
// Validate request
if err := s.validateVerifyEmailRequest(req); err != nil {
return nil, err // Returns RFC 9457 ProblemDetail
}
// Get user by verification code
user, err := s.userGetByVerificationCodeUC.Execute(ctx, req.Code)
if err != nil || user == nil {
s.logger.Warn("Invalid verification code attempted")
return nil, httperror.NewNotFoundError("Verification code not found or has already been used")
}
// Check if code has expired
if time.Now().After(user.SecurityData.CodeExpiry) {
s.logger.Warn("Verification code expired",
zap.String("user_id", user.ID.String()),
zap.Time("expiry", user.SecurityData.CodeExpiry))
return nil, httperror.NewBadRequestError("Verification code has expired. Please request a new verification email.")
}
// Update user to mark as verified
user.SecurityData.WasEmailVerified = true
user.SecurityData.Code = ""
user.SecurityData.CodeExpiry = time.Time{}
user.ModifiedAt = time.Now()
if err := s.userUpdateUC.Execute(ctx, user); err != nil {
s.logger.Error("Failed to update user", zap.Error(err))
return nil, httperror.NewInternalServerError(fmt.Sprintf("Failed to verify email: %v", err))
}
s.logger.Info("Email verified successfully", zap.String("user_id", user.ID.String()))
// Audit log email verification
s.auditLogger.LogAuth(ctx, auditlog.EventTypeEmailVerified, auditlog.OutcomeSuccess,
validation.MaskEmail(user.Email), "", map[string]string{
"user_id": user.ID.String(),
})
return &VerifyEmailResponseDTO{
Message: "Email verified successfully. You can now log in.",
Success: true,
UserRole: user.Role,
}, nil
}
// validateVerifyEmailRequest validates the verify email request.
// Returns RFC 9457 ProblemDetail error with field-specific errors.
func (s *verifyEmailServiceImpl) validateVerifyEmailRequest(req *VerifyEmailRequestDTO) error {
errors := make(map[string]string)
// Validate verification code
code := strings.TrimSpace(req.Code)
if code == "" {
errors["code"] = "Verification code is required"
} else if len(code) != 8 {
errors["code"] = "Verification code must be 8 digits"
} else {
// Validate that code is numeric
for _, c := range code {
if c < '0' || c > '9' {
errors["code"] = "Verification code must contain only numbers"
break
}
}
}
// If there are validation errors, return RFC 9457 error
if len(errors) > 0 {
return httperror.NewValidationError(errors)
}
return nil
}