package useCases import ( "RenewCMS/internal/domain/audit" "RenewCMS/internal/domain/contenttype" "RenewCMS/internal/domain/hash" "RenewCMS/internal/domain/invite" "RenewCMS/internal/domain/mail" "RenewCMS/internal/domain/role" "RenewCMS/internal/domain/user" "context" "errors" "strings" "time" ) type CreateInviteUseCase struct { userRepository user.Repository inviteRepository invite.Repository roles role.Repository contentTypes contenttype.Repository hasher hash.Hasher mailer inviteMailer recorder audit.Recorder } type CreateInviteCommand struct { Caller ContentCaller Email string InvitedBy uint32 RoleID uint32 } func NewCreateInviteUseCase(userRepository user.Repository, inviteRepository invite.Repository, roles role.Repository, contentTypes contenttype.Repository, hasher hash.Hasher, mailRepository mail.Repository, publicURL string, recorder audit.Recorder) *CreateInviteUseCase { return &CreateInviteUseCase{userRepository, inviteRepository, roles, contentTypes, hasher, inviteMailer{mailRepository, publicURL}, recorder} } var inviteRoleRefusals = []error{role.ErrUnknownRole, role.ErrRoleLocked, role.ErrEscalation} func isInviteRoleRefusal(err error) bool { for _, refusal := range inviteRoleRefusals { if errors.Is(err, refusal) { return true } } return false } func inviteRoleParams(event audit.Event, assigned role.Role) audit.Event { event.Params[audit.ParamRoleID] = assigned.ID event.Params[audit.ParamRole] = assigned.Name return event } func (g *CreateInviteUseCase) CreateInvite(ctx context.Context, command CreateInviteCommand) (InviteView, error) { event := audit.NewEvent(audit.ActionInviteCreate, audit.CategoryUsers) created, assigned, err := g.create(command) if err != nil { if !isInviteRoleRefusal(err) { recordFailure(ctx, g.recorder, event, err) } return InviteView{}, err } event.Target = inviteTarget(created) g.recorder.Record(ctx, inviteRoleParams(event, assigned)) return InviteView{Invite: created, Role: NewRoleRef(assigned)}, nil } func (g *CreateInviteUseCase) assignedRole(command CreateInviteCommand) (role.Role, error) { if command.RoleID == 0 { return builtInRole(g.roles, role.WriterName) } return assignableRole(g.roles, command.RoleID) } func (g *CreateInviteUseCase) grantableRole(command CreateInviteCommand) (role.Role, error) { assigned, err := g.assignedRole(command) if err != nil { return role.Role{}, err } if err := ensureCovered(command.Caller, g.contentTypes, assigned); err != nil { return role.Role{}, err } return assigned, nil } func (g *CreateInviteUseCase) create(command CreateInviteCommand) (invite.Invite, role.Role, error) { email := strings.ToLower(command.Email) now := time.Now() inviter, err := g.userRepository.Get(command.InvitedBy) if err != nil { return invite.Invite{}, role.Role{}, err } assigned, err := g.grantableRole(command) if err != nil { return invite.Invite{}, role.Role{}, err } if g.emailHasAccount(email) { return invite.Invite{}, role.Role{}, invite.ErrEmailHasAccount } if err := g.discardOpenInvite(email, now); err != nil { return invite.Invite{}, role.Role{}, err } rawCode, hashedCode, err := newInviteCode(g.hasher) if err != nil { return invite.Invite{}, role.Role{}, err } createdInvite, err := g.inviteRepository.Create(invite.Invite{ Email: email, HashedCode: hashedCode, ExpiresAt: now.Add(invite.Lifetime), InvitedBy: command.InvitedBy, RoleID: assigned.ID, CreatedAt: now, }) if err != nil { return invite.Invite{}, role.Role{}, err } if sendErr := g.mailer.send(email, rawCode, inviter); sendErr != nil { if deleteErr := g.inviteRepository.Delete(createdInvite.ID); deleteErr != nil { return invite.Invite{}, role.Role{}, errors.Join(sendErr, deleteErr) } return invite.Invite{}, role.Role{}, sendErr } return createdInvite, assigned, 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 }