feat: invitation accept screen and api
This commit is contained in:
1 parent
310cfd528e
commit
55525c06d6
23 files changed
+2043
-11
No files matched your search
@@ -9,6 +9,8 @@ var ErrEmailHasAccount = errors.New("email already has an account")
|
||||
var ErrPendingInvite = errors.New("pending invite already exists")
|
||||
var ErrInviteNotFound = errors.New("invite not found")
|
||||
var ErrInviteEmailNotSent = errors.New("invite email not sent")
|
||||
var ErrInviteInvalid = errors.New("invite invalid")
|
||||
var ErrInviteExpired = errors.New("invite expired")
|
||||
|
||||
const Lifetime = 3 * 24 * time.Hour
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package invite
|
||||
|
||||
import "time"
|
||||
|
||||
type Repository interface {
|
||||
Create(invite Invite) (Invite, error)
|
||||
GetAll() []Invite
|
||||
Delete(id uint32) error
|
||||
MarkAccepted(id uint32, acceptedAt time.Time) error
|
||||
}
|
||||
@@ -14,6 +14,7 @@ var ErrAlreadyVerified = errors.New("user already verified")
|
||||
var ErrInvalidPasswordResetCode = errors.New("invalid password reset code")
|
||||
var ErrPasswordResetCodeExpired = errors.New("password reset code expired")
|
||||
var ErrInvalidDisplayName = errors.New("invalid display name")
|
||||
var ErrUsernameTaken = errors.New("username taken")
|
||||
|
||||
type User struct {
|
||||
ID uint32 `json:"id"`
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
domainInvite "RenewCMS/internal/domain/invite"
|
||||
"RenewCMS/internal/infrastructure/persistence/mappers"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -43,4 +44,9 @@ func (i *InviteRepository) Delete(id uint32) error {
|
||||
return deleteOrNotFound(result, domainInvite.ErrInviteNotFound)
|
||||
}
|
||||
|
||||
func (i *InviteRepository) MarkAccepted(id uint32, acceptedAt time.Time) error {
|
||||
result := i.db.Model(&entity.Invite{}).Where("id = ?", id).Update("accepted_at", acceptedAt)
|
||||
return deleteOrNotFound(result, domainInvite.ErrInviteNotFound)
|
||||
}
|
||||
|
||||
var _ domainInvite.Repository = &InviteRepository{}
|
||||
@@ -33,6 +33,7 @@ func (u *UserRepository) Create(user domainUser.User) (domainUser.User, error) {
|
||||
Password: user.Password,
|
||||
PasswordResetCode: mappers.DomainToNullableString(user.PasswordResetCode),
|
||||
Email: user.Email,
|
||||
IsVerified: user.IsVerified,
|
||||
VerificationCode: mappers.DomainToNullableString(user.VerificationCode),
|
||||
VerificationExpiration: user.VerificationExpiration,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/hash"
|
||||
"RenewCMS/internal/domain/invite"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AcceptInviteUseCase struct {
|
||||
userRepository user.Repository
|
||||
inviteRepository invite.Repository
|
||||
hasher hash.Hasher
|
||||
}
|
||||
|
||||
type AcceptInviteCommand struct {
|
||||
Code string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func NewAcceptInviteUseCase(userRepository user.Repository, inviteRepository invite.Repository, hasher hash.Hasher) *AcceptInviteUseCase {
|
||||
return &AcceptInviteUseCase{userRepository, inviteRepository, hasher}
|
||||
}
|
||||
|
||||
func (a *AcceptInviteUseCase) AcceptInvite(command AcceptInviteCommand) (user.User, error) {
|
||||
now := time.Now()
|
||||
|
||||
pending, err := findUsableInvite(a.inviteRepository, a.hasher, command.Code, now)
|
||||
if err != nil {
|
||||
return user.User{}, err
|
||||
}
|
||||
|
||||
if a.emailHasAccount(pending.Email) {
|
||||
return user.User{}, invite.ErrEmailHasAccount
|
||||
}
|
||||
|
||||
if err := a.ensureUsernameFree(command.Username); err != nil {
|
||||
return user.User{}, err
|
||||
}
|
||||
|
||||
hashedPassword, err := a.hasher.Hash(command.Password)
|
||||
if err != nil {
|
||||
return user.User{}, err
|
||||
}
|
||||
|
||||
createdUser, err := a.userRepository.Create(user.User{
|
||||
Username: command.Username,
|
||||
Password: hashedPassword,
|
||||
Email: pending.Email,
|
||||
IsVerified: true,
|
||||
})
|
||||
if err != nil {
|
||||
return user.User{}, err
|
||||
}
|
||||
|
||||
if err := a.inviteRepository.MarkAccepted(pending.ID, now); err != nil {
|
||||
if deleteErr := a.userRepository.Delete(createdUser.ID); deleteErr != nil {
|
||||
return user.User{}, errors.Join(err, deleteErr)
|
||||
}
|
||||
return user.User{}, err
|
||||
}
|
||||
|
||||
return createdUser, nil
|
||||
}
|
||||
|
||||
func (a *AcceptInviteUseCase) emailHasAccount(email string) bool {
|
||||
for _, existingUser := range a.userRepository.GetAll() {
|
||||
if strings.EqualFold(existingUser.Email, email) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *AcceptInviteUseCase) ensureUsernameFree(username string) error {
|
||||
_, err := a.userRepository.GetByUsername(username)
|
||||
if err == nil {
|
||||
return user.ErrUsernameTaken
|
||||
}
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/hash"
|
||||
"RenewCMS/internal/domain/invite"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GetInviteUseCase struct {
|
||||
inviteRepository invite.Repository
|
||||
hasher hash.Hasher
|
||||
}
|
||||
|
||||
func NewGetInviteUseCase(inviteRepository invite.Repository, hasher hash.Hasher) *GetInviteUseCase {
|
||||
return &GetInviteUseCase{inviteRepository, hasher}
|
||||
}
|
||||
|
||||
func (g *GetInviteUseCase) GetInvite(code string) (invite.Invite, error) {
|
||||
return findUsableInvite(g.inviteRepository, g.hasher, code, time.Now())
|
||||
}
|
||||
|
||||
func findUsableInvite(inviteRepository invite.Repository, hasher hash.Hasher, code string, now time.Time) (invite.Invite, error) {
|
||||
if code == "" {
|
||||
return invite.Invite{}, invite.ErrInviteInvalid
|
||||
}
|
||||
|
||||
for _, candidate := range inviteRepository.GetAll() {
|
||||
if candidate.AcceptedAt != nil {
|
||||
continue
|
||||
}
|
||||
if hasher.Compare(candidate.HashedCode, code) != nil {
|
||||
continue
|
||||
}
|
||||
if candidate.StatusAt(now) == invite.StatusExpired {
|
||||
return invite.Invite{}, invite.ErrInviteExpired
|
||||
}
|
||||
return candidate, nil
|
||||
}
|
||||
|
||||
return invite.Invite{}, invite.ErrInviteInvalid
|
||||
}
|
||||
Reference in new issue
Block a user