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

vocdoni / saas-backend / 21707633339

04 Feb 2026 05:32PM UTC coverage: 63.493% (+0.8%) from 62.657%
21707633339

Pull #412

github

emmdim
fix(csp): remove cooldown time from auth-only token
Pull Request #412: chore: merge v.2.3 to stage

460 of 615 new or added lines in 23 files covered. (74.8%)

9 existing lines in 7 files now uncovered.

7148 of 11258 relevant lines covered (63.49%)

40.25 hits per line

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

41.0
/api/users.go
1
package api
2

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

9
        "github.com/vocdoni/saas-backend/api/apicommon"
10
        "github.com/vocdoni/saas-backend/db"
11
        "github.com/vocdoni/saas-backend/errors"
12
        "github.com/vocdoni/saas-backend/internal"
13
        "github.com/vocdoni/saas-backend/notifications/mailtemplates"
14
        "go.vocdoni.io/dvote/log"
15
)
16

17
// registerHandler godoc
18
//
19
//        @Summary                Register a new user
20
//        @Description        Register a new user with email, password, and personal information
21
//        @Tags                        users
22
//        @Accept                        json
23
//        @Produce                json
24
//        @Param                        request        body                apicommon.UserInfo        true        "User registration information"
25
//        @Success                200                {string}        string                                "OK"
26
//        @Failure                400                {object}        errors.Error                "Invalid input data"
27
//        @Failure                409                {object}        errors.Error                "User already exists"
28
//        @Failure                500                {object}        errors.Error                "Internal server error"
29
//        @Router                        /users [post]
30
func (a *API) registerHandler(w http.ResponseWriter, r *http.Request) {
84✔
31
        userInfo := &apicommon.UserInfo{}
84✔
32
        body, err := io.ReadAll(r.Body)
84✔
33
        if err != nil {
84✔
34
                errors.ErrMalformedBody.Write(w)
×
35
                return
×
36
        }
×
37
        if err := json.Unmarshal(body, userInfo); err != nil {
85✔
38
                errors.ErrMalformedBody.Write(w)
1✔
39
                return
1✔
40
        }
1✔
41
        // check the email is correct format
42
        if !internal.ValidEmail(userInfo.Email) {
85✔
43
                errors.ErrEmailMalformed.Write(w)
2✔
44
                return
2✔
45
        }
2✔
46
        // check the password is correct format
47
        if len(userInfo.Password) < 8 {
83✔
48
                errors.ErrPasswordTooShort.Write(w)
2✔
49
                return
2✔
50
        }
2✔
51
        // check the first name is not empty
52
        if userInfo.FirstName == "" {
80✔
53
                errors.ErrMalformedBody.Withf("first name is empty").Write(w)
1✔
54
                return
1✔
55
        }
1✔
56
        // check the last name is not empty
57
        if userInfo.LastName == "" {
79✔
58
                errors.ErrMalformedBody.Withf("last name is empty").Write(w)
1✔
59
                return
1✔
60
        }
1✔
61
        // hash the password
62
        hPassword := internal.HexHashPassword(passwordSalt, userInfo.Password)
77✔
63
        // add the user to the database
77✔
64
        userID, err := a.db.SetUser(&db.User{
77✔
65
                Email:     userInfo.Email,
77✔
66
                FirstName: userInfo.FirstName,
77✔
67
                LastName:  userInfo.LastName,
77✔
68
                Password:  hPassword,
77✔
69
        })
77✔
70
        if err != nil {
78✔
71
                if err == db.ErrAlreadyExists {
2✔
72
                        errors.ErrDuplicateConflict.With("user already exists").Write(w)
1✔
73
                        return
1✔
74
                }
1✔
75
                log.Warnw("could not create user", "error", err)
×
76
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
77
                return
×
78
        }
79
        // compose the new user and send the verification code
80
        newUser := &db.User{
76✔
81
                ID:        userID,
76✔
82
                Email:     userInfo.Email,
76✔
83
                FirstName: userInfo.FirstName,
76✔
84
                LastName:  userInfo.LastName,
76✔
85
        }
76✔
86
        // generate a new verification code
76✔
87
        code, link, err := a.generateVerificationCodeAndLink(newUser, db.CodeTypeVerifyAccount)
76✔
88
        if err != nil {
76✔
89
                log.Warnw("could not generate verification code", "error", err)
×
90
                errors.ErrGenericInternalServerError.Write(w)
×
91
                return
×
92
        }
×
93
        // send the verification mail to the user email with the verification code
94
        // and the verification link
95
        if err := a.sendMail(r.Context(), userInfo.Email,
76✔
96
                mailtemplates.VerifyAccountNotification, struct {
76✔
97
                        Code string
76✔
98
                        Link string
76✔
99
                }{code, link},
76✔
100
        ); err != nil {
76✔
101
                log.Warnw("could not send verification code", "error", err)
×
102
                errors.ErrGenericInternalServerError.Write(w)
×
103
                return
×
104
        }
×
105
        // send the token back to the user
106
        apicommon.HTTPWriteOK(w)
76✔
107
}
108

109
// verifyUserAccountHandler godoc
110
//
111
//        @Summary                Verify user account
112
//        @Description        Verify a user account with the verification code
113
//        @Tags                        users
114
//        @Accept                        json
115
//        @Produce                json
116
//        @Param                        request        body                apicommon.UserVerification        true        "Verification information"
117
//        @Success                200                {object}        apicommon.LoginResponse
118
//        @Failure                400                {object}        errors.Error        "Invalid input data"
119
//        @Failure                401                {object}        errors.Error        "Unauthorized"
120
//        @Failure                409                {object}        errors.Error        "User already verified"
121
//        @Failure                410                {object}        errors.Error        "Verification code expired"
122
//        @Failure                500                {object}        errors.Error        "Internal server error"
123
//        @Router                        /users/verify [post]
124
func (a *API) verifyUserAccountHandler(w http.ResponseWriter, r *http.Request) {
69✔
125
        verification := &apicommon.UserVerification{}
69✔
126
        if err := json.NewDecoder(r.Body).Decode(verification); err != nil {
69✔
127
                errors.ErrMalformedBody.Write(w)
×
128
                return
×
129
        }
×
130
        // check the email and verification code are not empty only if the mail
131
        // service is available
132
        if a.mail != nil && (verification.Code == "" || verification.Email == "") {
69✔
133
                errors.ErrInvalidUserData.With("no verification code or email provided").Write(w)
×
134
                return
×
135
        }
×
136
        // get the user information from the database by email
137
        user, err := a.db.UserByEmail(verification.Email)
69✔
138
        if err != nil {
69✔
139
                if err == db.ErrNotFound {
×
140
                        errors.ErrUnauthorized.Write(w)
×
141
                        return
×
142
                }
×
143
                errors.ErrGenericInternalServerError.Write(w)
×
144
                return
×
145
        }
146
        // check the user is not already verified
147
        if user.Verified {
69✔
148
                errors.ErrUserAlreadyVerified.Write(w)
×
149
                return
×
150
        }
×
151
        // get the userVerification from the database
152
        userVerification, err := a.db.UserVerificationCode(user, db.CodeTypeVerifyAccount)
69✔
153
        if err != nil {
69✔
154
                if err != db.ErrNotFound {
×
155
                        log.Warnw("could not get verification code", "error", err)
×
156
                }
×
157
                errors.ErrUnauthorized.Write(w)
×
158
                return
×
159
        }
160
        // check the verification code is not expired
161
        if userVerification.Expiration.Before(time.Now()) {
69✔
162
                errors.ErrVerificationCodeExpired.Write(w)
×
163
                return
×
164
        }
×
165
        // check the verification code is correct
166
        code, err := internal.OpenToken(userVerification.SealedCode, verification.Email, a.secret)
69✔
167
        if err != nil {
69✔
168
                errors.ErrGenericInternalServerError.Write(w)
×
169
                return
×
170
        }
×
171
        if code != verification.Code {
69✔
172
                errors.ErrUnauthorized.With("code mismatch").Write(w)
×
173
                return
×
174
        }
×
175
        // verify the user account if the current verification code is valid and
176
        // matches with the provided one
177
        if err := a.db.VerifyUserAccount(user); err != nil {
69✔
178
                errors.ErrGenericInternalServerError.Write(w)
×
179
                return
×
180
        }
×
181
        // generate a new token with the user name as the subject
182
        res, err := a.buildLoginResponse(user.Email)
69✔
183
        if err != nil {
69✔
184
                errors.ErrGenericInternalServerError.Write(w)
×
185
                return
×
186
        }
×
187
        // send the token back to the user
188
        apicommon.HTTPWriteJSON(w, res)
69✔
189
}
190

191
// userVerificationCodeInfoHandler godoc
192
//
193
//        @Summary                Get verification code information
194
//        @Description        Get information about a user's verification code
195
//        @Tags                        users
196
//        @Accept                        json
197
//        @Produce                json
198
//        @Param                        email        query                string        true        "User email"
199
//        @Success                200                {object}        apicommon.UserVerification
200
//        @Failure                400                {object}        errors.Error        "Invalid input data"
201
//        @Failure                401                {object}        errors.Error        "Unauthorized"
202
//        @Failure                404                {object}        errors.Error        "User not found"
203
//        @Failure                409                {object}        errors.Error        "User already verified"
204
//        @Failure                500                {object}        errors.Error        "Internal server error"
205
//        @Router                        /users/verify/code [get]
206
func (a *API) userVerificationCodeInfoHandler(w http.ResponseWriter, r *http.Request) {
×
207
        // get the user email of the user from the request query
×
208
        userEmail := r.URL.Query().Get("email")
×
209
        // check the email is not empty
×
210
        if userEmail == "" {
×
211
                errors.ErrInvalidUserData.With("no email provided").Write(w)
×
212
                return
×
213
        }
×
214
        var err error
×
215
        var user *db.User
×
216
        // get the user information from the database by email
×
217
        user, err = a.db.UserByEmail(userEmail)
×
218
        if err != nil {
×
219
                if err == db.ErrNotFound {
×
220
                        errors.ErrUserNotFound.Write(w)
×
221
                        return
×
222
                }
×
223
                errors.ErrGenericInternalServerError.Write(w)
×
224
                return
×
225
        }
226
        // check if the user is already verified
227
        if user.Verified {
×
228
                errors.ErrUserAlreadyVerified.Write(w)
×
229
                return
×
230
        }
×
231
        // get the userVerification from the database
232
        userVerification, err := a.db.UserVerificationCode(user, db.CodeTypeVerifyAccount)
×
233
        if err != nil {
×
234
                if err != db.ErrNotFound {
×
235
                        log.Warnw("could not get verification code", "error", err)
×
236
                }
×
237
                errors.ErrUnauthorized.Write(w)
×
238
                return
×
239
        }
240
        // return the verification code information
241
        apicommon.HTTPWriteJSON(w, apicommon.UserVerification{
×
242
                Email:      user.Email,
×
243
                Expiration: userVerification.Expiration,
×
244
                Valid:      userVerification.Expiration.After(time.Now()),
×
245
        })
×
246
}
247

248
// resendUserVerificationCodeHandler godoc
249
//
250
//        @Summary                Resend verification code
251
//        @Description        Resend a verification code to the user's email
252
//        @Tags                        users
253
//        @Accept                        json
254
//        @Produce                json
255
//        @Param                        request        body                apicommon.UserVerification        true        "User email information"
256
//        @Success                200                {string}        string                                                "OK"
257
//        @Failure                400                {object}        errors.Error                                "Invalid input data, user already verified, or max resend attempts reached"
258
//        @Failure                401                {object}        errors.Error                                "Unauthorized"
259
//        @Failure                500                {object}        errors.Error                                "Internal server error"
260
//        @Router                        /users/verify/code [post]
261
func (a *API) resendUserVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
8✔
262
        verification := &apicommon.UserVerification{}
8✔
263
        if err := json.NewDecoder(r.Body).Decode(verification); err != nil {
9✔
264
                errors.ErrMalformedBody.Write(w)
1✔
265
                return
1✔
266
        }
1✔
267
        // check the email is not empty
268
        if verification.Email == "" {
8✔
269
                errors.ErrInvalidUserData.With("no email provided").Write(w)
1✔
270
                return
1✔
271
        }
1✔
272
        // get the user information from the database by email
273
        user, err := a.db.UserByEmail(verification.Email)
6✔
274
        if err != nil {
7✔
275
                if err == db.ErrNotFound {
2✔
276
                        errors.ErrUnauthorized.Write(w)
1✔
277
                        return
1✔
278
                }
1✔
279
                errors.ErrGenericInternalServerError.Write(w)
×
280
                return
×
281
        }
282
        // check the user is not already verified
283
        if user.Verified {
6✔
284
                errors.ErrUserAlreadyVerified.Write(w)
1✔
285
                return
1✔
286
        }
1✔
287
        // get the verification userVerification from the database
288
        userVerification, err := a.db.UserVerificationCode(user, db.CodeTypeVerifyAccount)
4✔
289
        if err != nil {
4✔
290
                if err != db.ErrNotFound {
×
291
                        log.Warnw("could not get verification code", "error", err)
×
292
                }
×
293
                errors.ErrUnauthorized.Write(w)
×
294
                return
×
295
        }
296
        // if the verification code is not expired
297
        if userVerification.Expiration.After(time.Now()) {
7✔
298
                // check if the maximum number of attempts has been reached for resending
3✔
299
                if userVerification.Attempts >= apicommon.VerificationCodeMaxAttempts {
4✔
300
                        errors.ErrVerificationMaxAttempts.WithData(apicommon.UserVerification{
1✔
301
                                Expiration: userVerification.Expiration,
1✔
302
                        }).Write(w)
1✔
303
                        return
1✔
304
                }
1✔
305
                code, err := internal.OpenToken(userVerification.SealedCode, user.Email, a.secret)
2✔
306
                if err != nil {
2✔
307
                        errors.ErrGenericInternalServerError.Write(w)
×
308
                        return
×
309
                }
×
310
                link, err := a.generateVerificationLink(user, code)
2✔
311
                if err != nil {
2✔
312
                        log.Warnw("could not generate verification link", "error", err)
×
313
                        errors.ErrGenericInternalServerError.Write(w)
×
314
                        return
×
315
                }
×
316
                // resend the existing verification code
317
                if err := a.sendMail(r.Context(), user.Email, mailtemplates.VerifyAccountNotification,
2✔
318
                        struct {
2✔
319
                                Code string
2✔
320
                                Link string
2✔
321
                        }{code, link},
2✔
322
                ); err != nil {
2✔
323
                        log.Warnw("could not resend verification code", "error", err)
×
324
                        errors.ErrGenericInternalServerError.Write(w)
×
325
                        return
×
326
                }
×
327
                if err = a.db.VerificationCodeIncrementAttempts(userVerification.SealedCode, db.CodeTypeVerifyAccount); err != nil {
2✔
328
                        log.Warnw("could not increment verification code attempts", "error", err)
×
329
                        errors.ErrGenericInternalServerError.Write(w)
×
330
                        return
×
331
                }
×
332
                // return the verification code information
333
                apicommon.HTTPWriteJSON(w, apicommon.UserVerification{
2✔
334
                        Expiration: userVerification.Expiration,
2✔
335
                })
2✔
336
                return
2✔
337
        }
338

339
        // generate a new verification code
340
        newCode, link, err := a.generateVerificationCodeAndLink(user, db.CodeTypeVerifyAccount)
1✔
341
        if err != nil {
1✔
342
                log.Warnw("could not generate verification code", "error", err)
×
343
                errors.ErrGenericInternalServerError.Write(w)
×
344
                return
×
345
        }
×
346
        // send the verification mail to the user email with the verification code
347
        // and the verification link
348
        if err := a.sendMail(r.Context(), user.Email, mailtemplates.VerifyAccountNotification,
1✔
349
                struct {
1✔
350
                        Code string
1✔
351
                        Link string
1✔
352
                }{newCode, link},
1✔
353
        ); err != nil {
1✔
354
                log.Warnw("could not send verification code", "error", err)
×
355
                errors.ErrGenericInternalServerError.Write(w)
×
356
                return
×
357
        }
×
358
        apicommon.HTTPWriteOK(w)
1✔
359
}
360

361
// userInfoHandler godoc
362
//
363
//        @Summary                Get user information
364
//        @Description        Get information about the authenticated user
365
//        @Tags                        users
366
//        @Accept                        json
367
//        @Produce                json
368
//        @Security                BearerAuth
369
//        @Success                200        {object}        apicommon.UserInfo
370
//        @Failure                401        {object}        errors.Error        "Unauthorized"
371
//        @Failure                500        {object}        errors.Error        "Internal server error"
372
//        @Router                        /users/me [get]
373
func (a *API) userInfoHandler(w http.ResponseWriter, r *http.Request) {
80✔
374
        user, ok := apicommon.UserFromContext(r.Context())
80✔
375
        if !ok {
80✔
376
                errors.ErrUnauthorized.Write(w)
×
377
                return
×
378
        }
×
379
        // get the user organizations information from the database if any
380
        userOrgs := make([]*apicommon.UserOrganization, 0)
80✔
381
        for _, orgInfo := range user.Organizations {
84✔
382
                org, parent, err := a.db.OrganizationWithParent(orgInfo.Address)
4✔
383
                if err != nil {
4✔
384
                        if err == db.ErrNotFound {
×
385
                                continue
×
386
                        }
387
                        errors.ErrGenericInternalServerError.Write(w)
×
388
                        return
×
389
                }
390
                userOrgs = append(userOrgs, &apicommon.UserOrganization{
4✔
391
                        Role:         string(orgInfo.Role),
4✔
392
                        Organization: apicommon.OrganizationFromDB(org, parent),
4✔
393
                })
4✔
394
        }
395
        // extract the list of linked OAuth providers
396
        providers := make([]string, 0, len(user.OAuth))
80✔
397
        for provider := range user.OAuth {
80✔
NEW
398
                providers = append(providers, provider)
×
NEW
399
        }
×
400
        // return the user information
401
        apicommon.HTTPWriteJSON(w, apicommon.UserInfo{
80✔
402
                ID:            user.ID,
80✔
403
                Email:         user.Email,
80✔
404
                FirstName:     user.FirstName,
80✔
405
                LastName:      user.LastName,
80✔
406
                Verified:      user.Verified,
80✔
407
                HasPassword:   user.Password != "",
80✔
408
                Providers:     providers,
80✔
409
                Organizations: userOrgs,
80✔
410
        })
80✔
411
}
412

413
// updateUserInfoHandler godoc
414
//
415
//        @Summary                Update user information
416
//        @Description        Update information for the authenticated user
417
//        @Tags                        users
418
//        @Accept                        json
419
//        @Produce                json
420
//        @Security                BearerAuth
421
//        @Param                        request        body                apicommon.UserInfo        true        "User information to update"
422
//        @Success                200                {object}        apicommon.LoginResponse
423
//        @Failure                400                {object}        errors.Error        "Invalid input data"
424
//        @Failure                401                {object}        errors.Error        "Unauthorized"
425
//        @Failure                500                {object}        errors.Error        "Internal server error"
426
//        @Router                        /users/me [put]
427
func (a *API) updateUserInfoHandler(w http.ResponseWriter, r *http.Request) {
×
428
        user, ok := apicommon.UserFromContext(r.Context())
×
429
        if !ok {
×
430
                errors.ErrUnauthorized.Write(w)
×
431
                return
×
432
        }
×
433
        userInfo := &apicommon.UserInfo{}
×
434
        if err := json.NewDecoder(r.Body).Decode(userInfo); err != nil {
×
435
                errors.ErrMalformedBody.Write(w)
×
436
                return
×
437
        }
×
438
        // create a flag to check if the user information has changed and needs to
439
        // be updated and store the current email to check if it has changed
440
        // specifically
441
        updateUser := false
×
442
        currentEmail := user.Email
×
443
        // check the email is correct format if it is not empty
×
444
        if userInfo.Email != "" {
×
445
                if !internal.ValidEmail(userInfo.Email) {
×
446
                        errors.ErrEmailMalformed.Write(w)
×
447
                        return
×
448
                }
×
449
                // update the user email and set the flag to true to update the user
450
                // info
451
                user.Email = userInfo.Email
×
452
                updateUser = true
×
453
        }
454
        // check the first name is not empty
455
        if userInfo.FirstName != "" {
×
456
                // update the user first name and set the flag to true to update the
×
457
                // user info
×
458
                user.FirstName = userInfo.FirstName
×
459
                updateUser = true
×
460
        }
×
461
        // check the last name is not empty
462
        if userInfo.LastName != "" {
×
463
                // update the user last name and set the flag to true to update the
×
464
                // user info
×
465
                user.LastName = userInfo.LastName
×
466
                updateUser = true
×
467
        }
×
468
        // update the user information if needed
469
        if updateUser {
×
470
                if _, err := a.db.SetUser(user); err != nil {
×
471
                        log.Warnw("could not update user", "error", err)
×
472
                        errors.ErrGenericInternalServerError.Write(w)
×
473
                        return
×
474
                }
×
475
                // if user email has changed, update the creator email in the
476
                // organizations where the user is creator
477
                if user.Email != currentEmail {
×
478
                        if err := a.db.ReplaceCreatorEmail(currentEmail, user.Email); err != nil {
×
479
                                // revert the user update if the creator email update fails
×
480
                                user.Email = currentEmail
×
481
                                if _, err := a.db.SetUser(user); err != nil {
×
482
                                        log.Warnw("could not revert user update", "error", err)
×
483
                                }
×
484
                                // return an error
485
                                errors.ErrGenericInternalServerError.Write(w)
×
486
                                return
×
487
                        }
488
                }
489
        }
490
        // generate a new token with the new user email as the subject
491
        res, err := a.buildLoginResponse(user.Email)
×
492
        if err != nil {
×
493
                errors.ErrGenericInternalServerError.Write(w)
×
494
                return
×
495
        }
×
496
        apicommon.HTTPWriteJSON(w, res)
×
497
}
498

499
// updateUserPasswordHandler godoc
500
//
501
//        @Summary                Update user password
502
//        @Description        Update the password for the authenticated user
503
//        @Tags                        users
504
//        @Accept                        json
505
//        @Produce                json
506
//        @Security                BearerAuth
507
//        @Param                        request        body                apicommon.UserPasswordUpdate        true        "Password update information"
508
//        @Success                200                {string}        string                                                        "OK"
509
//        @Failure                400                {object}        errors.Error                                        "Invalid input data"
510
//        @Failure                401                {object}        errors.Error                                        "Unauthorized or old password does not match"
511
//        @Failure                500                {object}        errors.Error                                        "Internal server error"
512
//        @Router                        /users/password [put]
513
func (a *API) updateUserPasswordHandler(w http.ResponseWriter, r *http.Request) {
×
514
        user, ok := apicommon.UserFromContext(r.Context())
×
515
        if !ok {
×
516
                errors.ErrUnauthorized.Write(w)
×
517
                return
×
518
        }
×
519
        // check if user is OAuth-only (no password set)
NEW
520
        if user.Password == "" {
×
NEW
521
                errors.ErrOAuthUserCannotUsePasswordRecovery.Write(w)
×
NEW
522
                return
×
NEW
523
        }
×
524
        userPasswords := &apicommon.UserPasswordUpdate{}
×
525
        if err := json.NewDecoder(r.Body).Decode(userPasswords); err != nil {
×
526
                errors.ErrMalformedBody.Write(w)
×
527
                return
×
528
        }
×
529
        // check the password is correct format
530
        if len(userPasswords.NewPassword) < 8 {
×
531
                errors.ErrPasswordTooShort.Write(w)
×
532
                return
×
533
        }
×
534
        // hash the password the old password to compare it with the stored one
535
        hOldPassword := internal.HexHashPassword(passwordSalt, userPasswords.OldPassword)
×
536
        if hOldPassword != user.Password {
×
537
                errors.ErrUnauthorized.Withf("old password does not match").Write(w)
×
538
                return
×
539
        }
×
540
        // hash and update the new password
541
        user.Password = internal.HexHashPassword(passwordSalt, userPasswords.NewPassword)
×
542
        if _, err := a.db.SetUser(user); err != nil {
×
543
                log.Warnw("could not update user password", "error", err)
×
544
                errors.ErrGenericInternalServerError.Write(w)
×
545
                return
×
546
        }
×
547
        apicommon.HTTPWriteOK(w)
×
548
}
549

550
// recoverUserPasswordHandler godoc
551
//
552
//        @Summary                Recover user password
553
//        @Description        Request a password recovery code for a user
554
//        @Tags                        users
555
//        @Accept                        json
556
//        @Produce                json
557
//        @Param                        request        body                apicommon.UserInfo        true        "User email information"
558
//        @Success                200                {string}        string                                "OK"
559
//        @Failure                400                {object}        errors.Error                "Invalid input data"
560
//        @Failure                500                {object}        errors.Error                "Internal server error"
561
//        @Router                        /users/recovery [post]
562
func (a *API) recoverUserPasswordHandler(w http.ResponseWriter, r *http.Request) {
1✔
563
        // get the user info from the request body
1✔
564
        userInfo := &apicommon.UserInfo{}
1✔
565
        if err := json.NewDecoder(r.Body).Decode(userInfo); err != nil {
1✔
566
                errors.ErrMalformedBody.Write(w)
×
567
                return
×
568
        }
×
569
        // get the user information from the database by email
570
        user, err := a.db.UserByEmail(userInfo.Email)
1✔
571
        if err != nil {
1✔
572
                if err == db.ErrNotFound {
×
573
                        // do not return an error if the user is not found to avoid
×
574
                        // information leakage
×
575
                        apicommon.HTTPWriteOK(w)
×
576
                        return
×
577
                }
×
578
                errors.ErrGenericInternalServerError.Write(w)
×
579
                return
×
580
        }
581
        // check the user is verified
582
        if user.Verified {
2✔
583
                // generate a new verification code
1✔
584
                code, link, err := a.generateVerificationCodeAndLink(user, db.CodeTypePasswordReset)
1✔
585
                if err != nil {
1✔
586
                        log.Warnw("could not generate verification code", "error", err)
×
587
                        errors.ErrGenericInternalServerError.Write(w)
×
588
                        return
×
589
                }
×
590
                // send the password reset mail to the user email with the verification
591
                // code and the verification link
592
                if err := a.sendMail(r.Context(), user.Email, mailtemplates.PasswordResetNotification,
1✔
593
                        struct {
1✔
594
                                Code string
1✔
595
                                Link string
1✔
596
                        }{code, link},
1✔
597
                ); err != nil {
1✔
598
                        log.Warnw("could not send reset passworod code", "error", err)
×
599
                        errors.ErrGenericInternalServerError.Write(w)
×
600
                        return
×
601
                }
×
602
        }
603
        apicommon.HTTPWriteOK(w)
1✔
604
}
605

606
// resetUserPasswordHandler godoc
607
//
608
//        @Summary                Reset user password
609
//        @Description        Reset a user's password using a verification code
610
//        @Tags                        users
611
//        @Accept                        json
612
//        @Produce                json
613
//        @Param                        request        body                apicommon.UserPasswordReset        true        "Password reset information"
614
//        @Success                200                {string}        string                                                "OK"
615
//        @Failure                400                {object}        errors.Error                                "Invalid input data"
616
//        @Failure                401                {object}        errors.Error                                "Unauthorized or invalid verification code"
617
//        @Failure                500                {object}        errors.Error                                "Internal server error"
618
//        @Router                        /users/reset [post]
619
func (a *API) resetUserPasswordHandler(w http.ResponseWriter, r *http.Request) {
1✔
620
        userPasswords := &apicommon.UserPasswordReset{}
1✔
621
        if err := json.NewDecoder(r.Body).Decode(userPasswords); err != nil {
1✔
622
                errors.ErrMalformedBody.Write(w)
×
623
                return
×
624
        }
×
625
        // check the password is correct format
626
        if len(userPasswords.NewPassword) < 8 {
1✔
627
                errors.ErrPasswordTooShort.Write(w)
×
628
                return
×
629
        }
×
630

631
        // get the user information from the database by email
632
        user, err := a.db.UserByEmail(userPasswords.Email)
1✔
633
        if err != nil {
1✔
634
                if err == db.ErrNotFound {
×
635
                        errors.ErrUserNotFound.Write(w)
×
636
                        return
×
637
                }
×
638
                errors.ErrGenericInternalServerError.Write(w)
×
639
                return
×
640
        }
641

642
        // check if user is OAuth-only (no password set)
643
        if user.Password == "" {
1✔
NEW
644
                errors.ErrOAuthUserCannotUsePasswordRecovery.Write(w)
×
NEW
645
                return
×
NEW
646
        }
×
647

648
        userVerification, err := a.db.UserVerificationCode(user, db.CodeTypePasswordReset)
1✔
649
        if err != nil {
1✔
650
                if err != db.ErrNotFound {
×
651
                        log.Warnw("could not get verification code", "error", err)
×
652
                }
×
653
                errors.ErrUnauthorized.Write(w)
×
654
                return
×
655
        }
656

657
        // check the verification code is not expired
658
        if userVerification.Expiration.Before(time.Now()) {
1✔
659
                errors.ErrVerificationCodeExpired.Write(w)
×
660
                return
×
661
        }
×
662

663
        code, err := internal.OpenToken(userVerification.SealedCode, userPasswords.Email, a.secret)
1✔
664
        if err != nil {
1✔
665
                errors.ErrGenericInternalServerError.Write(w)
×
666
                return
×
667
        }
×
668
        if code != userPasswords.Code {
1✔
669
                errors.ErrUnauthorized.With("code mismatch").Write(w)
×
670
        }
×
671

672
        // hash and update the new password
673
        user.Password = internal.HexHashPassword(passwordSalt, userPasswords.NewPassword)
1✔
674
        if _, err := a.db.SetUser(user); err != nil {
1✔
675
                log.Warnw("could not update user password", "error", err)
×
676
                errors.ErrGenericInternalServerError.Write(w)
×
677
                return
×
678
        }
×
679
        apicommon.HTTPWriteOK(w)
1✔
680
}
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

© 2026 Coveralls, Inc