173 lines
4.7 KiB
Go
173 lines
4.7 KiB
Go
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
|
|
}
|
|
|
|
hasAccount, err := g.emailHasAccount(email)
|
|
if err != nil {
|
|
return invite.Invite{}, role.Role{}, err
|
|
}
|
|
if hasAccount {
|
|
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, error) {
|
|
users, err := g.userRepository.GetAll()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, existingUser := range users {
|
|
if strings.EqualFold(existingUser.Email, email) {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (g *CreateInviteUseCase) discardOpenInvite(email string, now time.Time) error {
|
|
invites, err := g.inviteRepository.GetAll()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, existing := range invites {
|
|
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
|
|
}
|