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

supabase / gotrue / 8135412125

04 Mar 2024 03:34AM UTC coverage: 65.142% (+0.1%) from 65.009%
8135412125

push

github

web-flow
fix: refactor request params to use generics (#1464)

## What kind of change does this PR introduce?
* Introduce a new method `retrieveRequestParams` which makes use of
generics to parse a request
* This will help to simplify parsing a request from:
```go

params := RequestParams{}
body, err := getBodyBytes(r)
if err != nil {
  return nil, badRequestError("Could not read body").WithInternalError(err)
}

if err := json.Unmarshal(body, &params); err != nil {
  return nil, badRequestError("Could not decode request params: %v", err)
}
```
to 
```go
params := &Request{}
err := retrieveRequestParams(req, params)
```

## TODO
- [x] Add type constraint instead of using `any`

48 of 69 new or added lines in 19 files covered. (69.57%)

19 existing lines in 14 files now uncovered.

7806 of 11983 relevant lines covered (65.14%)

59.29 hits per line

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

64.29
/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(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 badRequestError("Could not read claims")
×
67
        }
×
68

69
        aud := a.requestAud(ctx, r)
1✔
70
        if aud != claims.Audience {
1✔
71
                return badRequestError("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✔
NEW
87
                return err
×
UNCOV
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 unauthorizedError("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
                        return unprocessableEntityError("Updating password of an anonymous user is not possible")
×
108
                }
×
109
        }
110

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

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

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

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

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

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

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

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

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

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

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

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

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

195
                if params.Email != "" && params.Email != user.GetEmail() {
24✔
196
                        mailer := a.Mailer(ctx)
7✔
197
                        referrer := utilities.GetReferrer(r, config)
7✔
198
                        flowType := getFlowFromChallenge(params.CodeChallenge)
7✔
199
                        if isPKCEFlow(flowType) {
8✔
200
                                codeChallengeMethod, terr := models.ParseCodeChallengeMethod(params.CodeChallengeMethod)
1✔
201
                                if terr != nil {
1✔
202
                                        return terr
×
203
                                }
×
204
                                if terr := models.NewFlowStateWithUserID(tx, models.EmailChange.String(), params.CodeChallenge, codeChallengeMethod, models.EmailChange, &user.ID); terr != nil {
1✔
205
                                        return terr
×
206
                                }
×
207
                        }
208
                        externalURL := getExternalHost(ctx)
7✔
209
                        if terr = a.sendEmailChange(tx, config, user, mailer, params.Email, referrer, externalURL, config.Mailer.OtpLength, flowType); terr != nil {
7✔
210
                                if errors.Is(terr, MaxFrequencyLimitError) {
×
211
                                        return tooManyRequestsError("For security purposes, you can only request this once every 60 seconds")
×
212
                                }
×
213
                                return internalServerError("Error sending change email").WithInternalError(terr)
×
214
                        }
215
                }
216

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

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

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

247
        return sendJSON(w, http.StatusOK, user)
13✔
248
}
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