127 lines
4.1 KiB
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
|
|
}
|