package useCases import ( "RenewCMS/internal/domain/hash" "RenewCMS/internal/domain/invite" "RenewCMS/internal/domain/mail" "RenewCMS/internal/domain/user" "errors" "fmt" "strings" "time" "github.com/google/uuid" ) const inviteTemplateName = "invitation" type CreateInviteUseCase struct { userRepository user.Repository inviteRepository invite.Repository hasher hash.Hasher mailRepository mail.Repository publicURL string } type CreateInviteCommand struct { Email string InvitedBy uint32 } type MailInviteData struct { Host string VerificationCode string Email string InvitedBy string } func (d MailInviteData) Subject() string { inviter := strings.NewReplacer("\r", " ", "\n", " ").Replace(d.InvitedBy) return inviter + " invited you to RenewCMS" } func NewCreateInviteUseCase(userRepository user.Repository, inviteRepository invite.Repository, hasher hash.Hasher, mailRepository mail.Repository, publicURL string) *CreateInviteUseCase { return &CreateInviteUseCase{userRepository, inviteRepository, hasher, mailRepository, publicURL} } func (g *CreateInviteUseCase) CreateInvite(command CreateInviteCommand) (invite.Invite, error) { email := strings.ToLower(command.Email) now := time.Now() inviter, err := g.userRepository.Get(command.InvitedBy) if err != nil { return invite.Invite{}, err } if g.emailHasAccount(email) { return invite.Invite{}, invite.ErrEmailHasAccount } if err := g.discardOpenInvite(email, now); err != nil { return invite.Invite{}, err } rawCode := uuid.NewString() hashedCode, err := g.hasher.Hash(rawCode) if err != nil { return invite.Invite{}, err } createdInvite, err := g.inviteRepository.Create(invite.Invite{ Email: email, HashedCode: hashedCode, ExpiresAt: now.Add(invite.Lifetime), InvitedBy: command.InvitedBy, CreatedAt: now, }) if err != nil { return invite.Invite{}, err } if err := g.mailRepository.Send(email, inviteTemplateName, MailInviteData{ Host: g.publicURL, VerificationCode: rawCode, Email: email, InvitedBy: inviterName(inviter), }); err != nil { sendErr := fmt.Errorf("%w: %w", invite.ErrInviteEmailNotSent, err) if deleteErr := g.inviteRepository.Delete(createdInvite.ID); deleteErr != nil { return invite.Invite{}, errors.Join(sendErr, deleteErr) } return invite.Invite{}, sendErr } return createdInvite, nil } func (g *CreateInviteUseCase) emailHasAccount(email string) bool { for _, existingUser := range g.userRepository.GetAll() { if strings.EqualFold(existingUser.Email, email) { return true } } return false } func (g *CreateInviteUseCase) discardOpenInvite(email string, now time.Time) error { for _, existing := range g.inviteRepository.GetAll() { if existing.Email != email { continue } switch existing.StatusAt(now) { case invite.StatusPending: return invite.ErrPendingInvite case invite.StatusExpired: if err := g.inviteRepository.Delete(existing.ID); err != nil { return err } } } return nil } func inviterName(inviter user.User) string { if inviter.DisplayName != "" { return inviter.DisplayName } return inviter.Username }