• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / gotrue / 8316299588

17 Mar 2024 02:55PM UTC coverage: 64.923% (-0.3%) from 65.241%
8316299588

Pull #1474

github

J0
fix: remove unneeded if check
Pull Request #1474: feat: add custom sms hook

87 of 197 new or added lines in 13 files covered. (44.16%)

72 existing lines in 3 files now uncovered.

8005 of 12330 relevant lines covered (64.92%)

59.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.46
/internal/api/user.go
1
package api
2

3
import (
4
        "context"
5
        "errors"
6
        "net/http"
7
        "time"
8

9
        "github.com/gofrs/uuid"
10
        "github.com/supabase/auth/internal/api/sms_provider"
11
        "github.com/supabase/auth/internal/models"
12
        "github.com/supabase/auth/internal/storage"
13
        "github.com/supabase/auth/internal/utilities"
14
)
15

16
// UserUpdateParams parameters for updating a user
17
type UserUpdateParams struct {
18
        Email               string                 `json:"email"`
19
        Password            *string                `json:"password"`
20
        Nonce               string                 `json:"nonce"`
21
        Data                map[string]interface{} `json:"data"`
22
        AppData             map[string]interface{} `json:"app_metadata,omitempty"`
23
        Phone               string                 `json:"phone"`
24
        Channel             string                 `json:"channel"`
25
        CodeChallenge       string                 `json:"code_challenge"`
26
        CodeChallengeMethod string                 `json:"code_challenge_method"`
27
}
28

29
func (a *API) validateUserUpdateParams(ctx context.Context, p *UserUpdateParams) error {
22✔
30
        config := a.config
22✔
31

22✔
32
        var err error
22✔
33
        if p.Email != "" {
29✔
34
                p.Email, err = validateEmail(p.Email)
7✔
35
                if err != nil {
7✔
36
                        return err
×
37
                }
×
38
        }
39

40
        if p.Phone != "" {
29✔
41
                if p.Phone, err = validatePhone(p.Phone); err != nil {
7✔
42
                        return err
×
43
                }
×
44
                if p.Channel == "" {
14✔
45
                        p.Channel = sms_provider.SMSProvider
7✔
46
                }
7✔
47
                if !sms_provider.IsValidMessageChannel(p.Channel, config.Sms.Provider) {
7✔
48
                        return badRequestError(ErrorCodeValidationFailed, InvalidChannelError)
×
49
                }
×
50
        }
51

52
        if p.Password != nil {
30✔
53
                if err := a.checkPasswordStrength(ctx, *p.Password); err != nil {
9✔
54
                        return err
1✔
55
                }
1✔
56
        }
57

58
        return nil
21✔
59
}
60

61
// UserGet returns a user
62
func (a *API) UserGet(w http.ResponseWriter, r *http.Request) error {
1✔
63
        ctx := r.Context()
1✔
64
        claims := getClaims(ctx)
1✔
65
        if claims == nil {
1✔
66
                return internalServerError("Could not read claims")
×
67
        }
×
68

69
        aud := a.requestAud(ctx, r)
1✔
70
        if aud != claims.Audience {
1✔
71
                return badRequestError(ErrorCodeValidationFailed, "Token audience doesn't match request audience")
×
72
        }
×
73

74
        user := getUser(ctx)
1✔
75
        return sendJSON(w, http.StatusOK, user)
1✔
76
}
77

78
// UserUpdate updates fields on a user
79
func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error {
22✔
80
        ctx := r.Context()
22✔
81
        db := a.db.WithContext(ctx)
22✔
82
        config := a.config
22✔
83
        aud := a.requestAud(ctx, r)
22✔
84

22✔
85
        params := &UserUpdateParams{}
22✔
86
        if err := retrieveRequestParams(r, params); err != nil {
22✔
87
                return err
×
88
        }
×
89

90
        user := getUser(ctx)
22✔
91
        session := getSession(ctx)
22✔
92

22✔
93
        if err := a.validateUserUpdateParams(ctx, params); err != nil {
23✔
94
                return err
1✔
95
        }
1✔
96

97
        if params.AppData != nil && !isAdmin(user, config) {
21✔
98
                if !isAdmin(user, config) {
×
99
                        return forbiddenError(ErrorCodeNotAdmin, "Updating app_metadata requires admin privileges")
×
100
                }
×
101
        }
102

103
        if user.IsAnonymous {
22✔
104
                updatingForbiddenFields := false
1✔
105
                updatingForbiddenFields = updatingForbiddenFields || (params.Password != nil && *params.Password != "")
1✔
106
                if updatingForbiddenFields {
1✔
107
                        // CHECK
×
108
                        return unprocessableEntityError(ErrorCodeUnknown, "Updating password of an anonymous user is not possible")
×
109
                }
×
110
        }
111

112
        if user.IsSSOUser {
21✔
113
                updatingForbiddenFields := false
×
114

×
115
                updatingForbiddenFields = updatingForbiddenFields || (params.Password != nil && *params.Password != "")
×
116
                updatingForbiddenFields = updatingForbiddenFields || (params.Email != "" && params.Email != user.GetEmail())
×
117
                updatingForbiddenFields = updatingForbiddenFields || (params.Phone != "" && params.Phone != user.GetPhone())
×
118
                updatingForbiddenFields = updatingForbiddenFields || (params.Nonce != "")
×
119

×
120
                if updatingForbiddenFields {
×
121
                        return unprocessableEntityError(ErrorCodeUserSSOManaged, "Updating email, phone, password of a SSO account only possible via SSO")
×
122
                }
×
123
        }
124

125
        if params.Email != "" && user.GetEmail() != params.Email {
28✔
126
                if duplicateUser, err := models.IsDuplicatedEmail(db, params.Email, aud, user); err != nil {
7✔
127
                        return internalServerError("Database error checking email").WithInternalError(err)
×
128
                } else if duplicateUser != nil {
7✔
129
                        return unprocessableEntityError(ErrorCodeEmailExists, DuplicateEmailMsg)
×
130
                }
×
131
        }
132

133
        if params.Phone != "" && user.GetPhone() != params.Phone {
27✔
134
                if exists, err := models.IsDuplicatedPhone(db, params.Phone, aud); err != nil {
6✔
135
                        return internalServerError("Database error checking phone").WithInternalError(err)
×
136
                } else if exists {
7✔
137
                        return unprocessableEntityError(ErrorCodePhoneExists, DuplicatePhoneMsg)
1✔
138
                }
1✔
139
        }
140

141
        if params.Password != nil {
27✔
142
                if config.Security.UpdatePasswordRequireReauthentication {
12✔
143
                        now := time.Now()
5✔
144
                        // we require reauthentication if the user hasn't signed in recently in the current session
5✔
145
                        if session == nil || now.After(session.CreatedAt.Add(24*time.Hour)) {
9✔
146
                                if len(params.Nonce) == 0 {
6✔
147
                                        return badRequestError(ErrorCodeReauthenticationNeeded, "Password update requires reauthentication")
2✔
148
                                }
2✔
149
                                if err := a.verifyReauthentication(params.Nonce, db, config, user); err != nil {
3✔
150
                                        return err
1✔
151
                                }
1✔
152
                        }
153
                }
154

155
                password := *params.Password
4✔
156
                if password != "" {
8✔
157
                        if user.EncryptedPassword != "" && user.Authenticate(ctx, password) {
4✔
158
                                return unprocessableEntityError(ErrorCodeSamePassword, "New password should be different from the old password.")
×
159
                        }
×
160
                }
161

162
                if err := user.SetPassword(ctx, password); err != nil {
4✔
163
                        return err
×
164
                }
×
165
        }
166

167
        err := db.Transaction(func(tx *storage.Connection) error {
34✔
168
                var terr error
17✔
169
                if params.Password != nil {
21✔
170
                        var sessionID *uuid.UUID
4✔
171
                        if session != nil {
6✔
172
                                sessionID = &session.ID
2✔
173
                        }
2✔
174

175
                        if terr = user.UpdatePassword(tx, sessionID); terr != nil {
4✔
176
                                return internalServerError("Error during password storage").WithInternalError(terr)
×
177
                        }
×
178

179
                        if terr := models.NewAuditLogEntry(r, tx, user, models.UserUpdatePasswordAction, "", nil); terr != nil {
4✔
180
                                return terr
×
181
                        }
×
182
                }
183

184
                if params.Data != nil {
17✔
185
                        if terr = user.UpdateUserMetaData(tx, params.Data); terr != nil {
×
186
                                return internalServerError("Error updating user").WithInternalError(terr)
×
187
                        }
×
188
                }
189

190
                if params.AppData != nil {
17✔
191
                        if terr = user.UpdateAppMetaData(tx, params.AppData); terr != nil {
×
192
                                return internalServerError("Error updating user").WithInternalError(terr)
×
193
                        }
×
194
                }
195

196
                if params.Email != "" && params.Email != user.GetEmail() {
24✔
197
                        mailer := a.Mailer(ctx)
7✔
198
                        referrer := utilities.GetReferrer(r, config)
7✔
199
                        flowType := getFlowFromChallenge(params.CodeChallenge)
7✔
200
                        if isPKCEFlow(flowType) {
8✔
201
                                _, terr := generateFlowState(tx, models.EmailChange.String(), models.EmailChange, params.CodeChallengeMethod, params.CodeChallenge, &user.ID)
1✔
202
                                if terr != nil {
1✔
203
                                        return terr
×
204
                                }
×
205

206
                        }
207
                        externalURL := getExternalHost(ctx)
7✔
208
                        if terr = a.sendEmailChange(tx, config, user, mailer, params.Email, referrer, externalURL, config.Mailer.OtpLength, flowType); terr != nil {
7✔
209
                                if errors.Is(terr, MaxFrequencyLimitError) {
×
210
                                        return tooManyRequestsError(ErrorCodeOverEmailSendRateLimit, "For security purposes, you can only request this once every 60 seconds")
×
211
                                }
×
212
                                return internalServerError("Error sending change email").WithInternalError(terr)
×
213
                        }
214
                }
215

216
                if params.Phone != "" && params.Phone != user.GetPhone() {
22✔
217
                        if config.Sms.Autoconfirm {
6✔
218
                                user.PhoneChange = params.Phone
1✔
219
                                if _, terr := a.smsVerify(r, tx, user, &VerifyParams{
1✔
220
                                        Type:  phoneChangeVerification,
1✔
221
                                        Phone: params.Phone,
1✔
222
                                }); terr != nil {
1✔
223
                                        return terr
×
224
                                }
×
225
                        } else {
4✔
226
                                smsProvider, terr := sms_provider.GetSmsProvider(*config)
4✔
227
                                if terr != nil {
8✔
228
                                        return internalServerError("Error finding SMS provider").WithInternalError(terr)
4✔
229
                                }
4✔
NEW
230
                                if _, terr := a.sendPhoneConfirmation(r, tx, user, params.Phone, phoneChangeVerification, smsProvider, params.Channel); terr != nil {
×
231
                                        return internalServerError("Error sending phone change otp").WithInternalError(terr)
×
232
                                }
×
233
                        }
234
                }
235

236
                if terr = models.NewAuditLogEntry(r, tx, user, models.UserModifiedAction, "", nil); terr != nil {
13✔
237
                        return internalServerError("Error recording audit log entry").WithInternalError(terr)
×
238
                }
×
239

240
                return nil
13✔
241
        })
242
        if err != nil {
21✔
243
                return err
4✔
244
        }
4✔
245

246
        return sendJSON(w, http.StatusOK, user)
13✔
247
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc