This commit is contained in:
2026-08-17 00:53:51 +02:00
parent 716604eb69
commit c2429873b3
12 changed files with 94 additions and 116 deletions
+12 -28
View File
@@ -217,34 +217,18 @@ func (s *Server) handleCreateInvite(w http.ResponseWriter, r *http.Request) {
ClientOrLog(w, http.StatusBadRequest, "could not create invite", err, auth.ClientError)
return
}
companyName, _ := s.Auth.CompanyName(r.Context(), cid)
smtpOn := s.Mail != nil && s.Mail.Enabled()
sendOK := false
if s.Mail != nil {
msg := mail.InviteMessage(s.Config.WebOrigin, inv.Email, token, companyName)
if err := s.Mail.Send(msg); err == nil {
sendOK = true
}
}
// noop/disabled mailers return nil from Send; only count real SMTP as delivered.
mailSent, includeToken := inviteMailResult(smtpOn, sendOK)
resp := map[string]any{
"id": inv.ID, "email": inv.Email, "role": inv.Role,
"expires_at": inv.ExpiresAt, "mail_sent": mailSent, "smtp_enabled": smtpOn,
}
// Token returned when email was not delivered so operators can share the accept link.
if includeToken {
resp["token"] = token
resp["accept_url"] = mail.AcceptInviteURL(s.Config.WebOrigin, token)
}
JSON(w, http.StatusCreated, resp)
}
// inviteMailResult decides mail_sent and whether the accept token must be returned to the client.
func inviteMailResult(smtpEnabled, sendOK bool) (mailSent bool, includeToken bool) {
mailSent = smtpEnabled && sendOK
includeToken = !mailSent
return
// Invites are link-based: always return the one-time accept URL for the admin to share.
// Email delivery is not used for team invites (SMTP may still be used for password reset, etc.).
JSON(w, http.StatusCreated, map[string]any{
"id": inv.ID,
"email": inv.Email,
"role": inv.Role,
"expires_at": inv.ExpiresAt,
"mail_sent": false,
"smtp_enabled": s.Mail != nil && s.Mail.Enabled(),
"token": token,
"accept_url": mail.AcceptInviteURL(s.Config.WebOrigin, token),
})
}
func (s *Server) handleRemoveMember(w http.ResponseWriter, r *http.Request) {
@@ -1,31 +1,20 @@
package httpapi
import "testing"
import (
"strings"
"testing"
func TestInviteMailResult(t *testing.T) {
"github.com/descrybe/descrybe-v2/apps/api/internal/mail"
)
func TestInviteAcceptURLAlwaysShareable(t *testing.T) {
t.Parallel()
cases := []struct {
name string
smtpEnabled bool
sendOK bool
wantMailSent bool
wantToken bool
}{
{name: "smtp_ok", smtpEnabled: true, sendOK: true, wantMailSent: true, wantToken: false},
{name: "smtp_send_fail", smtpEnabled: true, sendOK: false, wantMailSent: false, wantToken: true},
{name: "noop_mailer_send_ok", smtpEnabled: false, sendOK: true, wantMailSent: false, wantToken: true},
{name: "disabled_no_send", smtpEnabled: false, sendOK: false, wantMailSent: false, wantToken: true},
url := mail.AcceptInviteURL("https://app.example.com/", "tok_abc")
want := "https://app.example.com/accept-invite?token=tok_abc"
if url != want {
t.Fatalf("AcceptInviteURL = %q want %q", url, want)
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
mailSent, includeToken := inviteMailResult(tc.smtpEnabled, tc.sendOK)
if mailSent != tc.wantMailSent {
t.Fatalf("mailSent=%v want %v", mailSent, tc.wantMailSent)
}
if includeToken != tc.wantToken {
t.Fatalf("includeToken=%v want %v", includeToken, tc.wantToken)
}
})
if strings.Contains(url, "mailto:") {
t.Fatal("invite URL must not be mailto")
}
}