// 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 }