feat: Users API, ownership transfer and removal of the legacy role string

This commit is contained in:
Florian Sylvain committed 2026-10-27 03:08:15 +01:00
1 parent 094df63a7f
commit 80f7037d3a
124 files changed
+4644 -824

No files matched your search

@@ -2,9 +2,11 @@ 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"
@@ -15,52 +17,98 @@ import (
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, hasher hash.Hasher, mailRepository mail.Repository, publicURL string, recorder audit.Recorder) *CreateInviteUseCase {
return &CreateInviteUseCase{userRepository, inviteRepository, hasher, inviteMailer{mailRepository, publicURL}, recorder}
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}
}
func (g *CreateInviteUseCase) CreateInvite(ctx context.Context, command CreateInviteCommand) (invite.Invite, error) {
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, err := g.create(command)
created, assigned, err := g.create(command)
if err != nil {
recordFailure(ctx, g.recorder, event, err)
return invite.Invite{}, err
if !isInviteRoleRefusal(err) {
recordFailure(ctx, g.recorder, event, err)
}
return InviteView{}, err
}
event.Target = inviteTarget(created)
g.recorder.Record(ctx, event)
return created, nil
g.recorder.Record(ctx, inviteRoleParams(event, assigned))
return InviteView{Invite: created, Role: NewRoleRef(assigned)}, nil
}
func (g *CreateInviteUseCase) create(command CreateInviteCommand) (invite.Invite, error) {
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{}, err
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{}, invite.ErrEmailHasAccount
return invite.Invite{}, role.Role{}, invite.ErrEmailHasAccount
}
if err := g.discardOpenInvite(email, now); err != nil {
return invite.Invite{}, err
return invite.Invite{}, role.Role{}, err
}
rawCode, hashedCode, err := newInviteCode(g.hasher)
if err != nil {
return invite.Invite{}, err
return invite.Invite{}, role.Role{}, err
}
createdInvite, err := g.inviteRepository.Create(invite.Invite{
@@ -68,20 +116,21 @@ func (g *CreateInviteUseCase) create(command CreateInviteCommand) (invite.Invite
HashedCode: hashedCode,
ExpiresAt: now.Add(invite.Lifetime),
InvitedBy: command.InvitedBy,
RoleID: assigned.ID,
CreatedAt: now,
})
if err != nil {
return invite.Invite{}, err
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{}, errors.Join(sendErr, deleteErr)
return invite.Invite{}, role.Role{}, errors.Join(sendErr, deleteErr)
}
return invite.Invite{}, sendErr
return invite.Invite{}, role.Role{}, sendErr
}
return createdInvite, nil
return createdInvite, assigned, nil
}
func (g *CreateInviteUseCase) emailHasAccount(email string) bool {