add invite to space feature
This commit is contained in:
parent
109821d0a0
commit
4d6e6799a0
10 changed files with 407 additions and 8 deletions
|
|
@ -201,6 +201,29 @@ func (s *EmailService) SendWelcomeEmail(email, name string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (s *EmailService) SendInvitationEmail(email, spaceName, inviterName, token string) error {
|
||||
inviteURL := fmt.Sprintf("%s/join/%s", s.appURL, token)
|
||||
subject, body := invitationEmailTemplate(spaceName, inviterName, inviteURL, s.appName)
|
||||
|
||||
if !s.isProd {
|
||||
slog.Info("email sent (dev mode)", "type", "invitation", "to", email, "subject", subject, "url", inviteURL)
|
||||
return nil
|
||||
}
|
||||
|
||||
params := &EmailParams{
|
||||
From: s.fromEmail,
|
||||
To: []string{email},
|
||||
Subject: subject,
|
||||
Text: body,
|
||||
}
|
||||
|
||||
_, err := s.client.SendWithContext(context.Background(), params)
|
||||
if err == nil {
|
||||
slog.Info("email sent", "type", "invitation", "to", email)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func magicLinkEmailTemplate(magicURL, appName string) (string, string) {
|
||||
subject := fmt.Sprintf("Sign in to %s", appName)
|
||||
body := fmt.Sprintf(`Click this link to sign in to your account:
|
||||
|
|
@ -231,3 +254,20 @@ The %s Team`, name, dashboardURL, appName)
|
|||
|
||||
return subject, body
|
||||
}
|
||||
|
||||
func invitationEmailTemplate(spaceName, inviterName, inviteURL, appName string) (string, string) {
|
||||
subject := fmt.Sprintf("%s invited you to join %s on %s", inviterName, spaceName, appName)
|
||||
body := fmt.Sprintf(`Hi,
|
||||
|
||||
%s has invited you to join the space "%s" on %s.
|
||||
|
||||
Click the link below to accept the invitation:
|
||||
%s
|
||||
|
||||
If you don't have an account, you will be asked to create one.
|
||||
|
||||
Best,
|
||||
The %s Team`, inviterName, spaceName, appName, inviteURL, appName)
|
||||
|
||||
return subject, body
|
||||
}
|
||||
|
|
|
|||
112
internal/service/invite.go
Normal file
112
internal/service/invite.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/utils"
|
||||
)
|
||||
|
||||
type InviteService struct {
|
||||
inviteRepo repository.InvitationRepository
|
||||
spaceRepo repository.SpaceRepository
|
||||
userRepo repository.UserRepository
|
||||
emailSvc *EmailService
|
||||
}
|
||||
|
||||
func NewInviteService(ir repository.InvitationRepository, sr repository.SpaceRepository, ur repository.UserRepository, es *EmailService) *InviteService {
|
||||
return &InviteService{
|
||||
inviteRepo: ir,
|
||||
spaceRepo: sr,
|
||||
userRepo: ur,
|
||||
emailSvc: es,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *InviteService) CreateInvite(spaceID, inviterID, email string) (*model.SpaceInvitation, error) {
|
||||
// Check if space exists
|
||||
space, err := s.spaceRepo.ByID(spaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if inviter is member/owner of space? (Ideally yes, but for now assuming caller checks permissions)
|
||||
|
||||
// Check if user is already a member
|
||||
// This would require a method on SpaceRepo or SpaceService.
|
||||
// For now, let's proceed.
|
||||
|
||||
token := utils.RandomID() // Or a more secure token generator
|
||||
expiresAt := time.Now().Add(48 * time.Hour)
|
||||
|
||||
invitation := &model.SpaceInvitation{
|
||||
Token: token,
|
||||
SpaceID: spaceID,
|
||||
InviterID: inviterID,
|
||||
Email: email,
|
||||
Status: model.InvitationStatusPending,
|
||||
ExpiresAt: expiresAt,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.inviteRepo.Create(invitation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get inviter name
|
||||
inviter, err := s.userRepo.ByID(inviterID)
|
||||
inviterName := "Someone"
|
||||
if err == nil {
|
||||
inviterName = inviter.Email // Or Name if available
|
||||
// Get profile for better name?
|
||||
}
|
||||
|
||||
// Send Email
|
||||
go s.emailSvc.SendInvitationEmail(email, space.Name, inviterName, token)
|
||||
|
||||
return invitation, nil
|
||||
}
|
||||
|
||||
func (s *InviteService) AcceptInvite(token, userID string) (string, error) {
|
||||
invite, err := s.inviteRepo.GetByToken(token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if invite.Status != model.InvitationStatusPending {
|
||||
return "", errors.New("invitation is not pending")
|
||||
}
|
||||
|
||||
if time.Now().After(invite.ExpiresAt) {
|
||||
s.inviteRepo.UpdateStatus(token, model.InvitationStatusExpired)
|
||||
return "", errors.New("invitation expired")
|
||||
}
|
||||
|
||||
// Add user to space
|
||||
err = s.spaceRepo.AddMember(invite.SpaceID, userID, model.RoleMember)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return invite.SpaceID, s.inviteRepo.UpdateStatus(token, model.InvitationStatusAccepted)
|
||||
}
|
||||
|
||||
func (s *InviteService) GetPendingInvites(spaceID string) ([]*model.SpaceInvitation, error) {
|
||||
// Filter for pending only in memory or repo?
|
||||
// Repo returns all.
|
||||
all, err := s.inviteRepo.GetBySpaceID(spaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pending []*model.SpaceInvitation
|
||||
for _, inv := range all {
|
||||
if inv.Status == model.InvitationStatusPending {
|
||||
pending = append(pending, inv)
|
||||
}
|
||||
}
|
||||
return pending, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue