feat: invite management API: DELETE /v1/user/invite/:id (204, cancels a pending invite) and POST /v1/user/invite/:id/renew (200, new code, expires_at = now+3d, created_at = now, resends the invitation email, old link dead)
This commit is contained in:
1 parent
461e93e3b5
commit
153fd8719a
18 files changed
+1147
-44
No files matched your search
@@ -0,0 +1,18 @@
|
||||
package useCases
|
||||
|
||||
import "RenewCMS/internal/domain/invite"
|
||||
|
||||
type CancelInviteUseCase struct {
|
||||
inviteRepository invite.Repository
|
||||
}
|
||||
|
||||
func NewCancelInviteUseCase(inviteRepository invite.Repository) *CancelInviteUseCase {
|
||||
return &CancelInviteUseCase{inviteRepository}
|
||||
}
|
||||
|
||||
func (c *CancelInviteUseCase) CancelInvite(id uint32) error {
|
||||
if _, err := findManageableInvite(c.inviteRepository, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.inviteRepository.Delete(id)
|
||||
}
|
||||
@@ -6,21 +6,15 @@ import (
|
||||
"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
|
||||
mailer inviteMailer
|
||||
}
|
||||
|
||||
type CreateInviteCommand struct {
|
||||
@@ -28,20 +22,8 @@ type CreateInviteCommand struct {
|
||||
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}
|
||||
return &CreateInviteUseCase{userRepository, inviteRepository, hasher, inviteMailer{mailRepository, publicURL}}
|
||||
}
|
||||
|
||||
func (g *CreateInviteUseCase) CreateInvite(command CreateInviteCommand) (invite.Invite, error) {
|
||||
@@ -61,8 +43,7 @@ func (g *CreateInviteUseCase) CreateInvite(command CreateInviteCommand) (invite.
|
||||
return invite.Invite{}, err
|
||||
}
|
||||
|
||||
rawCode := uuid.NewString()
|
||||
hashedCode, err := g.hasher.Hash(rawCode)
|
||||
rawCode, hashedCode, err := newInviteCode(g.hasher)
|
||||
if err != nil {
|
||||
return invite.Invite{}, err
|
||||
}
|
||||
@@ -78,13 +59,7 @@ func (g *CreateInviteUseCase) CreateInvite(command CreateInviteCommand) (invite.
|
||||
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 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)
|
||||
}
|
||||
@@ -119,10 +94,3 @@ func (g *CreateInviteUseCase) discardOpenInvite(email string, now time.Time) err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func inviterName(inviter user.User) string {
|
||||
if inviter.DisplayName != "" {
|
||||
return inviter.DisplayName
|
||||
}
|
||||
return inviter.Username
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/hash"
|
||||
"RenewCMS/internal/domain/invite"
|
||||
"RenewCMS/internal/domain/mail"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RenewInviteUseCase struct {
|
||||
userRepository user.Repository
|
||||
inviteRepository invite.Repository
|
||||
hasher hash.Hasher
|
||||
mailer inviteMailer
|
||||
}
|
||||
|
||||
type RenewInviteCommand struct {
|
||||
ID uint32
|
||||
RenewedBy uint32
|
||||
}
|
||||
|
||||
func NewRenewInviteUseCase(userRepository user.Repository, inviteRepository invite.Repository, hasher hash.Hasher, mailRepository mail.Repository, publicURL string) *RenewInviteUseCase {
|
||||
return &RenewInviteUseCase{userRepository, inviteRepository, hasher, inviteMailer{mailRepository, publicURL}}
|
||||
}
|
||||
|
||||
func (u *RenewInviteUseCase) RenewInvite(command RenewInviteCommand) (invite.Invite, error) {
|
||||
now := time.Now()
|
||||
|
||||
renewer, err := u.userRepository.Get(command.RenewedBy)
|
||||
if err != nil {
|
||||
return invite.Invite{}, err
|
||||
}
|
||||
|
||||
previous, err := findManageableInvite(u.inviteRepository, command.ID)
|
||||
if err != nil {
|
||||
return invite.Invite{}, err
|
||||
}
|
||||
|
||||
rawCode, hashedCode, err := newInviteCode(u.hasher)
|
||||
if err != nil {
|
||||
return invite.Invite{}, err
|
||||
}
|
||||
|
||||
renewed, err := u.inviteRepository.Renew(previous.ID, hashedCode, now.Add(invite.Lifetime), now)
|
||||
if err != nil {
|
||||
return invite.Invite{}, err
|
||||
}
|
||||
|
||||
if sendErr := u.mailer.send(renewed.Email, rawCode, renewer); sendErr != nil {
|
||||
if _, restoreErr := u.inviteRepository.Renew(previous.ID, previous.HashedCode, previous.ExpiresAt, previous.CreatedAt); restoreErr != nil {
|
||||
return invite.Invite{}, errors.Join(sendErr, restoreErr)
|
||||
}
|
||||
return invite.Invite{}, sendErr
|
||||
}
|
||||
|
||||
return renewed, nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package useCases
|
||||
|
||||
import "RenewCMS/internal/domain/invite"
|
||||
|
||||
func findManageableInvite(inviteRepository invite.Repository, id uint32) (invite.Invite, error) {
|
||||
for _, existing := range inviteRepository.GetAll() {
|
||||
if existing.ID != id {
|
||||
continue
|
||||
}
|
||||
if existing.AcceptedAt != nil {
|
||||
return invite.Invite{}, invite.ErrInviteAlreadyAccepted
|
||||
}
|
||||
return existing, nil
|
||||
}
|
||||
return invite.Invite{}, invite.ErrInviteNotFound
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/hash"
|
||||
"RenewCMS/internal/domain/invite"
|
||||
"RenewCMS/internal/domain/mail"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const inviteTemplateName = "invitation"
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
type inviteMailer struct {
|
||||
mailRepository mail.Repository
|
||||
publicURL string
|
||||
}
|
||||
|
||||
func (m inviteMailer) send(email string, rawCode string, inviter user.User) error {
|
||||
if err := m.mailRepository.Send(email, inviteTemplateName, MailInviteData{
|
||||
Host: m.publicURL,
|
||||
VerificationCode: rawCode,
|
||||
Email: email,
|
||||
InvitedBy: inviterName(inviter),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("%w: %w", invite.ErrInviteEmailNotSent, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newInviteCode(hasher hash.Hasher) (string, string, error) {
|
||||
rawCode := uuid.NewString()
|
||||
hashedCode, err := hasher.Hash(rawCode)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return rawCode, hashedCode, nil
|
||||
}
|
||||
|
||||
func inviterName(inviter user.User) string {
|
||||
if inviter.DisplayName != "" {
|
||||
return inviter.DisplayName
|
||||
}
|
||||
return inviter.Username
|
||||
}
|
||||
Reference in new issue
Block a user