• 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

68.26
/internal/api/otp.go
1
package api
2

3
import (
4
        "bytes"
5
        "encoding/json"
6
        "io"
7
        "net/http"
8

9
        "github.com/sethvargo/go-password/password"
10
        "github.com/supabase/auth/internal/api/sms_provider"
11
        "github.com/supabase/auth/internal/models"
12
        "github.com/supabase/auth/internal/storage"
13
)
14

15
// OtpParams contains the request body params for the otp endpoint
16
type OtpParams struct {
17
        Email               string                 `json:"email"`
18
        Phone               string                 `json:"phone"`
19
        CreateUser          bool                   `json:"create_user"`
20
        Data                map[string]interface{} `json:"data"`
21
        Channel             string                 `json:"channel"`
22
        CodeChallengeMethod string                 `json:"code_challenge_method"`
23
        CodeChallenge       string                 `json:"code_challenge"`
24
}
25

26
// SmsParams contains the request body params for sms otp
27
type SmsParams struct {
28
        Phone               string                 `json:"phone"`
29
        Channel             string                 `json:"channel"`
30
        Data                map[string]interface{} `json:"data"`
31
        CodeChallengeMethod string                 `json:"code_challenge_method"`
32
        CodeChallenge       string                 `json:"code_challenge"`
33
}
34

35
func (p *OtpParams) Validate() error {
15✔
36
        if p.Email != "" && p.Phone != "" {
16✔
37
                return badRequestError(ErrorCodeValidationFailed, "Only an email address or phone number should be provided")
1✔
38
        }
1✔
39
        if p.Email != "" && p.Channel != "" {
14✔
40
                return badRequestError(ErrorCodeValidationFailed, "Channel should only be specified with Phone OTP")
×
41
        }
×
42
        if err := validatePKCEParams(p.CodeChallengeMethod, p.CodeChallenge); err != nil {
16✔
43
                return err
2✔
44
        }
2✔
45
        return nil
12✔
46
}
47

48
func (p *SmsParams) Validate(smsProvider string) error {
6✔
49
        if p.Phone != "" && !sms_provider.IsValidMessageChannel(p.Channel, smsProvider) {
7✔
50
                return badRequestError(ErrorCodeValidationFailed, InvalidChannelError)
1✔
51
        }
1✔
52

53
        var err error
5✔
54
        p.Phone, err = validatePhone(p.Phone)
5✔
55
        if err != nil {
5✔
56
                return err
×
57
        }
×
58

59
        return nil
5✔
60
}
61

62
// Otp returns the MagicLink or SmsOtp handler based on the request body params
63
func (a *API) Otp(w http.ResponseWriter, r *http.Request) error {
15✔
64
        params := &OtpParams{
15✔
65
                CreateUser: true,
15✔
66
        }
15✔
67
        if params.Data == nil {
30✔
68
                params.Data = make(map[string]interface{})
15✔
69
        }
15✔
70

71
        if err := retrieveRequestParams(r, params); err != nil {
15✔
72
                return err
×
73
        }
×
74

75
        if err := params.Validate(); err != nil {
18✔
76
                return err
3✔
77
        }
3✔
78
        if params.Data == nil {
16✔
79
                params.Data = make(map[string]interface{})
4✔
80
        }
4✔
81

82
        if ok, err := a.shouldCreateUser(r, params); !ok {
13✔
83
                return unprocessableEntityError(ErrorCodeOTPDisabled, "Signups not allowed for otp")
1✔
84
        } else if err != nil {
12✔
85
                return err
×
86
        }
×
87

88
        if params.Email != "" {
16✔
89
                return a.MagicLink(w, r)
5✔
90
        } else if params.Phone != "" {
17✔
91
                return a.SmsOtp(w, r)
6✔
92
        }
6✔
93

94
        return badRequestError(ErrorCodeValidationFailed, "One of email or phone must be set")
×
95
}
96

97
type SmsOtpResponse struct {
98
        MessageID string `json:"message_id,omitempty"`
99
}
100

101
// SmsOtp sends the user an otp via sms
102
func (a *API) SmsOtp(w http.ResponseWriter, r *http.Request) error {
6✔
103
        ctx := r.Context()
6✔
104
        db := a.db.WithContext(ctx)
6✔
105
        config := a.config
6✔
106

6✔
107
        if !config.External.Phone.Enabled {
6✔
108
                return badRequestError(ErrorCodePhoneProviderDisabled, "Unsupported phone provider")
×
109
        }
×
110
        var err error
6✔
111

6✔
112
        params := &SmsParams{}
6✔
113
        if err := retrieveRequestParams(r, params); err != nil {
6✔
114
                return err
×
115
        }
×
116

117
        // For backwards compatibility, we default to SMS if params Channel is not specified
118
        if params.Phone != "" && params.Channel == "" {
11✔
119
                params.Channel = sms_provider.SMSProvider
5✔
120
        }
5✔
121

122
        if err := params.Validate(config.Sms.Provider); err != nil {
7✔
123
                return err
1✔
124
        }
1✔
125

126
        var isNewUser bool
5✔
127
        aud := a.requestAud(ctx, r)
5✔
128
        user, err := models.FindUserByPhoneAndAudience(db, params.Phone, aud)
5✔
129
        if err != nil {
6✔
130
                if models.IsNotFoundError(err) {
2✔
131
                        isNewUser = true
1✔
132
                } else {
1✔
133
                        return internalServerError("Database error finding user").WithInternalError(err)
×
134
                }
×
135
        }
136
        if user != nil {
9✔
137
                isNewUser = !user.IsPhoneConfirmed()
4✔
138
        }
4✔
139
        if isNewUser {
6✔
140
                // User either doesn't exist or hasn't completed the signup process.
1✔
141
                // Sign them up with temporary password.
1✔
142
                password, err := password.Generate(64, 10, 1, false, true)
1✔
143
                if err != nil {
1✔
144
                        return internalServerError("error creating user").WithInternalError(err)
×
145
                }
×
146

147
                signUpParams := &SignupParams{
1✔
148
                        Phone:    params.Phone,
1✔
149
                        Password: password,
1✔
150
                        Data:     params.Data,
1✔
151
                        Channel:  params.Channel,
1✔
152
                }
1✔
153
                newBodyContent, err := json.Marshal(signUpParams)
1✔
154
                if err != nil {
1✔
155
                        // SignupParams must be marshallable
×
156
                        panic(err)
×
157
                }
158
                r.Body = io.NopCloser(bytes.NewReader(newBodyContent))
1✔
159

1✔
160
                fakeResponse := &responseStub{}
1✔
161

1✔
162
                if config.Sms.Autoconfirm {
1✔
163
                        // signups are autoconfirmed, send otp after signup
×
164
                        if err := a.Signup(fakeResponse, r); err != nil {
×
165
                                return err
×
166
                        }
×
167

168
                        signUpParams := &SignupParams{
×
169
                                Phone:   params.Phone,
×
170
                                Channel: params.Channel,
×
171
                        }
×
172
                        newBodyContent, err := json.Marshal(signUpParams)
×
173
                        if err != nil {
×
174
                                // SignupParams must be marshallable
×
175
                                panic(err)
×
176
                        }
177
                        r.Body = io.NopCloser(bytes.NewReader(newBodyContent))
×
178
                        return a.SmsOtp(w, r)
×
179
                }
180

181
                if err := a.Signup(fakeResponse, r); err != nil {
2✔
182
                        return err
1✔
183
                }
1✔
184
                return sendJSON(w, http.StatusOK, make(map[string]string))
×
185
        }
186

187
        messageID := ""
4✔
188
        err = db.Transaction(func(tx *storage.Connection) error {
8✔
189
                if err := models.NewAuditLogEntry(r, tx, user, models.UserRecoveryRequestedAction, "", map[string]interface{}{
4✔
190
                        "channel": params.Channel,
4✔
191
                }); err != nil {
4✔
192
                        return err
×
193
                }
×
194
                smsProvider, terr := sms_provider.GetSmsProvider(*config)
4✔
195
                if terr != nil {
8✔
196
                        return internalServerError("Unable to get SMS provider").WithInternalError(err)
4✔
197
                }
4✔
NEW
198
                mID, serr := a.sendPhoneConfirmation(r, tx, user, params.Phone, phoneConfirmationOtp, smsProvider, params.Channel)
×
199
                if serr != nil {
×
200
                        return badRequestError(ErrorCodeSMSSendFailed, "Error sending sms OTP: %v", serr).WithInternalError(serr)
×
201
                }
×
202
                messageID = mID
×
203
                return nil
×
204
        })
205

206
        if err != nil {
8✔
207
                return err
4✔
208
        }
4✔
209

210
        return sendJSON(w, http.StatusOK, SmsOtpResponse{
×
211
                MessageID: messageID,
×
212
        })
×
213
}
214

215
func (a *API) shouldCreateUser(r *http.Request, params *OtpParams) (bool, error) {
12✔
216
        ctx := r.Context()
12✔
217
        db := a.db.WithContext(ctx)
12✔
218

12✔
219
        if !params.CreateUser {
13✔
220
                ctx := r.Context()
1✔
221
                aud := a.requestAud(ctx, r)
1✔
222
                var err error
1✔
223
                if params.Email != "" {
2✔
224
                        params.Email, err = validateEmail(params.Email)
1✔
225
                        if err != nil {
1✔
226
                                return false, err
×
227
                        }
×
228
                        _, err = models.FindUserByEmailAndAudience(db, params.Email, aud)
1✔
229
                } else if params.Phone != "" {
×
230
                        params.Phone, err = validatePhone(params.Phone)
×
231
                        if err != nil {
×
232
                                return false, err
×
233
                        }
×
234
                        _, err = models.FindUserByPhoneAndAudience(db, params.Phone, aud)
×
235
                }
236

237
                if err != nil && models.IsNotFoundError(err) {
2✔
238
                        return false, nil
1✔
239
                }
1✔
240
        }
241
        return true, nil
11✔
242
}
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