Compare commits

...

1 Commits

Author SHA1 Message Date
Jiang Bohan
082c356182 feat(invitation): send email notification when inviting a user
Uses the existing Resend email service to notify invitees.
Email includes inviter name, workspace name, and a link to the app.
Sent fire-and-forget in a goroutine to avoid blocking the API response.
2026-04-15 00:16:29 +08:00
2 changed files with 49 additions and 0 deletions

View File

@@ -134,11 +134,26 @@ func (h *Handler) CreateInvitation(w http.ResponseWriter, r *http.Request) {
// Notify the invitee in real time if they are a registered user.
userID := requestUserID(r)
eventPayload := map[string]any{"invitation": resp}
var workspaceName string
if ws, err := h.Queries.GetWorkspace(r.Context(), parseUUID(workspaceID)); err == nil {
workspaceName = ws.Name
eventPayload["workspace_name"] = ws.Name
}
h.publish(protocol.EventInvitationCreated, workspaceID, "member", userID, eventPayload)
// Send invitation email (fire-and-forget).
if h.EmailService != nil && workspaceName != "" {
inviterName := email // fallback
if inviter, err := h.Queries.GetUser(r.Context(), requester.UserID); err == nil {
inviterName = inviter.Name
}
go func() {
if err := h.EmailService.SendInvitationEmail(email, inviterName, workspaceName); err != nil {
slog.Warn("failed to send invitation email", "email", email, "error", err)
}
}()
}
writeJSON(w, http.StatusCreated, resp)
}

View File

@@ -3,6 +3,7 @@ package service
import (
"fmt"
"os"
"strings"
"github.com/resend/resend-go/v2"
)
@@ -52,3 +53,36 @@ func (s *EmailService) SendVerificationCode(to, code string) error {
_, err := s.client.Emails.Send(params)
return err
}
// SendInvitationEmail notifies the invitee that they have been invited to a workspace.
func (s *EmailService) SendInvitationEmail(to, inviterName, workspaceName string) error {
// Build the app URL for the invitation — users will see pending invitations
// in the workspace switcher after logging in.
appURL := strings.TrimSpace(os.Getenv("FRONTEND_ORIGIN"))
if appURL == "" {
appURL = "https://app.multica.ai"
}
if s.client == nil {
fmt.Printf("[DEV] Invitation email to %s: %s invited you to %s — %s\n", to, inviterName, workspaceName, appURL)
return nil
}
params := &resend.SendEmailRequest{
From: s.fromEmail,
To: []string{to},
Subject: fmt.Sprintf("%s invited you to %s on Multica", inviterName, workspaceName),
Html: fmt.Sprintf(
`<div style="font-family: sans-serif; max-width: 480px; margin: 0 auto;">
<h2>You're invited to join %s</h2>
<p><strong>%s</strong> invited you to collaborate in the <strong>%s</strong> workspace on Multica.</p>
<p style="margin: 24px 0;">
<a href="%s" style="display: inline-block; padding: 12px 24px; background: #000; color: #fff; text-decoration: none; border-radius: 6px; font-weight: 500;">Open Multica</a>
</p>
<p style="color: #666; font-size: 14px;">Log in to accept or decline the invitation.</p>
</div>`, workspaceName, inviterName, workspaceName, appURL),
}
_, err := s.client.Emails.Send(params)
return err
}