Skip to content

Commit 96469bd

Browse files
cstocktonChris Stockton
andauthored
fix: invites should send another email when user exists (#2058)
* Added a test for double invites * Fixed control flow to send another invite Bug introduced in: #2034 Fixes: #2057 Co-authored-by: Chris Stockton <chris.stockton@supabase.io>
1 parent 2def536 commit 96469bd

File tree

2 files changed

+88
-24
lines changed

2 files changed

+88
-24
lines changed

internal/api/invite.go

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -37,38 +37,50 @@ func (a *API) Invite(w http.ResponseWriter, r *http.Request) error {
3737
if err != nil && !models.IsNotFoundError(err) {
3838
return apierrors.NewInternalServerError("Database error finding user").WithInternalError(err)
3939
}
40-
if user != nil && user.IsConfirmed() {
41-
return apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeEmailExists, DuplicateEmailMsg)
42-
}
4340

44-
signupParams := SignupParams{
45-
Email: params.Email,
46-
Data: params.Data,
47-
Aud: aud,
48-
Provider: "email",
49-
}
41+
isCreate := user == nil
42+
isConfirmed := user != nil && user.IsConfirmed()
5043

51-
user, err = signupParams.ToUserModel(false /* <- isSSOUser */)
52-
if err != nil {
53-
return err
54-
}
55-
if err := a.triggerBeforeUserCreated(r, db, user); err != nil {
56-
return err
57-
}
44+
if isCreate {
45+
signupParams := SignupParams{
46+
Email: params.Email,
47+
Data: params.Data,
48+
Aud: aud,
49+
Provider: "email",
50+
}
5851

59-
err = db.Transaction(func(tx *storage.Connection) error {
60-
user, err = a.signupNewUser(tx, user)
52+
// because params above sets no password, this method
53+
// is not computationally hard so it can be used within
54+
// a database transaction
55+
user, err = signupParams.ToUserModel(false /* <- isSSOUser */)
6156
if err != nil {
6257
return err
6358
}
64-
identity, err := a.createNewIdentity(tx, user, "email", structs.Map(provider.Claims{
65-
Subject: user.ID.String(),
66-
Email: user.GetEmail(),
67-
}))
68-
if err != nil {
59+
60+
if err := a.triggerBeforeUserCreated(r, db, user); err != nil {
6961
return err
7062
}
71-
user.Identities = []models.Identity{*identity}
63+
}
64+
65+
err = db.Transaction(func(tx *storage.Connection) error {
66+
if !isCreate {
67+
if isConfirmed {
68+
return apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeEmailExists, DuplicateEmailMsg)
69+
}
70+
} else {
71+
user, err = a.signupNewUser(tx, user)
72+
if err != nil {
73+
return err
74+
}
75+
identity, err := a.createNewIdentity(tx, user, "email", structs.Map(provider.Claims{
76+
Subject: user.ID.String(),
77+
Email: user.GetEmail(),
78+
}))
79+
if err != nil {
80+
return err
81+
}
82+
user.Identities = []models.Identity{*identity}
83+
}
7284

7385
if terr := models.NewAuditLogEntry(r, tx, adminUser, models.UserInvitedAction, "", map[string]interface{}{
7486
"user_id": user.ID,

internal/api/invite_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"testing"
1111
"time"
1212

13+
"github.com/gofrs/uuid"
1314
jwt "github.com/golang-jwt/jwt/v5"
1415
"github.com/stretchr/testify/assert"
1516
"github.com/stretchr/testify/require"
@@ -101,6 +102,57 @@ func (ts *InviteTestSuite) TestInvite() {
101102
assert.Equal(ts.T(), http.StatusOK, w.Code)
102103
}
103104

105+
func (ts *InviteTestSuite) TestInviteExists() {
106+
// To allow us to send signup and invite request in succession
107+
ts.Config.SMTP.MaxFrequency = 200
108+
109+
email := uuid.Must(uuid.NewV4()).String() + "@example.com"
110+
111+
{
112+
// Request body
113+
var buffer bytes.Buffer
114+
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
115+
"email": email,
116+
"data": map[string]interface{}{
117+
"a": 1,
118+
},
119+
}))
120+
121+
// Setup request
122+
req := httptest.NewRequest(http.MethodPost, "http://localhost/invite", &buffer)
123+
req.Header.Set("Content-Type", "application/json")
124+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
125+
126+
// Setup response recorder
127+
w := httptest.NewRecorder()
128+
129+
ts.API.handler.ServeHTTP(w, req)
130+
assert.Equal(ts.T(), http.StatusOK, w.Code)
131+
}
132+
133+
{
134+
// Request body
135+
var buffer bytes.Buffer
136+
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
137+
"email": email,
138+
"data": map[string]interface{}{
139+
"a": 1,
140+
},
141+
}))
142+
143+
// Setup request
144+
req := httptest.NewRequest(http.MethodPost, "http://localhost/invite", &buffer)
145+
req.Header.Set("Content-Type", "application/json")
146+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
147+
148+
// Setup response recorder
149+
w := httptest.NewRecorder()
150+
151+
ts.API.handler.ServeHTTP(w, req)
152+
assert.Equal(ts.T(), http.StatusOK, w.Code)
153+
}
154+
}
155+
104156
func (ts *InviteTestSuite) TestInviteAfterSignupShouldNotReturnSensitiveFields() {
105157
// To allow us to send signup and invite request in succession
106158
ts.Config.SMTP.MaxFrequency = 5

0 commit comments

Comments
 (0)