feat(API): user invitations and emails

This commit is contained in:
fsylvain committed 2026-09-29 07:15:17 +02:00
1 parent 7aae704d0a
commit acdef18b37
17 files changed
+1687 -7

No files matched your search

@@ -0,0 +1,128 @@
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
}