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

vocdoni / saas-backend / 19763979759

28 Nov 2025 12:34PM UTC coverage: 62.827% (+0.08%) from 62.747%
19763979759

Pull #331

github

altergui
refactor(auth): replace hashing with encryption for verification codes

Replace one-way hashing with reversible encryption for verification code
storage and validation. This change enables the system to decrypt stored
codes back to their original values for verification purposes.

Changes:
- Replace HashVerificationCode with SealToken when generating codes
- Replace hash comparison with OpenToken when validating codes
- Add error handling for encryption/decryption operations
- Update tests to validate encrypted code format and length constraints
- Apply changes across user verification, password reset, and code resend flows

This improves the verification flow by allowing bidirectional code
transformation while maintaining security through encryption with
user-specific secrets.
Pull Request #331: refactor(auth): replace hashing with encryption for verification codes

121 of 165 new or added lines in 6 files covered. (73.33%)

4 existing lines in 2 files now uncovered.

6715 of 10688 relevant lines covered (62.83%)

39.81 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