• 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

54.94
/internal/api/mail.go
1
package api
2

3
import (
4
        "net/http"
5
        "net/url"
6
        "strings"
7
        "time"
8

9
        "github.com/badoux/checkmail"
10
        "github.com/fatih/structs"
11
        "github.com/pkg/errors"
12
        "github.com/sethvargo/go-password/password"
13
        "github.com/supabase/auth/internal/api/provider"
14
        "github.com/supabase/auth/internal/conf"
15
        "github.com/supabase/auth/internal/crypto"
16
        "github.com/supabase/auth/internal/mailer"
17
        "github.com/supabase/auth/internal/models"
18
        "github.com/supabase/auth/internal/storage"
19
        "github.com/supabase/auth/internal/utilities"
20
)
21

22
var (
23
        MaxFrequencyLimitError error = errors.New("frequency limit reached")
24
)
25

26
type GenerateLinkParams struct {
27
        Type       string                 `json:"type"`
28
        Email      string                 `json:"email"`
29
        NewEmail   string                 `json:"new_email"`
30
        Password   string                 `json:"password"`
31
        Data       map[string]interface{} `json:"data"`
32
        RedirectTo string                 `json:"redirect_to"`
33
}
34

35
type GenerateLinkResponse struct {
36
        models.User
37
        ActionLink       string `json:"action_link"`
38
        EmailOtp         string `json:"email_otp"`
39
        HashedToken      string `json:"hashed_token"`
40
        VerificationType string `json:"verification_type"`
41
        RedirectTo       string `json:"redirect_to"`
42
}
43

44
func (a *API) adminGenerateLink(w http.ResponseWriter, r *http.Request) error {
7✔
45
        ctx := r.Context()
7✔
46
        db := a.db.WithContext(ctx)
7✔
47
        config := a.config
7✔
48
        mailer := a.Mailer(ctx)
7✔
49
        adminUser := getAdminUser(ctx)
7✔
50
        params := &GenerateLinkParams{}
7✔
51
        if err := retrieveRequestParams(r, params); err != nil {
7✔
NEW
52
                return err
×
UNCOV
53
        }
×
54

55
        var err error
7✔
56
        params.Email, err = validateEmail(params.Email)
7✔
57
        if err != nil {
7✔
58
                return err
×
59
        }
×
60
        referrer := utilities.GetReferrer(r, config)
7✔
61
        if utilities.IsRedirectURLValid(config, params.RedirectTo) {
8✔
62
                referrer = params.RedirectTo
1✔
63
        }
1✔
64

65
        aud := a.requestAud(ctx, r)
7✔
66
        user, err := models.FindUserByEmailAndAudience(db, params.Email, aud)
7✔
67
        if err != nil {
7✔
68
                if models.IsNotFoundError(err) {
×
69
                        if params.Type == magicLinkVerification {
×
70
                                params.Type = signupVerification
×
71
                                params.Password, err = password.Generate(64, 10, 1, false, true)
×
72
                                if err != nil {
×
73
                                        return internalServerError("error creating user").WithInternalError(err)
×
74
                                }
×
75
                        } else if params.Type == recoveryVerification || params.Type == "email_change_current" || params.Type == "email_change_new" {
×
76
                                return notFoundError(err.Error())
×
77
                        }
×
78
                } else {
×
79
                        return internalServerError("Database error finding user").WithInternalError(err)
×
80
                }
×
81
        }
82

83
        var url string
7✔
84
        now := time.Now()
7✔
85
        otp, err := crypto.GenerateOtp(config.Mailer.OtpLength)
7✔
86
        if err != nil {
7✔
87
                return err
×
88
        }
×
89

90
        hashedToken := crypto.GenerateTokenHash(params.Email, otp)
7✔
91

7✔
92
        var signupUser *models.User
7✔
93
        if params.Type == signupVerification && user == nil {
7✔
94
                signupParams := &SignupParams{
×
95
                        Email:    params.Email,
×
96
                        Password: params.Password,
×
97
                        Data:     params.Data,
×
98
                        Provider: "email",
×
99
                        Aud:      aud,
×
100
                }
×
101

×
102
                if err := a.validateSignupParams(ctx, signupParams); err != nil {
×
103
                        return err
×
104
                }
×
105

106
                signupUser, err = signupParams.ToUserModel(false /* <- isSSOUser */)
×
107
                if err != nil {
×
108
                        return err
×
109
                }
×
110
        }
111

112
        err = db.Transaction(func(tx *storage.Connection) error {
14✔
113
                var terr error
7✔
114
                switch params.Type {
7✔
115
                case magicLinkVerification, recoveryVerification:
2✔
116
                        if terr = models.NewAuditLogEntry(r, tx, user, models.UserRecoveryRequestedAction, "", nil); terr != nil {
2✔
117
                                return terr
×
118
                        }
×
119
                        user.RecoveryToken = hashedToken
2✔
120
                        user.RecoverySentAt = &now
2✔
121
                        terr = errors.Wrap(tx.UpdateOnly(user, "recovery_token", "recovery_sent_at"), "Database error updating user for recovery")
2✔
122
                case inviteVerification:
1✔
123
                        if user != nil {
2✔
124
                                if user.IsConfirmed() {
1✔
125
                                        return unprocessableEntityError(DuplicateEmailMsg)
×
126
                                }
×
127
                        } else {
×
128
                                signupParams := &SignupParams{
×
129
                                        Email:    params.Email,
×
130
                                        Data:     params.Data,
×
131
                                        Provider: "email",
×
132
                                        Aud:      aud,
×
133
                                }
×
134

×
135
                                // because params above sets no password, this
×
136
                                // method is not computationally hard so it can
×
137
                                // be used within a database transaction
×
138
                                user, terr = signupParams.ToUserModel(false /* <- isSSOUser */)
×
139
                                if terr != nil {
×
140
                                        return terr
×
141
                                }
×
142

143
                                user, terr = a.signupNewUser(tx, user)
×
144
                                if terr != nil {
×
145
                                        return terr
×
146
                                }
×
147
                                identity, terr := a.createNewIdentity(tx, user, "email", structs.Map(provider.Claims{
×
148
                                        Subject: user.ID.String(),
×
149
                                        Email:   user.GetEmail(),
×
150
                                }))
×
151
                                if terr != nil {
×
152
                                        return terr
×
153
                                }
×
154
                                user.Identities = []models.Identity{*identity}
×
155
                        }
156
                        if terr = models.NewAuditLogEntry(r, tx, adminUser, models.UserInvitedAction, "", map[string]interface{}{
1✔
157
                                "user_id":    user.ID,
1✔
158
                                "user_email": user.Email,
1✔
159
                        }); terr != nil {
1✔
160
                                return terr
×
161
                        }
×
162
                        user.ConfirmationToken = hashedToken
1✔
163
                        user.ConfirmationSentAt = &now
1✔
164
                        user.InvitedAt = &now
1✔
165
                        terr = errors.Wrap(tx.UpdateOnly(user, "confirmation_token", "confirmation_sent_at", "invited_at"), "Database error updating user for invite")
1✔
166
                case signupVerification:
2✔
167
                        if user != nil {
4✔
168
                                if user.IsConfirmed() {
2✔
169
                                        return unprocessableEntityError(DuplicateEmailMsg)
×
170
                                }
×
171
                                if err := user.UpdateUserMetaData(tx, params.Data); err != nil {
2✔
172
                                        return internalServerError("Database error updating user").WithInternalError(err)
×
173
                                }
×
174
                        } else {
×
175
                                // you should never use SignupParams with
×
176
                                // password here to generate a new user, use
×
177
                                // signupUser which is a model generated from
×
178
                                // SignupParams above
×
179
                                user, terr = a.signupNewUser(tx, signupUser)
×
180
                                if terr != nil {
×
181
                                        return terr
×
182
                                }
×
183
                                identity, terr := a.createNewIdentity(tx, user, "email", structs.Map(provider.Claims{
×
184
                                        Subject: user.ID.String(),
×
185
                                        Email:   user.GetEmail(),
×
186
                                }))
×
187
                                if terr != nil {
×
188
                                        return terr
×
189
                                }
×
190
                                user.Identities = []models.Identity{*identity}
×
191
                        }
192
                        user.ConfirmationToken = hashedToken
2✔
193
                        user.ConfirmationSentAt = &now
2✔
194
                        terr = errors.Wrap(tx.UpdateOnly(user, "confirmation_token", "confirmation_sent_at"), "Database error updating user for confirmation")
2✔
195
                case "email_change_current", "email_change_new":
2✔
196
                        if !config.Mailer.SecureEmailChangeEnabled && params.Type == "email_change_current" {
2✔
197
                                return unprocessableEntityError("Enable secure email change to generate link for current email")
×
198
                        }
×
199
                        params.NewEmail, terr = validateEmail(params.NewEmail)
2✔
200
                        if terr != nil {
2✔
201
                                return unprocessableEntityError("The new email address provided is invalid")
×
202
                        }
×
203
                        if duplicateUser, terr := models.IsDuplicatedEmail(tx, params.NewEmail, user.Aud, user); terr != nil {
2✔
204
                                return internalServerError("Database error checking email").WithInternalError(terr)
×
205
                        } else if duplicateUser != nil {
2✔
206
                                return unprocessableEntityError(DuplicateEmailMsg)
×
207
                        }
×
208
                        now := time.Now()
2✔
209
                        user.EmailChangeSentAt = &now
2✔
210
                        user.EmailChange = params.NewEmail
2✔
211
                        user.EmailChangeConfirmStatus = zeroConfirmation
2✔
212
                        if params.Type == "email_change_current" {
3✔
213
                                user.EmailChangeTokenCurrent = hashedToken
1✔
214
                        } else if params.Type == "email_change_new" {
3✔
215
                                user.EmailChangeTokenNew = crypto.GenerateTokenHash(params.NewEmail, otp)
1✔
216
                        }
1✔
217
                        terr = errors.Wrap(tx.UpdateOnly(user, "email_change_token_current", "email_change_token_new", "email_change", "email_change_sent_at", "email_change_confirm_status"), "Database error updating user for email change")
2✔
218
                default:
×
219
                        return badRequestError("Invalid email action link type requested: %v", params.Type)
×
220
                }
221

222
                if terr != nil {
7✔
223
                        return terr
×
224
                }
×
225

226
                externalURL := getExternalHost(ctx)
7✔
227
                url, terr = mailer.GetEmailActionLink(user, params.Type, referrer, externalURL)
7✔
228
                if terr != nil {
7✔
229
                        return terr
×
230
                }
×
231
                return nil
7✔
232
        })
233

234
        if err != nil {
7✔
235
                return err
×
236
        }
×
237

238
        resp := GenerateLinkResponse{
7✔
239
                User:             *user,
7✔
240
                ActionLink:       url,
7✔
241
                EmailOtp:         otp,
7✔
242
                HashedToken:      hashedToken,
7✔
243
                VerificationType: params.Type,
7✔
244
                RedirectTo:       referrer,
7✔
245
        }
7✔
246

7✔
247
        return sendJSON(w, http.StatusOK, resp)
7✔
248
}
249

250
func sendConfirmation(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string, externalURL *url.URL, otpLength int, flowType models.FlowType) error {
10✔
251
        var err error
10✔
252
        if u.ConfirmationSentAt != nil && !u.ConfirmationSentAt.Add(maxFrequency).Before(time.Now()) {
10✔
253
                return MaxFrequencyLimitError
×
254
        }
×
255
        oldToken := u.ConfirmationToken
10✔
256
        otp, err := crypto.GenerateOtp(otpLength)
10✔
257
        if err != nil {
10✔
258
                return err
×
259
        }
×
260
        token := crypto.GenerateTokenHash(u.GetEmail(), otp)
10✔
261
        u.ConfirmationToken = addFlowPrefixToToken(token, flowType)
10✔
262
        now := time.Now()
10✔
263
        if err := mailer.ConfirmationMail(u, otp, referrerURL, externalURL); err != nil {
10✔
264
                u.ConfirmationToken = oldToken
×
265
                return errors.Wrap(err, "Error sending confirmation email")
×
266
        }
×
267
        u.ConfirmationSentAt = &now
10✔
268
        return errors.Wrap(tx.UpdateOnly(u, "confirmation_token", "confirmation_sent_at"), "Database error updating user for confirmation")
10✔
269
}
270

271
func sendInvite(tx *storage.Connection, u *models.User, mailer mailer.Mailer, referrerURL string, externalURL *url.URL, otpLength int) error {
4✔
272
        var err error
4✔
273
        oldToken := u.ConfirmationToken
4✔
274
        otp, err := crypto.GenerateOtp(otpLength)
4✔
275
        if err != nil {
4✔
276
                return err
×
277
        }
×
278
        u.ConfirmationToken = crypto.GenerateTokenHash(u.GetEmail(), otp)
4✔
279
        now := time.Now()
4✔
280
        if err := mailer.InviteMail(u, otp, referrerURL, externalURL); err != nil {
4✔
281
                u.ConfirmationToken = oldToken
×
282
                return errors.Wrap(err, "Error sending invite email")
×
283
        }
×
284
        u.InvitedAt = &now
4✔
285
        u.ConfirmationSentAt = &now
4✔
286
        return errors.Wrap(tx.UpdateOnly(u, "confirmation_token", "confirmation_sent_at", "invited_at"), "Database error updating user for invite")
4✔
287
}
288

289
func (a *API) sendPasswordRecovery(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string, externalURL *url.URL, otpLength int, flowType models.FlowType) error {
7✔
290
        var err error
7✔
291
        if u.RecoverySentAt != nil && !u.RecoverySentAt.Add(maxFrequency).Before(time.Now()) {
8✔
292
                return MaxFrequencyLimitError
1✔
293
        }
1✔
294

295
        oldToken := u.RecoveryToken
6✔
296
        otp, err := crypto.GenerateOtp(otpLength)
6✔
297
        if err != nil {
6✔
298
                return err
×
299
        }
×
300
        token := crypto.GenerateTokenHash(u.GetEmail(), otp)
6✔
301
        u.RecoveryToken = addFlowPrefixToToken(token, flowType)
6✔
302
        now := time.Now()
6✔
303
        if err := mailer.RecoveryMail(u, otp, referrerURL, externalURL); err != nil {
6✔
304
                u.RecoveryToken = oldToken
×
305
                return errors.Wrap(err, "Error sending recovery email")
×
306
        }
×
307
        u.RecoverySentAt = &now
6✔
308
        return errors.Wrap(tx.UpdateOnly(u, "recovery_token", "recovery_sent_at"), "Database error updating user for recovery")
6✔
309
}
310

311
func (a *API) sendReauthenticationOtp(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, otpLength int) error {
1✔
312
        var err error
1✔
313
        if u.ReauthenticationSentAt != nil && !u.ReauthenticationSentAt.Add(maxFrequency).Before(time.Now()) {
1✔
314
                return MaxFrequencyLimitError
×
315
        }
×
316

317
        oldToken := u.ReauthenticationToken
1✔
318
        otp, err := crypto.GenerateOtp(otpLength)
1✔
319
        if err != nil {
1✔
320
                return err
×
321
        }
×
322
        u.ReauthenticationToken = crypto.GenerateTokenHash(u.GetEmail(), otp)
1✔
323
        now := time.Now()
1✔
324
        if err := mailer.ReauthenticateMail(u, otp); err != nil {
1✔
325
                u.ReauthenticationToken = oldToken
×
326
                return errors.Wrap(err, "Error sending reauthentication email")
×
327
        }
×
328
        u.ReauthenticationSentAt = &now
1✔
329
        return errors.Wrap(tx.UpdateOnly(u, "reauthentication_token", "reauthentication_sent_at"), "Database error updating user for reauthentication")
1✔
330
}
331

332
func (a *API) sendMagicLink(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string, externalURL *url.URL, otpLength int, flowType models.FlowType) error {
1✔
333
        var err error
1✔
334
        // since Magic Link is just a recovery with a different template and behaviour
1✔
335
        // around new users we will reuse the recovery db timer to prevent potential abuse
1✔
336
        if u.RecoverySentAt != nil && !u.RecoverySentAt.Add(maxFrequency).Before(time.Now()) {
1✔
337
                return MaxFrequencyLimitError
×
338
        }
×
339
        oldToken := u.RecoveryToken
1✔
340
        otp, err := crypto.GenerateOtp(otpLength)
1✔
341
        if err != nil {
1✔
342
                return err
×
343
        }
×
344
        token := crypto.GenerateTokenHash(u.GetEmail(), otp)
1✔
345
        u.RecoveryToken = addFlowPrefixToToken(token, flowType)
1✔
346

1✔
347
        now := time.Now()
1✔
348
        if err := mailer.MagicLinkMail(u, otp, referrerURL, externalURL); err != nil {
1✔
349
                u.RecoveryToken = oldToken
×
350
                return errors.Wrap(err, "Error sending magic link email")
×
351
        }
×
352
        u.RecoverySentAt = &now
1✔
353
        return errors.Wrap(tx.UpdateOnly(u, "recovery_token", "recovery_sent_at"), "Database error updating user for recovery")
1✔
354
}
355

356
// sendEmailChange sends out an email change token to the new email.
357
func (a *API) sendEmailChange(tx *storage.Connection, config *conf.GlobalConfiguration, u *models.User, mailer mailer.Mailer, email, referrerURL string, externalURL *url.URL, otpLength int, flowType models.FlowType) error {
9✔
358
        var err error
9✔
359
        if u.EmailChangeSentAt != nil && !u.EmailChangeSentAt.Add(config.SMTP.MaxFrequency).Before(time.Now()) {
9✔
360
                return MaxFrequencyLimitError
×
361
        }
×
362
        otpNew, err := crypto.GenerateOtp(otpLength)
9✔
363
        if err != nil {
9✔
364
                return err
×
365
        }
×
366
        u.EmailChange = email
9✔
367
        token := crypto.GenerateTokenHash(u.EmailChange, otpNew)
9✔
368
        u.EmailChangeTokenNew = addFlowPrefixToToken(token, flowType)
9✔
369

9✔
370
        otpCurrent := ""
9✔
371
        if config.Mailer.SecureEmailChangeEnabled && u.GetEmail() != "" {
12✔
372
                otpCurrent, err = crypto.GenerateOtp(otpLength)
3✔
373
                if err != nil {
3✔
374
                        return err
×
375
                }
×
376
                currentToken := crypto.GenerateTokenHash(u.GetEmail(), otpCurrent)
3✔
377
                u.EmailChangeTokenCurrent = addFlowPrefixToToken(currentToken, flowType)
3✔
378
        }
379

380
        u.EmailChangeConfirmStatus = zeroConfirmation
9✔
381
        now := time.Now()
9✔
382
        if err := mailer.EmailChangeMail(u, otpNew, otpCurrent, referrerURL, externalURL); err != nil {
9✔
383
                return err
×
384
        }
×
385

386
        u.EmailChangeSentAt = &now
9✔
387
        return errors.Wrap(tx.UpdateOnly(
9✔
388
                u,
9✔
389
                "email_change_token_current",
9✔
390
                "email_change_token_new",
9✔
391
                "email_change",
9✔
392
                "email_change_sent_at",
9✔
393
                "email_change_confirm_status",
9✔
394
        ), "Database error updating user for email change")
9✔
395
}
396

397
func validateEmail(email string) (string, error) {
67✔
398
        if email == "" {
67✔
399
                return "", unprocessableEntityError("An email address is required")
×
400
        }
×
401
        if err := checkmail.ValidateFormat(email); err != nil {
67✔
402
                return "", unprocessableEntityError("Unable to validate email address: " + err.Error())
×
403
        }
×
404
        return strings.ToLower(email), nil
67✔
405
}
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