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

vocdoni / saas-backend / 20263746296

11 Dec 2025 05:33PM UTC coverage: 63.071% (+0.3%) from 62.747%
20263746296

Pull #365

github

emmdim
feat(csp): add weight field to authentication response

Add weight field to AuthResponse structure and include it in the
sign-and-respond flow. This allows clients to receive the weight value
directly in the authentication response alongside the signature.

Also refactor error handling in signing functions by replacing generic
ErrStorageFailure with more specific ErrSign error type for better
error classification and debugging.

Changes:
- Add Weight field to AuthResponse struct with proper JSON and Swagger tags
- Include weight in signAndRespond HTTP response
- Replace ErrStorageFailure with ErrSign in prepareSaltedKeySigner and
  finishSaltedKeySigner functions
- Update Swagger documentation to reflect new weight field
Pull Request #365: v2.2.0

284 of 356 new or added lines in 20 files covered. (79.78%)

7 existing lines in 3 files now uncovered.

6794 of 10772 relevant lines covered (63.07%)

34.68 hits per line

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

41.18
/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) {
77✔
31
        userInfo := &apicommon.UserInfo{}
77✔
32
        body, err := io.ReadAll(r.Body)
77✔
33
        if err != nil {
77✔
34
                errors.ErrMalformedBody.Write(w)
×
35
                return
×
36
        }
×
37
        if err := json.Unmarshal(body, userInfo); err != nil {
78✔
38
                errors.ErrMalformedBody.Write(w)
1✔
39
                return
1✔
40
        }
1✔
41
        // check the email is correct format
42
        if !internal.ValidEmail(userInfo.Email) {
78✔
43
                errors.ErrEmailMalformed.Write(w)
2✔
44
                return
2✔
45
        }
2✔
46
        // check the password is correct format
47
        if len(userInfo.Password) < 8 {
76✔
48
                errors.ErrPasswordTooShort.Write(w)
2✔
49
                return
2✔
50
        }
2✔
51
        // check the first name is not empty
52
        if userInfo.FirstName == "" {
73✔
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 == "" {
72✔
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)
70✔
63
        // add the user to the database
70✔
64
        userID, err := a.db.SetUser(&db.User{
70✔
65
                Email:     userInfo.Email,
70✔
66
                FirstName: userInfo.FirstName,
70✔
67
                LastName:  userInfo.LastName,
70✔
68
                Password:  hPassword,
70✔
69
        })
70✔
70
        if err != nil {
71✔
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{
69✔
81
                ID:        userID,
69✔
82
                Email:     userInfo.Email,
69✔
83
                FirstName: userInfo.FirstName,
69✔
84
                LastName:  userInfo.LastName,
69✔
85
        }
69✔
86
        // generate a new verification code
69✔
87
        code, link, err := a.generateVerificationCodeAndLink(newUser, db.CodeTypeVerifyAccount)
69✔
88
        if err != nil {
69✔
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,
69✔
96
                mailtemplates.VerifyAccountNotification, struct {
69✔
97
                        Code string
69✔
98
                        Link string
69✔
99
                }{code, link},
69✔
100
        ); err != nil {
69✔
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)
69✔
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) {
64✔
125
        verification := &apicommon.UserVerification{}
64✔
126
        if err := json.NewDecoder(r.Body).Decode(verification); err != nil {
64✔
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 == "") {
64✔
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)
64✔
138
        if err != nil {
64✔
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 {
64✔
148
                errors.ErrUserAlreadyVerified.Write(w)
×
149
                return
×
150
        }
×
151
        // get the userVerification from the database
152
        userVerification, err := a.db.UserVerificationCode(user, db.CodeTypeVerifyAccount)
64✔
153
        if err != nil {
64✔
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()) {
64✔
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)
64✔
167
        if err != nil {
64✔
NEW
168
                errors.ErrGenericInternalServerError.Write(w)
×
NEW
169
                return
×
NEW
170
        }
×
171
        if code != verification.Code {
64✔
NEW
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 {
64✔
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)
64✔
183
        if err != nil {
64✔
184
                errors.ErrGenericInternalServerError.Write(w)
×
185
                return
×
186
        }
×
187
        // send the token back to the user
188
        apicommon.HTTPWriteJSON(w, res)
64✔
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
NEW
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,
×
NEW
243
                Expiration: userVerification.Expiration,
×
NEW
244
                Valid:      userVerification.Expiration.After(time.Now()),
×
UNCOV
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✔
NEW
307
                        errors.ErrGenericInternalServerError.Write(w)
×
NEW
308
                        return
×
NEW
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) {
16✔
374
        user, ok := apicommon.UserFromContext(r.Context())
16✔
375
        if !ok {
16✔
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)
16✔
381
        for _, orgInfo := range user.Organizations {
20✔
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
        // return the user information
396
        apicommon.HTTPWriteJSON(w, apicommon.UserInfo{
16✔
397
                ID:            user.ID,
16✔
398
                Email:         user.Email,
16✔
399
                FirstName:     user.FirstName,
16✔
400
                LastName:      user.LastName,
16✔
401
                Verified:      user.Verified,
16✔
402
                Organizations: userOrgs,
16✔
403
        })
16✔
404
}
405

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

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

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

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

619
        // get the user information from the database by email
620
        user, err := a.db.UserByEmail(userPasswords.Email)
1✔
621
        if err != nil {
1✔
622
                if err == db.ErrNotFound {
×
NEW
623
                        errors.ErrUserNotFound.Write(w)
×
624
                        return
×
625
                }
×
626
                errors.ErrGenericInternalServerError.Write(w)
×
627
                return
×
628
        }
629

630
        userVerification, err := a.db.UserVerificationCode(user, db.CodeTypePasswordReset)
1✔
631
        if err != nil {
1✔
NEW
632
                if err != db.ErrNotFound {
×
NEW
633
                        log.Warnw("could not get verification code", "error", err)
×
NEW
634
                }
×
NEW
635
                errors.ErrUnauthorized.Write(w)
×
NEW
636
                return
×
637
        }
638

639
        // check the verification code is not expired
640
        if userVerification.Expiration.Before(time.Now()) {
1✔
NEW
641
                errors.ErrVerificationCodeExpired.Write(w)
×
NEW
642
                return
×
NEW
643
        }
×
644

645
        code, err := internal.OpenToken(userVerification.SealedCode, userPasswords.Email, a.secret)
1✔
646
        if err != nil {
1✔
NEW
647
                errors.ErrGenericInternalServerError.Write(w)
×
NEW
648
                return
×
NEW
649
        }
×
650
        if code != userPasswords.Code {
1✔
NEW
651
                errors.ErrUnauthorized.With("code mismatch").Write(w)
×
NEW
652
        }
×
653

654
        // hash and update the new password
655
        user.Password = internal.HexHashPassword(passwordSalt, userPasswords.NewPassword)
1✔
656
        if _, err := a.db.SetUser(user); err != nil {
1✔
657
                log.Warnw("could not update user password", "error", err)
×
658
                errors.ErrGenericInternalServerError.Write(w)
×
659
                return
×
660
        }
×
661
        apicommon.HTTPWriteOK(w)
1✔
662
}
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